いくつかのドキュメントと質問/回答を閲覧していて、それが言及されているのを見ました。簡単な説明を読みましたが、それは基本的に、ポインターが他の場所を指すために使用されないというプログラマーからの約束であると述べています。
これを実際に使用する価値がある現実的なケースを誰かが提供できますか?
いくつかのドキュメントと質問/回答を閲覧していて、それが言及されているのを見ました。簡単な説明を読みましたが、それは基本的に、ポインターが他の場所を指すために使用されないというプログラマーからの約束であると述べています。
これを実際に使用する価値がある現実的なケースを誰かが提供できますか?
restrict
は、ポインターが基になるオブジェクトにアクセスする唯一のものであると述べています。これにより、ポインターのエイリアシングの可能性がなくなり、コンパイラーによる最適化が向上します。
たとえば、メモリ内の数値のベクトルを乗算できる特殊な命令を備えたマシンがあり、次のコードがあるとします。
void MultiplyArrays(int* dest, int* src1, int* src2, int n)
{
for(int i = 0; i < n; i++)
{
dest[i] = src1[i]*src2[i];
}
}
dest
コンパイラは、if 、src1
、およびオーバーラップを適切に処理する必要がsrc2
あります。つまり、最初から最後まで、一度に 1 つの乗算を実行する必要があります。を持つことによりrestrict
、コンパイラはベクトル命令を使用してこのコードを自由に最適化できます。
ウィキペディアには にエントリがrestrict
あり、別の例がここにあります。
ウィキペディアの例は非常にわかりやすいです。
1 つのアセンブリ インストラクションを保存する方法が明確に示されています。
制限なし:
void f(int *a, int *b, int *x) {
*a += *x;
*b += *x;
}
疑似アセンブリ:
load R1 ← *x ; Load the value of x pointer
load R2 ← *a ; Load the value of a pointer
add R2 += R1 ; Perform Addition
set R2 → *a ; Update the value of a pointer
; Similarly for b, note that x is loaded twice,
; because x may point to a (a aliased by x) thus
; the value of x will change when the value of a
; changes.
load R1 ← *x
load R2 ← *b
add R2 += R1
set R2 → *b
制限あり:
void fr(int *restrict a, int *restrict b, int *restrict x);
疑似アセンブリ:
load R1 ← *x
load R2 ← *a
add R2 += R1
set R2 → *a
; Note that x is not reloaded,
; because the compiler knows it is unchanged
; "load R1 ← *x" is no longer needed.
load R2 ← *b
add R2 += R1
set R2 → *b
GCCは本当にそれを行いますか?
GCC 4.8 Linux x86-64:
gcc -g -std=c99 -O0 -c main.c
objdump -S main.o
と-O0
、同じです。
と-O3
:
void f(int *a, int *b, int *x) {
*a += *x;
0: 8b 02 mov (%rdx),%eax
2: 01 07 add %eax,(%rdi)
*b += *x;
4: 8b 02 mov (%rdx),%eax
6: 01 06 add %eax,(%rsi)
void fr(int *restrict a, int *restrict b, int *restrict x) {
*a += *x;
10: 8b 02 mov (%rdx),%eax
12: 01 07 add %eax,(%rdi)
*b += *x;
14: 01 06 add %eax,(%rsi)
初心者向けの呼び出し規約は次のとおりです。
rdi
= 最初のパラメーターrsi
= 2 番目のパラメーターrdx
= 3 番目のパラメーターGCC の出力は、wiki の記事よりもさらに明確でした: 4 つの命令と 3 つの命令。
配列
これまでのところ、単一の命令の節約がありますが、ポインターがループする配列を表している場合、一般的なユースケースであり、supercatで述べたように、一連の命令を節約できます。
たとえば、次のように考えてください。
void f(char *restrict p1, char *restrict p2) {
for (int i = 0; i < 50; i++) {
p1[i] = 4;
p2[i] = 9;
}
}
のおかげでrestrict
、スマート コンパイラ (または人間) は、次のように最適化できます。
memset(p1, 4, 50);
memset(p2, 9, 50);
まともなlibc実装(glibcなど)でアセンブリ最適化される可能性があるため、これは潜在的にはるかに効率的です:パフォーマンスの観点から、std::memcpy()またはstd::copy()を使用する方が良いですか?
GCCは本当にそれを行いますか?
GCC 5.2.1.Linux x86-64 Ubuntu 15.10:
gcc -g -std=c99 -O0 -c main.c
objdump -dr main.o
では-O0
、どちらも同じです。
と-O3
:
制限あり:
3f0: 48 85 d2 test %rdx,%rdx
3f3: 74 33 je 428 <fr+0x38>
3f5: 55 push %rbp
3f6: 53 push %rbx
3f7: 48 89 f5 mov %rsi,%rbp
3fa: be 04 00 00 00 mov $0x4,%esi
3ff: 48 89 d3 mov %rdx,%rbx
402: 48 83 ec 08 sub $0x8,%rsp
406: e8 00 00 00 00 callq 40b <fr+0x1b>
407: R_X86_64_PC32 memset-0x4
40b: 48 83 c4 08 add $0x8,%rsp
40f: 48 89 da mov %rbx,%rdx
412: 48 89 ef mov %rbp,%rdi
415: 5b pop %rbx
416: 5d pop %rbp
417: be 09 00 00 00 mov $0x9,%esi
41c: e9 00 00 00 00 jmpq 421 <fr+0x31>
41d: R_X86_64_PC32 memset-0x4
421: 0f 1f 80 00 00 00 00 nopl 0x0(%rax)
428: f3 c3 repz retq
予想どおり2回のmemset
呼び出し。
制限なし: stdlib 呼び出しはなく、ここで再現するつもりはない16 回の反復ワイドループ展開だけです :-)
それらをベンチマークする忍耐力はありませんでしたが、制限バージョンの方が高速になると思います。
C99
完全を期すために標準を見てみましょう。
restrict
は、2 つのポインタが重複するメモリ領域を指すことはできないと述べています。最も一般的な使用法は、関数の引数です。
これにより、関数の呼び出し方法が制限されますが、コンパイル時の最適化が可能になります。
restrict
呼び出し元が契約に従わない場合、未定義の動作。
C99 N1256 ドラフト6.7.3/7「型修飾子」には次のように書かれています。
restrict 修飾子 (register ストレージ クラスなど) の使用目的は、最適化を促進することであり、適合プログラムを構成するすべての前処理翻訳単位から修飾子のすべてのインスタンスを削除しても、その意味 (つまり、観察可能な動作) は変わりません。
および 6.7.3.1「restrict の正式な定義」では、詳細を説明しています。
厳密なエイリアシング ルール
このrestrict
キーワードは、互換性のある型 (例: two int*
) のポインターにのみ影響します。これは、厳密なエイリアシング規則により、互換性のない型のエイリアシングはデフォルトでは未定義の動作であることが示されているため、コンパイラーはそれが起こらないと想定して最適化して取り除くことができるためです。
こちらもご覧ください
restrict
GCC には__restrict__
拡張機能があります: C++ での restrict キーワードの意味は何ですか?__attribute__((malloc))
は、関数の戻り値が何にもエイリアスされていないことを示しています: GCC: __attribute__((malloc))