在C中使用restrict关键字的规则?
我试图了解何时何时不使用C中的restrict
关键字,以及在什么情况下它提供了实际的好处。
阅读后,“ 解密限制关键字 ”(提供了一些使用经验的规则),我得到的印象是,当一个函数传递指针时,必须考虑到指向的数据可能重叠的可能性(别名)与任何其他parameter passing给函数。 给定一个函数:
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]; } }
编译器必须在第二个expression式中重新加载c
,因为可能b
和c
指向相同的位置。 它也必须等待b
被存储之前,它可以加载a
相同的原因。 然后它必须等待a
被存储,并且必须在下一个循环开始时重新加载b
和c
。 如果你这样调用函数:
int a[N]; foo(a, a, a, N);
那么你可以看到为什么编译器必须这样做。 有效地使用restrict
告诉编译器,你永远不会这样做,这样它可以放弃c
的冗余负载,并加载之前b
存储。
在另一个SOpost中,Nils Pipenbrinck提供了这个场景的一个工作示例,展示了性能优势。
到目前为止,我已经认识到,对传递给不会被内联的函数的指针使用restrict
是个好主意。 显然,如果代码被内联,编译器可以发现指针不重叠。
现在,这里的事情开始变得模糊。
在Ulrich Drepper的论文“ 每个程序员应该知道的内存 ”中,他声明:“除非使用限制,否则所有的指针访问都是潜在的混叠源。”他给出了一个特定的子代matrix的代码示例,使用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; }
此外,GCC 4.0.0-4.4有一个回归bug,导致restrict关键字被忽略。 这个bug在4.5版中报告为固定的(虽然我错过了bug数)。
这是代码优化器的一个提示。 使用restrict可以确保它可以将指针variables存储在CPU寄存器中,而不必将指针值的更新刷新到内存中,以便更新别名。
它是否利用它很大程度上取决于优化器和CPU的实现细节。 代码优化器已经在检测非锯齿方面进行了大量投入,因为它是如此重要的优化。 在代码中检测它应该没有问题。
(我不知道使用这个关键字实际上是否给你一个显着的优势,程序员很容易犯这个限定符,因为没有强制执行,所以优化器不能确定程序员是不是“撒谎”的。 )
当你知道指针A是唯一指向某个内存区域的指针时,也就是说,它没有别名(也就是说,任何其他指针B必然不等于A,B!= A),你可以告诉通过使用“restrict”关键字来限定A的types,这是优化器的事实。
我在这里写了这个: http : //mathdev.org/node/23并试图表明,一些限制指针实际上是“线性的”(如该文章中提到的)。
值得注意的是,最近版本的clang
能够生成带有别名的运行时检查的代码,以及两个代码path:一个用于存在潜在别名的情况,另一个用于明显没有机会的情况。
这显然取决于指向编译器显着的数据范围 – 就像上面的例子一样。
我相信最主要的理由是大量使用STL的程序 – 尤其是<algorithm>
,要么在其中引入__restrict
限定符是困难的或不可能的。
当然,这一切都是以代码大小为代价的,但是消除了大量潜在的错误,这些错误可能会导致声明为__restrict
指针不像开发人员认为的那样重叠。
如果GCC没有得到这个优化,我会感到惊讶。
可能是在这里做的优化不要依赖指针不被别名? 除非在写入结果res2之前预先加载多个mul2元素,否则我不会看到任何别名问题。
在你展示的第一段代码中,很清楚会出现什么样的别名问题。 这里不太清楚。
重读Dreppers文章,他没有具体说限制可能会解决任何问题。 甚至有这样一句话:
{从理论上讲,1999年修订版中引入C语言的限制关键字应该可以解决这个问题。 编译器还没有赶上,但。 原因主要是存在太多不正确的代码,这会误导编译器并导致其生成错误的目标代码。}
在这个代码中,内存访问的优化已经在algorithm中完成了。 剩余优化似乎在附录中呈现的vector化代码中完成。 所以对于这里介绍的代码,我想没有区别,因为没有依赖限制的优化。 每个指针访问都是别名的来源,但并不是每一个优化都依赖于别名。
不成熟的优化是万恶之源,限制关键字的使用应限制在您正在积极学习和优化的情况下,而不是在任何可以使用的地方使用。
如果完全不同,将mm
移动到一个单独的DSO(例如gcc不能再知道关于调用代码的所有内容)将是展示它的方式。
你在32位或64位Ubuntu上运行吗? 如果是32位,那么你需要添加-march=core2 -mfpmath=sse
(或者你的处理器架构是什么),否则它不会使用SSE。 其次,为了使用GCC 4.2来实现vector化,你需要添加-ftree-vectorize
选项(从4.3或4.4开始,这在-O3
被默认包含)。 可能还需要添加-ffast-math
(或提供轻松浮点语义的其他选项),以便编译器重新sorting浮点操作。
另外,添加-ftree-vectorizer-verbose=1
选项以查看是否pipe理向量化循环; 这是检查添加restrict关键字效果的简单方法。
你的示例代码的问题是,编译器只是内联调用,看看你的例子中没有别名可能。 我build议你删除main()函数并使用-c编译它。