17

キーワードのrestrict動作は、C99で6.7.3.1によって定義されています。

Dを、オブジェクトPをタイプTへの制限修飾ポインターとして指定する手段を提供する通常の識別子の宣言とします。

Dがブロック内に表示され、ストレージクラスexternがない場合は、Bがブロックを示します。関数定義のパラメーター宣言のリストにDが表示されている場合は、Bが関連するブロックを示しているとします。それ以外の場合は、Bがメインのブロック(または自立環境でのプログラムの起動時に呼び出される関数のブロック)を示します。

以下では、ポインタ式Eは、(Eの評価前のBの実行のあるシーケンスポイントで)以前にポイントした配列オブジェクトのコピーを指すようにPを変更する場合、オブジェクトPに基づくと言われます。 E.119の値を変更します)'' based''は、ポインタ型の式に対してのみ定義されていることに注意してください。

Bを実行するたびに、LをPに基づく&Lを持つ任意の左辺値とします。Lが指定されたオブジェクトXの値にアクセスするために使用され、Xも(何らかの方法で)変更される場合、次の要件が適用されます。 :Tはconst-qualifiedであってはなりません。Xの値にアクセスするために使用される他のすべての左辺値も、Pに基づくアドレスを持つものとします。Xを変更するすべてのアクセスは、この節の目的のために、Pを変更することも考慮されるものとします。ブロックB2に関連付けられた別の制限付きポインタオブジェクトP2に基づくポインタ式Eの値がPに割り当てられている場合、B2の実行はBの実行の前に開始するか、B2の実行はBの実行の前に終了する必要があります。割り当て。これらの要件が満たされていない場合、動作は未定義です。

他のほとんどの人と同じように、私はこの定義のすべての複雑さを理解するのに苦労しています。この質問への回答として、4番目の段落の各要件について、要件に違反する使用法の一連の良い例を見てみたいと思います。この記事:

http://web.archive.org/web/20120225055041/http://developers.sun.com/solaris/articles/cc_restrict.html

「コンパイラが想定するかもしれない...」という観点からルールを提示するのに良い仕事をします。そのパターンを拡張し、コンパイラーが行うことができる仮定と、それらがどのように成り立たないかを、それぞれの例で結び付けることは素晴らしいことです。

4

1 に答える 1

8

以下では、質問にリンクされているSunの論文のユースケースを参照します。

(比較的)明白なケースはmem_copy()ケースであり、これはSunペーパー(f1()関数)の2番目のユースケースカテゴリに分類されます。次の2つの実装があるとしましょう。

void mem_copy_1(void * restrict s1, const void * restrict s2, size_t n);
void mem_copy_2(void *          s1, const void *          s2, size_t n);

s1とs2が指す2つの配列の間に重複がないことがわかっているため、1番目の関数のコードは単純です。

void mem_copy_1(void * restrict s1, const void * restrict s2, size_t n)
{
     // naively copy array s2 to array s1.
     for (int i=0; i<n; i++)
         s1[i] = s2[i];
     return;
}

s2 = '....................1234567890abcde' <- s2 before the naive copy
s1 = '1234567890abcde....................' <- s1 after the naive copy
s2 = '....................1234567890abcde' <- s2 after the naive copy

OTOH、2番目の関数では、重複がある可能性があります。この場合、ソース配列が宛先の前にあるか、またはその逆であるかを確認し、それに応じてループインデックス境界を選択する必要があります。

たとえば、s1 = 100s2 = 105。次に、の場合n=15、コピー後、新しくコピーされたs1配列はソース配列の最初の10バイトをオーバーランしますs2。最初に下位バイトをコピーしたことを確認する必要があります。

s2 = '.....1234567890abcde' <- s2 before the naive copy
s1 = '1234567890abcde.....' <- s1 after the naive copy
s2 = '.....67890abcdeabcde' <- s2 after the naive copy

ただし、、、の場合、最初に下位バイトを書き込むと、ソースの最後の10バイトがオーバーランs1 = 105し、誤ったコピーが発生します。s2 = 100s2

s2 = '1234567890abcde.....' <- s2 before the naive copy
s1 = '.....123451234512345' <- s1 after the naive copy - not what we wanted
s2 = '123451234512345.....' <- s2 after the naive copy

この場合、最初に配列の最後のバイトをコピーする必要があり、場合によっては逆方向にステップします。コードは次のようになります。

void mem_copy_2(void *s1, const void *s2, size_t n)
{
    if (((unsigned) s1) < ((unsigned) s2))
        for (int i=0; i<n; i++)
             s1[i] = s2[i];
    else
        for (int i=(n-1); i>=0; i--)
             s1[i] = s2[i];
    return;
}

restrictモディファイアがどのように速度の最適化を改善し、余分なコードを排除し、if-elseを決定するかを簡単に理解できます。

同時に、この状況は、重複する配列をrestrict-ed関数に渡す不注意なプログラマーにとって危険です。この場合、アレイの適切なコピーを保証するためのガードはありません。コンパイラーによって選択された最適化パスに応じて、結果は未定義です。


最初のユースケース(init()関数)は、上記の2番目のユースケースのバリエーションと見なすことができます。ここでは、1回の動的メモリ割り当て呼び出しで2つの配列が作成されます。

2つのポインターをrestrict-edとして指定すると、命令の順序が重要になる最適化が可能になります。たとえば、次のコードがある場合:

a1[5] = 4;
a2[3] = 8;

次に、オプティマイザは、有用であると判断した場合、これらのステートメントを並べ替えることができます。

OTOH、ポインタが-edされていない場合は、最初の割り当てが2番目の割り当ての前に実行されることが重要です。 これは、とが実際には同じメモリ位置restrictである可能性があるためです。この場合、最終値は8になるはずです。命令を並べ替えると、最終値は4になります。a1[5]a2[3]

restrict繰り返しますが、この-edの想定コードに互いに素でないポインターが与えられた場合、結果は未定義です。

于 2012-09-04T03:05:50.347 に答える