私は、C でキーワードをいつ使用し、いつ使用しないかrestrict
、またどのような状況で具体的なメリットが得られるかを理解しようとしています。
読んだあと、 "制限キーワードの謎を解く" (これは使用に関する経験則をいくつか示しています) によると、関数にポインターが渡されるときは、関数に渡される他の引数とポインターが指すデータが重複する (エイリアス) 可能性を考慮する必要があるという印象を受けます。次のような関数があるとします。
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];
}
}
コンパイラはc
2 番目の式で再ロードする必要があります。これは、おそらくb
と がc
同じ場所を指しているためです。また、同じ理由でb
、 が格納されるまで待ってからロードする必要があります。その後、 が格納されるa
のを待ってから、次のループの先頭でとを再ロードする必要があります。関数を次のように呼び出すとします。a
b
c
int a[N];
foo(a, a, a, N);
そうすれば、コンパイラがなぜこれを行う必要があるのかがわかります。 をrestrict
効果的に使用することで、コンパイラにこれを行わないことを伝え、 の冗長なロードと のロード前のc
ロードを削除できます。a
b
別の SO 投稿では、Nils Pipenbrinck がこのシナリオの実例を示し、パフォーマンス上の利点を実証しています。
これまでのところ、インライン化されない関数に渡すポインターに を使用するのは良い考えだとわかりましたrestrict
。どうやら、コードがインライン化されている場合、コンパイラーはポインターが重複していないと判断できるようです。
さて、ここから私にとって物事が曖昧になり始めます。
ウルリッヒ・ドレッパーの論文では、「すべてのプログラマがメモリについて知っておくべきこと「彼は、「restrict が使用されない限り、すべてのポインタ アクセスはエイリアシングの潜在的な原因となる」と述べ、 を使用する部分行列行列乗算の具体的なコード例を示しています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
-DUSE_RESTRICT
使用しない場合は削除するだけです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;
}
ベストアンサー1
これはコード オプティマイザへのヒントです。restrict を使用すると、ポインタ変数を CPU レジスタに格納でき、ポインタ値の更新をメモリにフラッシュする必要がなくなり、エイリアスも更新されます。
この機能を活用するかどうかは、オプティマイザと CPU の実装の詳細に大きく依存します。非エイリアシングは非常に重要な最適化であるため、コード オプティマイザはすでに非エイリアシングの検出に多大な投資を行っています。コード内で非エイリアシングを検出することは問題ないはずです。