Cでrestrict
キーワードを使用しない場合と使用しない場合、およびそれが具体的なメリットをもたらす状況を理解しようとしています。
「 Destricting The Restrict Keyword "」(これは使用法に関する経験則を提供します)を読んだ後、関数にポインターが渡されると、データの可能性を考慮する必要があるという印象を受けます指摘されたものが、関数に渡される他の引数と(エイリアスで)重複する可能性があります。与えられた関数:
_foo(int *a, int *b, int *c, int n) {
for (int i = 0; i<n; ++i) {
b[i] = b[i] + c[i];
a[i] = a[i] + b[i] * c[i];
}
}
_
コンパイラは2番目の式でc
をリロードする必要があります。これは、おそらくb
とc
が同じ場所を指しているためです。また、同じ理由でb
をロードする前に、a
が格納されるのを待つ必要があります。次に、a
が格納されるまで待機する必要があり、次のループの開始時にb
およびc
を再ロードする必要があります。このような関数を呼び出すと:
_int a[N];
foo(a, a, a, N);
_
次に、コンパイラがこれを行う必要がある理由を確認できます。 restrict
を使用すると、これを絶対に行わないことをコンパイラーに効果的に通知できるため、c
の冗長ロードを削除し、a
が格納される前にb
をロードできます。 。
別のSO投稿では、Nils Pipenbrinckが、パフォーマンスの利点を示すこのシナリオの実用的な例を提供しています。
これまでのところ、インライン化されない関数に渡すポインターにrestrict
を使用することをお勧めします。どうやらコードがインライン化されている場合、コンパイラーはポインターが重複していないことを理解できます。
さて、ここからがぼやけ始めます
Ulrich Drepperの論文では、「 すべてのプログラマーがメモリについて知っておくべきこと "彼は、「制限が使用されない限り、すべてのポインタアクセスがエイリアシングの潜在的な原因である」と述べ、具体的なコード例を示しています。彼がrestrict
を使用する部分行列行列乗算の例。
ただし、restrict
を使用して、または使用せずに彼のサンプルコードをコンパイルすると、どちらの場合も同じバイナリが取得されます。 gcc version 4.2.4 (Ubuntu 4.2.4-1ubuntu4)
を使用しています
次のコードで理解できないのは、restrict
をより広範囲に使用するためにコードを書き直す必要があるかどうか、またはGCCのエイリアス分析が非常に優れており、それを理解できるかどうかです。引数は互いにエイリアスを設定しません。 純粋に教育目的で、このコードでrestrict
を使用する方法と使用しない方法を教えてください。なぜですか?
でコンパイルされたrestrict
の場合:
_gcc -DCLS=$(getconf LEVEL1_DCACHE_LINESIZE) -DUSE_RESTRICT -Wextra -std=c99 -O3 matrixMul.c -o matrixMul
_
restrict
を使用しない場合は、_-DUSE_RESTRICT
_を削除するだけです。
_#include <stdlib.h>
#include <stdio.h>
#include <emmintrin.h>
#ifdef USE_RESTRICT
#else
#define restrict
#endif
#define N 1000
double _res[N][N] __attribute__ ((aligned (64)));
double _mul1[N][N] __attribute__ ((aligned (64)))
= { [0 ... (N-1)]
= { [0 ... (N-1)] = 1.1f }};
double _mul2[N][N] __attribute__ ((aligned (64)))
= { [0 ... (N-1)]
= { [0 ... (N-1)] = 2.2f }};
#define SM (CLS / sizeof (double))
void mm(double (* restrict res)[N], double (* restrict mul1)[N],
double (* restrict mul2)[N]) __attribute__ ((noinline));
void mm(double (* restrict res)[N], double (* restrict mul1)[N],
double (* restrict mul2)[N])
{
int i, i2, j, j2, k, k2;
double *restrict rres;
double *restrict rmul1;
double *restrict rmul2;
for (i = 0; i < N; i += SM)
for (j = 0; j < N; j += SM)
for (k = 0; k < N; k += SM)
for (i2 = 0, rres = &res[i][j],
rmul1 = &mul1[i][k]; i2 < SM;
++i2, rres += N, rmul1 += N)
for (k2 = 0, rmul2 = &mul2[k][j];
k2 < SM; ++k2, rmul2 += N)
for (j2 = 0; j2 < SM; ++j2)
rres[j2] += rmul1[k2] * rmul2[j2];
}
int main (void)
{
mm(_res, _mul1, _mul2);
return 0;
}
_
また、GCC 4.0.0-4.4には、restrictキーワードが無視される原因となる回帰バグがあります。このバグは4.5で修正されたと報告されています(ただし、バグ番号は紛失しました)。
これは、コードオプティマイザへのヒントです。制限を使用すると、ポインタ変数をCPUレジスタに格納でき、ポインタ値の更新をメモリにフラッシュする必要がないため、エイリアスも更新されます。
それを利用するかどうかは、オプティマイザとCPUの実装の詳細に大きく依存します。コードオプティマイザは、非常に重要な最適化であるため、非エイリアシングの検出にすでに多大な投資をしています。コードでそれを検出しても問題はないはずです。
(実際には、このキーワードを使用することで大きな利点が得られるかどうかはわかりません。強制がないため、プログラマーがこの修飾子を使用すると非常に簡単にエラーが発生します。 )
ポインタAがメモリの一部の領域への唯一のポインタであることがわかっている場合、つまり、エイリアスがない(つまり、他のポインタBは必ずA、B!= Aと等しくない)ことがわかります。 Aのタイプを「制限」キーワードで修飾することにより、この事実をオプティマイザに伝えます。
私はこれについてここに書きました: http://mathdev.org/node/2 そして、いくつかの制限されたポインタが実際に「線形」であることを示しようとしました(その投稿で述べられているように)。
clang
の最近のバージョンでは、エイリアスのランタイムチェックと2つのコードパスを使用してコードを生成できることに注意してください。1つはエイリアスの可能性がある場合、もう1つは明らかな場合です。それのチャンスはありません。
これは、上記の例のように、コンパイラーに目立つように指定されたデータの範囲に明らかに依存します。
主な正当化は、STLを多用するプログラム、特に<algorithm>
修飾子を導入することが困難または不可能である__restrict
のためのものであると思います。
もちろん、これはすべてコードサイズを犠牲にして行われますが、__restrict
として宣言されたポインターが開発者の考えほど完全に重複しない可能性がある、あいまいなバグの可能性を大幅に排除します。
GCCにもこの最適化がなかったとしたら、私は驚きます。
ここで行われる最適化は、エイリアスされていないポインタに依存しないのでしょうか?結果をres2に書き込む前に複数のmul2要素をプリロードしない限り、エイリアスの問題は発生しません。
表示する最初のコードでは、どのようなエイリアスの問題が発生する可能性があるかは非常に明確です。ここではそれほど明確ではありません。
Dreppersの記事をもう一度読んで、制限によって何かが解決されるかもしれないと彼は特に述べていません。次のフレーズもあります:
{理論的には、1999年の改訂でC言語に導入されたrestrictキーワードは問題を解決するはずです。しかし、コンパイラはまだ追いついていません。その理由は主に、コンパイラーを誤解させたり、不適切なオブジェクトコードを生成させたりする不正なコードが多すぎるためです。
このコードでは、メモリアクセスの最適化はアルゴリズム内ですでに行われています。残差の最適化は、付録にあるベクトル化されたコードで行われているようです。したがって、ここに示すコードでは、制限に依存する最適化は行われないため、違いはないと思います。すべてのポインターアクセスはエイリアスのソースですが、すべての最適化がエイリアスに依存しているわけではありません。
早期の最適化はすべての悪の根源であり、restrictキーワードの使用は、積極的に調査および最適化している場合に限定し、使用できる場所では使用しないようにします。
まったく違いがある場合は、mm
を別のDSOに移動すると(gccが呼び出しコードに関するすべてを認識できなくなるなど)、それを実証する方法になります。
サンプルコードの問題は、コンパイラが呼び出しをインライン化し、サンプルでエイリアスが発生しないことを確認することです。 main()関数を削除し、-cを使用してコンパイルすることをお勧めします。
32ビットまたは64ビットのUbuntuで実行していますか? 32ビットの場合、-march=core2 -mfpmath=sse
(またはプロセッサアーキテクチャが何であれ)を追加する必要があります。それ以外の場合は、SSEを使用しません。次に、GCC 4.2でベクトル化を有効にするには、-ftree-vectorize
オプションを追加する必要があります(4.3または4.4以降、これは-O3
にデフォルトで含まれています)。コンパイラーが浮動小数点演算を再配列できるようにするために、-ffast-math
(または緩和された浮動小数点セマンティクスを提供する別のオプション)を追加する必要がある場合もあります。
また、-ftree-vectorizer-verbose=1
オプションを追加して、ループがベクトル化されたかどうかを確認します。これは、restrictキーワードを追加した効果を確認する簡単な方法です。