我试图了解何时何时不在C中使用restrict
关键字,以及在什么情况下它提供了实实在在的好处。
阅读后,“Demystifying 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];
}
}
编译器必须在第二个表达式中重新加载c
,因为b
和c
可能指向同一位置。在出于同样的原因加载b
之前,它还必须等待a
存储。然后必须等待a
存储,并且必须在下一个循环开始时重新加载b
和c
。如果你这样调用函数:
int a[N];
foo(a, a, a, N);
然后你可以看到为什么编译器必须这样做。使用restrict
有效地告诉编译器你永远不会这样做,这样它就可以在存储c
之前删除a
的冗余负载并加载b
。
到目前为止,我已经收集了一个好主意,在你传递给不会内联的函数的指针上使用restrict
。显然,如果代码是内联的,编译器可以发现指针不重叠。
现在这里的事情开始让我变得模糊。
在Ulrich Drepper的论文中,“What every programmer should know about memory”他声明说,“除非使用限制,所有指针访问都是潜在的别名来源”,并且他给出了一个子矩阵矩阵的特定代码示例,其中他使用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;
}
答案 0 :(得分:14)
此外,GCC 4.0.0-4.4有一个回归错误,导致忽略restrict关键字。这个错误在4.5中被修复(虽然我丢失了错误号码)。
答案 1 :(得分:13)
这是代码优化器的提示。使用restrict可确保它可以将指针变量存储在CPU寄存器中,而不必将指针值的更新刷新到内存中,以便更新别名。
它是否利用它在很大程度上取决于优化器和CPU的实现细节。代码优化器已经在检测非混叠方面投入了大量资金,因为它是如此重要的优化。在代码中检测它应该没有问题。
答案 2 :(得分:3)
(我不知道是否使用这个关键字给你一个显着的优势,实际上。程序员很容易犯这个限定符,因为没有执行,所以优化器不能确定程序员不会“谎言”。)
当你知道指针A是指向某个内存区域的唯一指针时,也就是说,它没有别名(也就是说,任何其他指针B必然不等于A,B!= A),您可以通过使用“restrict”关键字限定A的类型来向优化程序说明这一事实。
我在这里写过:http://mathdev.org/node/23并试图证明一些限制性指针实际上是“线性的”(如该帖子所述)。
答案 3 :(得分:3)
值得注意的是,clang
的最新版本能够生成具有别名运行时检查的代码,以及两个代码路径:一个用于存在潜在别名的情况,另一个用于具有潜在别名的情况。显然没有机会。
这显然取决于指向编译器显眼的数据范围 - 正如上面的例子中所示。
我认为主要的理由是大量使用STL的程序 - 特别是<algorithm>
,要么很难或不可能引入__restrict
限定词。
当然,这一切都是以代码大小为代价的,但是消除了大量潜在的隐藏错误,这些错误可能导致声明为__restrict
的指针不像开发人员所认为的那样不重叠。
如果GCC没有得到这种优化,我会感到惊讶。
答案 4 :(得分:1)
可能是这里做的优化不依赖指针没有别名?除非在将结果写入res2之前预先加载多个mul2元素,否则我看不到任何别名问题。
在您展示的第一段代码中,很清楚会出现什么样的别名问题。 这里不太清楚。
重读Dreppers文章,他没有具体说限制可以解决任何问题。甚至还有这句话:
{理论上是restrict关键字 在C语言中引入了 1999修订应解决 问题。编制者还没有赶上 但是,尽管如此。原因主要是 存在太多不正确的代码 会误导编译器并造成错误 它生成不正确的目标代码。}
在此代码中,已在算法中完成内存访问的优化。剩余优化似乎是在附录中提供的矢量化代码中完成的。因此,对于此处提供的代码,我猜没有区别,因为没有完成依赖于限制的优化。每个指针访问都是别名的来源,但不是每个优化都依赖于别名。
过早优化是所有邪恶的根源,限制关键字的使用应限于您正在积极研究和优化的情况,而不是在任何可以使用的地方使用。
答案 5 :(得分:1)
如果存在差异,将mm
移动到单独的DSO(以便gcc不再知道有关调用代码的所有内容)将是演示它的方法。
答案 6 :(得分:0)
您是在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关键字的效果的简单方法。
答案 7 :(得分:0)
示例代码的问题在于编译器只是内联调用,并且在示例中看到没有可能的别名。我建议你删除main()函数并使用-c。
进行编译