他の人が言ったように、ifはC ++ 14の時点では何も意味しないので__restrict__
、C99と同じように機能するGCC拡張を考えてみましょうrestrict
。
C99
restrict
2つのポインタが重複するメモリ領域を指すことはできないと述べています。最も一般的な使用法は関数の引数です。
これにより、関数の呼び出し方法が制限されますが、より多くのコンパイル最適化が可能になります。
呼び出し元がrestrict
契約に従わない場合、動作は未定義です。
C99 N1256ドラフト 6.7.3 / 7「タイプ修飾子」と言います。
制限修飾子(レジスタストレージクラスなど)の使用目的は、最適化を促進することであり、適合プログラムを構成するすべての前処理翻訳単位から修飾子のすべてのインスタンスを削除しても、その意味(つまり、観察可能な動作)は変わりません。
6.7.3.1「制限の正式な定義」では、詳細について説明しています。
可能な最適化
Wikipediaの例では、され、非常照明します。
これは、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は本当にそれをしますか?
g++
4.8 Linux x86-64:
g++ -g -std=gnu++98 -O0 -c main.cpp
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, size_t size) {
for (size_t i = 0; i < size; i++) {
p1[i] = 4;
p2[i] = 9;
}
}
のためrestrict
、スマートコンパイラ(または人間)はそれを次のように最適化できます。
memset(p1, 4, size);
memset(p2, 9, size);
まともなlibc実装(glibcなど)でアセンブリ最適化される可能性があるため、潜在的にはるかに効率的です。パフォーマンスの観点から、std :: memcpy()またはstd :: copy()を使用する方が良いですか?おそらくSIMD命令で。
制限なしでは、この最適化は実行できません。たとえば、次のことを考慮してください。
char p1[4];
char *p2 = &p1[1];
f(p1, p2, 3);
次にfor
バージョンは次のようになります:
p1 == {4, 4, 4, 9}
しばらくmemset
バージョンが作ります:
p1 == {4, 9, 9, 9}
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
予想通り2つの呼び出し。
制限なし:stdlib呼び出しなし、ここで再現するつもりはない16回の反復幅のループ展開のみ :-)
私はそれらをベンチマークする忍耐力がありませんでしたが、制限バージョンの方が速くなると思います。
厳格なエイリアシングルール
restrict
キーワードは、互換性のあるタイプのポインタ(例えば、2に影響を与えint*
、厳密なエイリアシング規則は互換性のない型をエイリアシングすると、デフォルトでは未定義の動作であることを言うので)、およびコンパイラが想定できるので、それが離れて起こると最適化しません。
参照:厳密なエイリアスルールとは何ですか?
参考資料として機能しますか?
GCCのドキュメントによると、次のようになっています:https : //gcc.gnu.org/onlinedocs/gcc-5.1.0/gcc/Restricted-Pointers.html構文:
int &__restrict__ rref
this
メンバー関数のバージョンもあります:
void T::fn () __restrict__
restrict
c99キーワードです。はい、Rpbert S. Barnesさん、ほとんどのコンパイラがをサポートしていることを知ってい__restrict__
ます。アンダースコアが2つあるものはすべて、定義上、実装固有であり、したがってC ++ではなく、コンパイラ固有のバージョンであることに注意してください。