C99 の「restrict」キーワードの現実的な使用法は?
-
09-09-2019 - |
質問
いくつかのドキュメントと質問/回答を閲覧していて、それが言及されているのを見つけました。簡単な説明を読んだところ、基本的にはポインタが他の場所を指すために使用されないというプログラマからの約束になると述べられていました。
これを実際に使用する価値があるいくつかの現実的なケースを誰かが提供できますか?
解決
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
、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 a may be equal to x.
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
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 つの命令。
配列
これまでのところ、単一命令の節約ができていますが、ポインタがループされる配列を表す場合(一般的な使用例)、前述したように、大量の命令を節約できる可能性があります。 スーパーキャット.
たとえば、次のことを考えてみましょう。
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
二
memset
予想通りに電話がかかります。制限なし:stdlib 呼び出しはなく、反復幅は 16 回だけです ループ展開 ここで再現するつもりはありません:-)
ベンチマークを行う忍耐力はありませんが、制限付きバージョンの方が高速であると信じています。
C99
完全を期すための基準を見てみましょう。
restrict
2 つのポインタが重複するメモリ領域を指すことはできないと述べています。最も一般的な使用法は関数の引数です。
これにより、関数の呼び出し方法が制限されますが、コンパイル時の最適化がさらに可能になります。
発信者が指示に従わない場合、 restrict
契約、未定義の動作。
の C99 N1256 ドラフト 6.7.3/7「型修飾子」には次のように書かれています。
制限修飾子 (レジスタ ストレージ クラスなど) の使用目的は、最適化を促進することであり、適合プログラムを構成するすべての前処理翻訳単位から修飾子のすべてのインスタンスを削除しても、その意味 (つまり、観察可能な動作) は変わりません。
6.7.3.1「制限の正式な定義」に悲惨な詳細が記載されています。
厳密なエイリアス規則
の restrict
キーワードは、互換性のある型のポインタにのみ影響します (例:二 int*
) 厳密なエイリアス規則では、互換性のない型のエイリアスはデフォルトでは未定義の動作であると規定されているため、コンパイラはそれが起こらないと想定して最適化を行うことができます。
こちらも参照
- C++14 にはまだ類似した機能がありません。
restrict
, しかし、GCC には__restrict__
拡張子として: C++ では、restrict キーワードは何を意味しますか? - 多くの質問は次のとおりです。悲惨な詳細によると、このコードは UB ですか?
- 「いつ使用するか」という質問: 制限を使用する場合と使用しない場合
- 関連する GCC
__attribute__((malloc))
, これは、関数の戻り値が何にもエイリアス化されていないことを示しています。 GCC:__attribute__((malloc))