現代のプログラミング言語ではポインタは単なるアドレスではなく,provenanceを伴った参照として扱われています.
世界は既に変わっています.
概要
- ポインタは単なるアドレスではありません.
- ポインタにはprovenanceという,どのオブジェクト由来かの情報が含まれています.
- Provenanceを使うことで,最適化が効きやすくなったり,堅牢なプログラムを書きやすくなったりします.
追記: 次の英語記事を読むとprovenanceが必要な理由についてもっとよく知ることができます.クリックしよう!!!!(2020-12-15)
https://www.ralfj.de/blog/2020/12/14/provenance.html
ポインタはアドレスではない
次のCプログラムを見てみましょう.
#include <stdio.h>
#include <string.h>
int main() {
int a = 100;
int b = 200;
int *p = &a + 1;
int *q = &b;
printf("p = %p, q = %p, (p == q) = %d\n", p, q, p == q);
if (p == q) {
printf("p == q!\n");
}
printf("a = %d, b = %d", a, b);
}
このコードをGCC 10.1.0で実行すると次の結果が得られます.
p = 0x7ffd5d140d1c, q = 0x7ffd5d140d1c, (p == q) = 0
a = 100, b = 200
注目すべき点は,ポインタp
とq
がそれぞれ0x7ffd5d140d1c
という同じアドレス値1をとっているのにも関わらず,p == q
という比較が偽を返している(p
とq
とが異なるポインタである)ということです.
つまり,ポインタの等しさにはアドレス以外の何者かが関わってくるのです!
それはポインタのprovenance(由来)と呼ばれるものです.provenanceはポインタがどのオブジェクトから由来するものかを表します.さきほどのプログラムの場合はポインタp
のprovenanceは変数a
,ポインタq
は変数b
となっています.ポインタのprovenanceが異なっているため,p
とq
は(たとえアドレスが同じでも)異なるポインタと判定しても良いのです.
注: 変更前のバージョンでは上の段落は「provenanceが異なるポインタは『必ず』異なると判定される」と読める文章でした,これは意図(provenanceが異なるポインタは異なると判定『しても良い』)とは異なるので訂正します.例えば,プロセッサ上では通常アドレス数値のみを用いてポインタを扱うので,provenanceが異なっていてもアドレスが等しいポインタが==
の意味で等しいと判定しても良いです.(2020-09-08)
注意しなければならないのは,このようなprovenanceを用いた推論は既に行われているという点です.文書としての初出は2004年です.これはC20の提案などではなく,10年以上続いている現状なのです2.
Provenanceの何が嬉しいのか
最適化能力の向上
コンパイラはprovenance情報をポインタのエイリアス解析に活用しています.
つまり,provenanceを解析することでポインタを経由した読み込み/書き込みの影響範囲を計算でき,この情報によって最適化が可能かどうかより正確に判定できるようになります.
例えば,次の関数
int func(int *p) {
int x = 100;
*p = 300;
return x;
}
は直観的には
int func2(int *p) {
*p = 300;
return 100; // xの値を伝播させた
}
と最適化することができそうです.
この最適化を正当化するためには「ポインタp
が変数x
を指すことは無い」ということを証明する必要があります.
ここで,ポインタを単なるアドレスと同一視した場合,
func((int*)rand()); // たまたまxのアドレスが出てしまった!
というケースが存在するのでfunc → func2
の最適化が不正なものとなってしまいます.
一方で,provenanceを使った場合は(int*)rand()
がx
のprovenanceを持つことはありません(まだx
が登場していないため).ですから,func → func2
という最適化が正当化できます.
サニタイザの強化
ポインタのprovenanceは「オブジェクトを指すもの」としてのポインタを捉えた概念です.
これを活用することでプログラムの堅牢性を向上させることもできるかもしれません.
Cプログラミングではサニタイザを使ってプログラムが不正な動作を起こしていないかどうかテストし,プログラムの堅牢性を高めることがよく行われています.
例えば,AddressSanitizerやValgrindといったサニタイザを利用することで,バッファオーバーフローやメモリリークといったバグを検出することが可能です.
ですが,サニタイザは全てのバグを検出できるわけではありません.
この記事の最初に取り上げたプログラムの場合,ポインタp
は大元の変数a
の範囲からははみ出ていますから,ある意味でバッファオーバーフローが起きていると言えます3.
しかし,ポインタp
のアドレス値は変数b
のアドレスであり,アドレスだけを見た場合はこのバッファオーバーフローは検出できません.
しかし,ポインタのprovenanceも考慮した場合,ポインタp
は変数a
の範囲内に収まっていなければならないということがサニタイザからも分かるので,バッファオーバーフローの検出が可能です.
Provenance的な推論を行うサニタイザは既に登場しつつあります.
例えば,RustのMIRIインタプリタにはサニタイザが標準搭載されており,未初期化メモリやダングリングポインタといった典型的な未定義動作だけではなくprovenance的な未定義動作の検出4も可能です.
MIRIインタプリタはRust Playgroundからも簡単に実行でき,unsafe Rustを書く人々に重用されています.C言語にも同様のサニタイザが導入される未来は想像に難くありません.
更に詳しく
https://stefansf.de/post/pointers-are-more-abstract-than-you-might-expect/ ではほとんど同じ例(似ているのはたまたまです)について詳しく考察をしています.
残念ながらこのブログ記事の結論に自分は同意しない(aggregateは1つのオブジェクトをなすと自分は考えています)のですが,よく書かれています.
ポインタのprovenanceの取り扱いは確定したとは言い難く,プログラミング言語研究における最先端の一つと言えるでしょう.
例えば,次のような問題に答えていく必要があります.
- ポインタを
memcpy
したときのprovenanceはどうなる? - ポインタ↔整数キャストの場合は?
- MMIO等特殊なポインタの取り扱いは?
__attribute__((malloc))
気になる方は次の文献をチェックしてみるのはいかがでしょうか.
- Ralf Jung (Rust研究者)のブログ.分かりやすいのでオススメ
- Cのprovenance解説文書
- C2xへのprovenance提案
- 自分もいくつかprovenance関連の論文メモを残しています
謝辞
素晴らしいタイトルを提供してくれた親友に感謝します.
-
アドレス値自体は実行ごとに変わります. ↩
-
C2xへのprovenance提案は現状を明文化するものと捉えるのが良いでしょう. ↩
-
厳密には1個先のアドレスはアクセスしない限り大丈夫なはずですが,ここでは問題があるということにします. ↩
-
Rustの場合はprovenanceではなくStacked Borrowsと呼ばれる定式化を用いて参照の由来を表現しています. ↩