在 C 中使用 limit 关键字的规则?

我试图了解什么时候和什么时候不要在 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,因为 bc可能指向同一个位置。出于同样的原因,它还必须等待 b被存储之后才能加载 a。然后它必须等待 a被存储,并且必须在下一个循环的开始重新加载 bc。如果像这样调用函数:

int a[N];
foo(a, a, a, N);

然后你就知道为什么编译器要这么做了。使用 restrict有效地告诉编译器,您永远不会这样做,这样它就可以在存储 b之前减少 c的冗余负载并加载 a

在另一篇 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

只需删除 -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;
}
23251 次浏览

也许这里做的优化不依赖于指针没有别名?除非在将 result 写入 res2之前预加载多个 mul2元素,否则我看不出任何别名问题。

在您展示的第一段代码中,可以很清楚地看到会发生什么样的别名问题。 这里就不那么清楚了。

重读 Dreppers 的文章,他没有明确说限制可能解决任何问题。甚至有这样一句话:

{理论上限制关键字 引入到 C 语言中 1999年的修订应该解决 问题。编译器没有跟上 主要原因是 存在太多不正确的代码 会误导编译器,导致 生成错误的目标代码。}

在这段代码中,内存访问的优化已经在算法中完成。残差优化似乎是在附录中提出的矢量化代码中完成的。因此,对于这里提供的代码,我想没有什么区别,因为没有依赖于限制的优化。每个指针访问都是别名的来源,但并非每个优化都依赖于别名。

过早的优化是万恶之源,使用限制关键字应该限制在你正在积极学习和优化的情况下,而不是使用在任何可以使用的地方。

这是对代码优化器的提示。使用 limit 确保它可以将指针变量存储在 CPU 寄存器中,而不必将指针值的更新刷新到内存中,以便也更新别名。

它是否利用了这一优势在很大程度上取决于优化器和 CPU 的实现细节。由于非混叠是一个非常重要的优化问题,代码优化器已经在检测非混叠方面投入了大量的精力。在您的代码中检测到这一点应该没有问题。

你运行的是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选项以查看它是否设法向量化循环; 这是检查添加 limit 关键字的效果的简单方法。

此外,GCC 4.0.0-4.4还有一个回归 bug,它会导致忽略 limit 关键字。这个 bug 在4.5中被报告为已修复(但是我丢失了 bug 编号)。

如果存在任何差异,那么将 mm移动到一个单独的 DSO (这样 gcc 就不能再知道关于调用代码的所有信息)将是演示它的方法。

(实际上,我不知道使用这个关键词是否会给你带来显著的优势。由于没有强制执行,程序员很容易在使用这个限定符时出错,因此优化器不能确定程序员没有“撒谎”。)

当您知道指针 A 是指向内存某个区域的唯一指针时,也就是说,它没有别名(也就是说,任何其他指针 B 都必然不等于 A,B!= A) ,您可以通过使用“ limit”关键字限定 A 的类型来告诉优化器这个事实。

我在这里写过: http://mathdev.org/node/23,并试图展示一些受限制的指针实际上是“线性的”(正如那篇文章中提到的)。

值得注意的是,最新版本的 clang能够生成带有别名运行时检查的代码,以及两个代码路径: 一个用于存在潜在别名的情况,另一个用于明显没有别名的情况。

这显然取决于编译器所关注的数据范围——正如上面的例子所示。

我相信主要的理由是节目大量使用 STL-特别是 <algorithm>,其中要么困难或不可能引入 __restrict限定词。

当然,这一切都是以代码大小为代价的,但是消除了大量潜在的模糊错误,这些错误可能导致声明为 __restrict的指针不像开发人员认为的那样完全不重叠。

如果海湾合作委员会没有得到这种优化,我会感到惊讶。

示例代码的问题在于,编译器将只是内联调用,并查看示例中是否存在任何别名。我建议您删除 main ()函数并使用-c 编译它。

下面的 C99代码可以显示程序 看情况的输出受到限制:

__attribute__((noinline))
int process(const int * restrict const a, int * const b) {
*b /= (*a + 1) ;
return *a + *b ;
}


int main(void) {
int data[2] = {1, 2};
return process(&data[0], &data[0]);
}

该软件终止代码1使用 限制和0没有 限制限定符。
编译是用 gcc -std=c99 -Wall -pedantic -O3 main.c完成的。
标志 -O1也做这项工作。

例如,当您可以告诉编译器循环条件保持不变,即使另一个指针已经更新(必然的,循环条件不能由于 限制而更改)时,使用 limit 是有用的。

等等。