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

Rob*_*nes 68 c memory optimization

我试图了解何时何时不在restrictC中使用关键字以及在什么情况下它提供了实实在在的好处.

在阅读" 揭秘限制关键字 "(提供一些关于使用的经验法则)之后,我得到的印象是,当函数通过指针时,它必须考虑指向的数据可能重叠的可能性(别名)将任何其他参数传递给函数.给定一个功能:

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];
    } 
}
Run Code Online (Sandbox Code Playgroud)

编译器必须c在第二个表达式中重新加载,因为可能bc指向相同的位置.ba出于同样的原因加载之前,它还必须等待存储.然后它必须等待a存储并且必须重新加载b并且c在下一个循环的开始.如果你这样调用函数:

int a[N];
foo(a, a, a, N);
Run Code Online (Sandbox Code Playgroud)

然后你就可以看到为什么编译器必须这样做了.使用restrict有效地告诉编译器你永远不会这样做,这样它就可以在存储之前丢弃冗余负载c和负载.ab

在另一篇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
Run Code Online (Sandbox Code Playgroud)

只需删除-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;
}
Run Code Online (Sandbox Code Playgroud)

小智 14

此外,GCC 4.0.0-4.4有一个回归错误,导致restrict关键字被忽略.这个错误在4.5中被修复(虽然我丢失了错误号).


Han*_*ant 13

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

它是否利用它在很大程度上取决于优化器和CPU的实现细节.代码优化器已经在检测非混叠方面投入了大量资金,因为它是如此重要的优化.在代码中检测它应该没有问题.

  • 但是如果不知道潜在的调用者,结果是一样的,所以我怀疑这是在这里发生的事情 (7认同)

sho*_*nex 1

可能这里完成的优化不依赖于没有别名的指针?除非您在将结果写入 res2 之前预加载多个 mul2 元素,否则我没有看到任何别名问题。

在您显示的第一段代码中,很清楚可能会发生哪种别名问题。这里就不太清楚了。

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

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

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

过早的优化是万恶之源,restrict关键字的使用应该限制在你正在积极研究和优化的情况下,而不是在能使用的地方使用。