みなさんは、C言語で異なるポインタが同じアドレスを指していても、それらが「別物」として扱われることがあるのをご存じですか?一見すると同じメモリ位置を指すポインタでも、その由来(provenance1)によって、コンパイラの最適化や動作が変わることがあります。
この記事では、C言語の新しい国際標準ISO/IEC TS 6010:20252で定められた「provenance-aware memory object model」について、実例を交えながら徹底的に解説します。
更新 (2025-07-02): @Loofehtさんからの貴重なご指摘をいただき、記事の技術的な誤りを修正しました。特に以下の点を改善しています。
- 「コンパイラ最適化」→「手動最適化」という用語の修正
recip(&x, &x)が収束しない(発散する)ことの明記と実証- より実践的な例への差し替え
建設的なフィードバックをいただき、ありがとうございます。
背景:なぜポインタの「由来」が重要なのか?
手動最適化とエイリアシング問題
まず、以下のコードを見てください。
// 逆数の近似計算(ニュートン・ラフソン法)
void recip(double* ap, double* rp) {
for (;;) {
double prod = (*ap)*(*rp);
if ((0.999999 < prod) && (prod < 1.000001)) {
break;
} else {
(*rp) *= (2.0 - prod);
}
}
}
このコードは、*apの逆数を*rpに計算する関数です(ニュートン・ラフソン法の変形)。一見すると、以下のように手動で最適化できそうです。
// 手動最適化版(危険!)
void recip_opt(double* ap, double* rp) {
double a = *ap; // 一度だけ読み込み
double r = *rp;
for (;;) {
double prod = a*r;
if ((0.999999 < prod) && (prod < 1.000001)) {
break;
} else {
r *= (2.0 - prod);
}
}
*rp = r; // 最後に書き戻し
}
注: C23では
registerキーワードは無視されるようになったため、例から削除しました。
しかし、この手動最適化には重大な問題があります。
エイリアシングの罠
もしapとrpが同じアドレスを指していたら3どうなるでしょうか?
double x = 2.0;
recip(&x, &x); // 同じ変数のアドレスを渡す!
警告: この使い方(
ap == rp)は、アルゴリズムの設計上誤りです。ニュートン・ラフソン法では、計算対象の値(*ap)と近似値(*rp)は別々である必要があります。
| 関数 | 実際の動作 | 結果 |
|---|---|---|
recip |
x²の計算を繰り返す | 収束しない(発散) |
recip_opt |
初期値のまま計算される | 収束しない(無限ループ) |
実際にテストしたところ、recip(&x, &x)は収束せず、値が発散してしまいます:
- 初期値: x = 2.0
- 1回目: x = 2.0 × (2.0 - 4.0) = -4.0
- 2回目: x = -4.0 × (2.0 - 16.0) = 56.0
- 以降、急速に発散...
エイリアシング問題の動作シーケンス
以下のシーケンス図は、同じアドレスを渡した場合の動作の違いを示しています。
このように、ポインタのエイリアシングを考慮せずに手動で最適化すると、プログラムの意味が完全に変わってしまうのです。本来は異なるメモリ領域を指すことを前提としたアルゴリズムで、同じアドレスを渡すと予期しない動作になります。
より実践的な例:配列処理における最適化
エイリアシング問題をより正確に理解するため、実践的な配列処理の例を見てみましょう:
// 配列の要素を定数倍する関数
void scale_array(const double* src, double* dst, size_t n, double factor) {
for (size_t i = 0; i < n; i++) {
dst[i] = src[i] * factor;
}
}
// コンパイラによる最適化が可能な版(restrictを使用)
void scale_array_opt(const double* restrict src,
double* restrict dst,
size_t n, double factor) {
// srcとdstが重ならないことが保証されるため、
// コンパイラはベクトル化などの最適化が可能
for (size_t i = 0; i < n; i++) {
dst[i] = src[i] * factor;
}
}
この例では:
-
scale_arrayでは、srcとdstが重なる可能性があるため、コンパイラは保守的な最適化しかできません -
scale_array_optでは、restrictにより重ならないことが保証されるため、より積極的な最適化が可能です
基本概念:provenance(由来)とは何か?
provenanceの定義
provenanceとは、ポインタがどの「記憶域インスタンス」4から派生したかを示す情報です。
int a = 10;
int b = 20;
int *p1 = &a; // p1の由来は変数a
int *p2 = &b; // p2の由来は変数b
// p1とp2は異なる由来を持つ
記憶域インスタンスの種類
| 生成元 | 例 | 生存期間 |
|---|---|---|
| 動的割り当て | malloc(size) |
malloc()からfree()まで |
| 自動変数 | int x; |
ブロック開始から終了まで |
| 静的変数 | static int y; |
プログラム開始から終了まで |
| 複合リテラル |
(int[]){1,2,3}5
|
式の評価中 |
なぜprovenanceが必要なのか?
コンパイラは、異なる由来を持つポインタはエイリアスしないと仮定して最適化を行います。これにより、より効率的なコードを生成できます。
void example(int *p, int *q) {
*p = 10;
*q = 20;
// pとqが異なる由来なら、*pは10のまま
// 同じ由来なら、*pは20になる可能性
printf("%d\n", *p);
}
ISO/IEC TS 6010の主要条項
ポインタ比較のルール
ISO/IEC TS 6010では、ポインタの比較について以下のルールを定めています。
int a, b;
int *pa = &a;
int *pb = &b;
// 大小比較の結果は未規定(unspecified)
if (pa < pb) { /* 結果は実装依存 */ }
// 等価比較は定義済み(ただし実装定義の挙動あり)
if (pa == pb) { /* 通常はfalse */ }
露出(exposure)と合成(synthesis)
ポインタの露出
ポインタが「露出」するとは、そのアドレス情報が外部に漏れることです。
int x = 42;
int *p = &x;
// 整数への変換
uintptr_t addr = (uintptr_t)p; // 露出![^11]
// printfでの出力
printf("%p\n", p); // 露出!
// バイト単位でのアクセス
unsigned char *bytes = (unsigned char*)&p;
for (size_t i = 0; i < sizeof(p); i++) {
printf("%02x", bytes[i]); // 露出!
}
ポインタの合成
整数値からポインタを「合成」することも可能ですが、事前に露出していることが必要です。
// 露出→合成
int x = 42;
int *p1 = &x;
uintptr_t addr = (uintptr_t)p1; // 露出
int *p2 = (int*)addr; // 合成
*p2 = 100; // xは100になる
// 任意アドレスの合成は危険
int *p3 = (int*)0x12345678; // 露出なし
*p3 = 200; // 未定義動作!
注: TS 6010では、露出済み整数から合成したポインタのprovenanceは実装定義です。可搬性のためには標準APIを使用することを推奨します。
露出と合成のプロセス
以下のシーケンス図は、ポインタが露出され、その後合成される流れを示しています。
restrictとprovenanceの相互作用
restrict8はプログラマからコンパイラへの「約束」であり、provenanceはコンパイラが追跡する「事実」です。
// restrictはエイリアシングしないことの保証
void copy_array(double * restrict dst,
const double * restrict src,
size_t n) {
// コンパイラはdstとsrcが異なる由来と仮定可能
for (size_t i = 0; i < n; i++) {
dst[i] = src[i];
}
}
具体例:実践的なコード
XORリスト(メモリ節約技法)
typedef struct elem {
uintptr_t both; // prev XOR next を格納
double data;
} elem;
void elem_store(elem* e, elem* prev, elem* next, double val) {
e->both = (uintptr_t)prev ^ (uintptr_t)next;
e->data = val;
}
elem* elem_next(elem const* e, elem* prev) {
return (elem*)((uintptr_t)prev ^ e->both);
}
provenance上の問題: XORにより2つのポインタの由来が混在します。より安全な実装では
memcpy9を使用することを推奨します。
// より安全だが、メモリ効率を犠牲にした実装例
// (XORリストの利点であるメモリ節約は失われる)
typedef struct safe_elem {
unsigned char prev_bytes[sizeof(void*)];
unsigned char next_bytes[sizeof(void*)];
double data;
} safe_elem;
void safe_elem_store(safe_elem* e, void* prev, void* next, double val) {
memcpy(e->prev_bytes, &prev, sizeof(prev));
memcpy(e->next_bytes, &next, sizeof(next));
e->data = val;
}
隣接オブジェクトの問題
int A[2] = {1, 2};
int B[2] = {3, 4};
// AとBが隣接している可能性
int *end_of_A = &A[2]; // Aの終端
int *start_of_B = &B[0]; // Bの開始
// アドレス値は一致しうるが、由来は異なる
曖昧性の解決
// 両方の記憶域が露出している場合
uintptr_t addr1 = (uintptr_t)&A[0];
uintptr_t addr2 = (uintptr_t)&B[0];
// 境界のアドレスを合成
int *ambiguous = (int*)(addr1 + 2 * sizeof(int));
// 注意:以下のアクセスは未定義動作の可能性あり
// 配列Aの境界を超えたアクセスは、たとえBと隣接していても未定義
ambiguous[-1] = 10; // 未定義動作!配列境界外へのアクセス
ambiguous[0] = 20; // Bを指す場合のみ有効(実装定義)
ベストプラクティス
不要な露出を避ける
// 避けるべき
void bad_practice(int *p) {
printf("Debug: pointer = %p\n", p); // 不要な露出
uintptr_t addr = (uintptr_t)p; // 不要な変換
}
// 推奨
void good_practice(int *p) {
*p = 42; // 直接使用
}
restrict修飾子の活用
// エイリアシングしないことを明示
void matrix_multiply(double * restrict result,
const double * restrict a,
const double * restrict b,
size_t n) {
// 最適化可能
}
型ベースのエイリアス解析を活用
// 異なる型のポインタは通常エイリアスしない
void process(float *f, int *i) {
*f = 3.14;
*i = 42;
// コンパイラは*fが3.14のままと仮定可能
}
Unicode識別子に関する注意
移植性の注意: この記事の元の例では下付き文字を含むUnicode識別子12を使用していましたが、C23で正式に許可されても、既存ツールでは問題が生じる可能性があります。実務では ASCII 代替名も用意することを推奨します。
// Unicode識別子(C23で許可)
double aₚ; // 下付きp
// ASCII代替名(推奨)
double a_p; // 互換性のため
パフォーマンスへの影響
コンパイラサポート状況
| コンパイラ | バージョン | サポートフラグ |
|---|---|---|
| Clang | 17以降 | -fprovenance |
| GCC | 実験的 |
-fno-provenanceで無効化 |
| MSVC | 未対応 | - |
最適化の効果
| 最適化手法 | 効果 | 条件 |
|---|---|---|
| ループ不変式の移動 | メモリアクセス40%削減 | 異なる由来の保証 |
| レジスタ割り当て | 速度20-30%向上 | エイリアスなしの証明 |
| ベクトル化 | SIMD13命令の活用 | 独立したメモリアクセス |
C++への影響
C++標準化委員会でも、P2318R0「A Provenance-aware Memory Object Model for C++」として同様の議論が進んでいます。将来的にはC/C++間での一貫性のあるメモリモデルが期待されます。
まとめ
技術的なポイント
- provenanceは性能に直結 - 正しい由来情報により、コンパイラの最適化が有効になる
- 露出は最小限に - 不要なアドレス変換や出力は避ける
- 標準化の意義 - ISO/IEC TS 6010により、実装間の動作が統一される
実践的な教訓
// provenanceを意識したコーディング例
typedef struct {
size_t size;
int * restrict data; // エイリアスしないことを明示
} SafeArray;
SafeArray* array_create(size_t n) {
SafeArray *arr = malloc(sizeof(SafeArray));
if (!arr) return NULL;
arr->size = n;
arr->data = malloc(n * sizeof(int));
if (!arr->data && n > 0) {
free(arr);
return NULL;
}
return arr;
}
provenance memory modelは一見複雑に見えますが、その本質は「ポインタには由来がある」という単純な概念です。この概念を理解することで、より安全で効率的なCプログラムを書けるようになります。
| 用語 | 英語 | 説明 |
|---|---|---|
| 由来 | provenance | ポインタがどの記憶域インスタンスから派生したかを示す情報 |
| 記憶域インスタンス | storage instance | malloc()や変数定義によって確保される独立したメモリ領域 |
| エイリアシング | aliasing | 複数のポインタが同じメモリ領域を指すこと |
| 露出 | exposure | ポインタのアドレス情報が整数値などとして外部に漏れること |
| 合成 | synthesis | 整数値からポインタを生成すること |
| 未規定 | unspecified | 実装依存だが有効な動作 |
| 未定義 | undefined | 標準で定義されていない動作 |
もしこの記事が「面白かった!」「勉強になった!」と思ったら、ぜひ LGTM(いいね!) で応援してくださると嬉しいです。
参考資料
規格文書
- ISO/IEC TS 6010:2025 - Programming languages — C — A provenance-aware memory object model (DOI: 10.5594/ISO-IEC-TS-6010-2025)
- ISO/IEC TS 6010:2025 (IEC Webstore)
学術論文・技術文書
- A Provenance-aware Memory Object Model for C (N2577) - Peter Sewell et al.
- N2222: Further Pointer Issues - WG14 技術文書
- P2318: A Provenance-aware Memory Object Model for C++ - C++への拡張提案
解説記事・ブログ
- Cambridge University - C Memory Object Model Study
- Jens Gustedt's Blog - The provenance memory model for C
- Cambridge Cerberus Project
-
**provenance(プロヴィナンス)**は、「由来」「出所」を意味する英語で、美術品や考古学的遺物の来歴を示す用語としても使われます。プログラミングの文脈では、ポインタがどこから派生したかを示す概念として使用されます。 ↩
-
ISO/IEC TS(Technical Specification)は、国際標準化機構(ISO)と国際電気標準会議(IEC)が発行する技術仕様書です。正式な国際規格(IS)になる前の段階の文書で、新しい技術や概念を試験的に導入する際に使用されます。 ↩
-
エイリアシング(aliasing)とは、複数のポインタや参照が同じメモリ領域を指すことです。この状況では、一方のポインタ経由でメモリを変更すると、他方のポインタ経由でアクセスした値も変わります。 ↩
-
記憶域インスタンス(storage instance)は、プログラムが使用できる独立したメモリ領域のことです。変数の宣言、動的メモリ確保(malloc等)、静的変数の定義などによって生成されます。 ↩
-
複合リテラル(compound literal)は、C99で導入された機能で、名前のない配列や構造体を式の中で直接作成できる構文です。
(型名){初期化子リスト}の形式で記述します。 ↩ -
未規定(unspecified)の動作とは、複数の可能な動作のうちどれが選ばれるかが規格で定められていないものです。実装は実行ごとに異なる動作を選択する可能性があり、移植性のあるプログラムはどの動作でも正しく動作する必要があります。 ↩
-
未定義(undefined)の動作とは、規格で全く定義されていない動作です。プログラムがクラッシュしたり、予期しない結果になったり、見た目上正常に動作したりする可能性があります。未定義動作は必ず避けるべきです。 ↩
-
restrictは、C99で導入されたポインタ修飾子で、そのポインタが指すオブジェクトに他のポインタ経由でアクセスしないことをコンパイラに保証します。これにより、コンパイラはより積極的な最適化が可能になります。 ↩
-
memcpyは、
<string.h>で定義される標準ライブラリ関数で、メモリブロックの内容をバイト単位でコピーします。型情報を無視してバイトレベルでコピーするため、ポインタのバイト表現を保持したままコピーできます。 ↩ -
アラインメント(alignment)は、データがメモリ上でどのようなアドレス境界に配置されるかを示す制約です。例えば、4バイトアラインメントの場合、アドレスは4の倍数になります。 ↩
-
パディング(padding)は、アラインメント要求を満たすために構造体のメンバー間やメンバーの後に挿入される未使用のバイトです。コンパイラが自動的に挿入します。 ↩
-
Unicode識別子は、プログラミング言語でASCII文字以外のUnicode文字を変数名や関数名に使用できる機能です。C23では、下付き文字や上付き文字、各国の文字などが識別子に使用可能になりました。 ↩
-
SIMD(Single Instruction, Multiple Data)は、1つの命令で複数のデータを同時に処理する並列計算技術です。SSE、AVX、NEONなどの命令セットがあり、ベクトル演算の高速化に使用されます。 ↩

Comments
Let's comment your feelings that are more than good