C99の「restrict」キーワードの現実的な使用法


183

私はいくつかのドキュメントと質問/回答を閲覧していて、それが言及されているのを見ました。私は簡単な説明を読み、基本的にはポインターが他の場所を指すために使用されないことがプログラマーからの約束であると述べました。

誰かが実際にこれを使用する価値があるいくつかの現実的なケースを提供できますか?


4
memcpyvs memmoveは標準的な例の1つです。
AlexandreC。16年

@AlexandreC .:「制限」修飾子の欠如は、プログラムロジックがソースと宛先のオーバーロードで機能することを意味せず、そのような修飾子の存在が呼び出されたメソッドを妨げないため、特に当てはまるとは思わないソースと宛先が重複しているかどうかを判断し、重複している場合は、destをsrc +(dest-src)で置き換えます。
スーパーキャット2016

@supercat:だからコメントにした。ただし、1)のrestrict引数を修飾するmemcpyことで、基本的にナイーブな実装を積極的に最適化memcpyできます。2)呼び出しを行うだけで、コンパイラーに与えられた引数がエイリアスではないことを想定できるため、memcpy呼び出しに関する最適化が可能になります。
AlexandreC。16年

@AlexandreC .:ほとんどのプラットフォームのコンパイラーが、単純なmemcpyを最適化することは、「制限」があっても、ターゲットに合わせたバージョンと同じくらい効率的な場所になるようにするのは非常に困難です。呼び出し側の最適化では「restrict」キーワードは必要ありません。場合によっては、それらを容易にするための努力は逆効果になることがあります。たとえば、memcpyの多くの実装では、追加コストmemcpy(anything, anything, 0);なしで何もしないと見なし、if pが少なくともn書き込み可能なバイトへのポインタであることを確認できますmemcpy(p,p,n)。副作用はありません。このようなケースが発生する可能性があります...
スーパーキャット

...当然のことながら、特定の種類のアプリケーションコード(たとえば、アイテムをそれ自体と入れ替えるソートルーチン)、および悪影響がない実装では、これらのケースを一般的なケースのコードで処理する方が、特殊なケースのテストを追加します。残念ながら、一部のコンパイラ作成者は、コンパイラがほとんど利用しない「最適化の機会」を促進するために、コンパイラが最適化できないコードを追加することを要求する方が良いと考えているようです。
スーパーキャット

回答:


182

restrictポインタは、基になるオブジェクトにアクセスする唯一のものであると述べています。ポインターのエイリアシングの可能性を排除し、コンパイラーによる最適化を改善します。

たとえば、メモリ内の数値のベクトルを乗算できる特殊な命令を備えたマシンがあり、次のコードがあるとします。

void MultiplyArrays(int* dest, int* src1, int* src2, int n)
{
    for(int i = 0; i < n; i++)
    {
        dest[i] = src1[i]*src2[i];
    }
}

コンパイラが適切であれば処理する必要がありdestsrc1src2それは最初から最後まで、一度に一つの乗算を行う必要がありますつまり、重複を。を持つことrestrictにより、コンパイラーはベクトル命令を使用してこのコードを自由に最適化できます。

Wikipediaには、restrict別の例を挙げて、にエントリがあります


3
@Michael-私が間違えていなければ、問題はdestソースベクトルのいずれかと重複している場合に限られます。オーバーラップするsrc1となぜ問題が発生src2するのですか?
ysap

1
通常、restrictは、変更されたオブジェクトを指している場合にのみ効果があります。その場合、隠された副作用を考慮する必要がないとアサートされます。ほとんどのコンパイラは、ベクトル化を容易にするためにそれを使用します。Msvcは、その目的でデータのオーバーラップにランタイムチェックを使用します。
tim18

forループ変数にregisterキーワードを追加すると、restrictが追加されるだけでなく、処理が速くなります。

2
実際、registerキーワードは単なる助言です。また、2000年以降のコンパイラでは、例のi(および比較のためのn)は、registerキーワードを使用するかどうかにかかわらず、レジスタに最適化されます。
Mark Fischler、2018年

154

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は本当にそれをしますか?

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

    memset予想通り2つの呼び出し。

  • 制限なし:stdlib呼び出しなし、ここで再現するつもりはない16回の反復幅のループ展開のみ :-)

私はそれらをベンチマークする忍耐力がありませんでしたが、制限バージョンの方が速くなると思います。

C99

完全を期すために標準を見てみましょう。

restrict2つのポインタが重複するメモリ領域を指すことはできないと述べています。最も一般的な使用法は関数の引数です。

これにより、関数の呼び出し方法が制限されますが、コンパイル時の最適化が強化されます。

呼び出し元がrestrict契約に従わない場合、動作は未定義です。

C99 N1256ドラフト 6.7.3 / 7「タイプ修飾子」と言います。

制限修飾子(レジスタストレージクラスなど)の使用目的は、最適化を促進することであり、適合プログラムを構成するすべての前処理翻訳単位から修飾子のすべてのインスタンスを削除しても、その意味(つまり、観察可能な動作)は変わりません。

6.7.3.1「制限の正式な定義」では、詳細について説明しています。

厳格なエイリアシングルール

restrictキーワードは、互換性のあるタイプのポインタ(例えば、2に影響を与えint*、厳密なエイリアシング規則は互換性のない型をエイリアシングすると、デフォルトでは未定義の動作であることを言うので)、およびコンパイラが想定できるので、それが離れて起こると最適化しません。

参照:厳密なエイリアスルールとは何ですか?

こちらもご覧ください


9
「制限」修飾子を使用すると、実際には大幅に節約できます。たとえば、が指定されているvoid zap(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);」に書き換えることができます。制限はタイプベースのエイリアシングよりもはるかに優れています。コンパイラが後者に重点を置くのは残念です。
スーパーキャット2016年

回答に追加された@supercatの優れた例。
Ciro Santilli郝海东冠状病六四事件法轮功

2
@ tim18:「restrict」キーワードは、積極的なタイプベースの最適化でさえもできない多くの最適化を有効にすることができます。さらに、積極的な型ベースのエイリアスとは異なり、言語に "restrict"が存在するため、タスクが存在しない場合に可能な限り効率的にタスクを実行することは不可能になりません( "restrict"によって破壊されるコードは単純に積極的なTBAAによって破壊されたコードは、多くの場合、非効率的な方法で書き直す必要があります)。
スーパーキャット2016年

2
@ tim18:のように、バックティックで二重下線を含むものを囲み__restrictます。そうしないと、二重下線が叫んでいることを示すものとして誤って解釈される可能性があります。
スーパーキャット2016

1
叫んでいないことよりも重要なことは、アンダースコアがあなたがしようとしているポイントに直接関連する意味を持つということです。
2018
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.