【问题标题】:Rules for using the restrict keyword in C?在 C 中使用限制关键字的规则?
【发布时间】:2011-01-01 14:32:00
【问题描述】:

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

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

然后你就会明白为什么编译器必须这样做。使用restrict 有效地告诉编译器你永远不会这样做,这样它就可以在存储b 之前删除c 的冗余负载并加载a

In a different SO post, Nils Pipenbrinck, provides a working example of this scenario demonstrating the performance benefit.

到目前为止,我已经收集到在传递给不会被内联的函数的指针上使用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;
}

【问题讨论】:

  • 快速回答是:不要。使用另一种类型限定符会降低代码的可读性,并增加难以调试的错误的机会。在大多数情况下,您应该相信您的编译器能够解决这些问题。
  • 但是如果你正在编写一个库,编译器无法解决这个问题,因为它无法知道所有的调用者。此外,将restrict 用作函数参数作为 API 用户的文档。
  • @gs:考虑到许多以编写高度优化的代码为生的受人尊敬的人都建议使用restrict 是“最佳实践”,我认为尝试理解所涉及的问题是值得的。否则我不会问这个问题。
  • 非常好的问题。建议将for (int i = n; i&lt;n; ++i) 改为for (int i = 0; i&lt;n; ++i),否则for-loop 的内容永远不会执行。
  • @chux 三年内第一个注意到这个错字的人...

标签: c optimization memory


【解决方案1】:

您的示例代码的问题是编译器只会内联调用,并看到您的示例中不可能有别名。我建议你删除 main() 函数并使用 -c 编译它。

【讨论】:

    【解决方案2】:

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

    这显然取决于指向编译器显眼的数据范围 - 就像在上面的示例中一样。

    我认为主要的理由是大量使用 STL 的程序 - 特别是 &lt;algorithm&gt; ,其中很难或不可能引入 __restrict 限定符。

    当然,这一切都以牺牲代码大小为代价,但消除了许多潜在的隐蔽错误,这些错误可能导致声明为 __restrict 的指针不像开发人员想象的那样不重叠。

    如果 GCC 没有得到这个优化,我会感到惊讶。

    【讨论】:

      【解决方案3】:

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

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

      我在这里写过这个:http://mathdev.org/node/23 并试图证明一些受限指针实际上是“线性的”(如那篇文章中所述)。

      【讨论】:

      • 不,优化器完全有权假设程序员没有“撒谎”——如果他们这样做了,结果发生的任何事情都是程序员的错,而不是编译器的错。
      【解决方案4】:

      如果有任何区别,将mm 移至单独的 DSO(这样 gcc 不再知道调用代码的所有内容)将是演示它的方法。

      【讨论】:

        【解决方案5】:

        此外,GCC 4.0.0-4.4 有一个回归错误,导致限制关键字被忽略。此错误已在 4.5 中报告为已修复(但我丢失了错误编号)。

        【讨论】:

        • gcc 4.2.4 中是否也存在此错误?根据链接的错误报告,我很难判断。
        【解决方案6】:

        您是在 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 选项以查看它是否能够对循环进行矢量化;这是检查添加限制关键字效果的简单方法。

        【讨论】:

        • 我在带有 -march=native 的 Pentium M 上使用 32 位 Ubuntu。我添加了快速数学。我发布的代码不使用 SSE。使用建议的选项似乎没有帮助。在这两种情况下,我仍然得到相同的二进制文件。
        • 啊,确实如此。我相信您也看到了在 -ftree-vectorizer-verbose= 选项的帮助下无法进行矢量化的原因。这就解释了为什么 Drepper 不得不为他的代码的矢量化版本求助于内在函数。所以你需要想出另一个例子。
        【解决方案7】:

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

        在您展示的第一段代码中,很清楚会发生什么样的别名问题。 这里不是很清楚。

        重读 Dreppers 的文章,他并没有特别说限制可以解决任何问题。甚至还有这句话:

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

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

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

        【讨论】:

        • @shodanex:这正是我想知道的。 Drepper 似乎在他的论文中指出,这段代码特别受益于restrict。您可以在此处使用 SSE2 指令查看相同代码的矢量化版本:lwn.net/Articles/258188。有没有可能他只是一直使用restrict 作为他个人的最佳实践,而只是懒得检查它是否与这段代码有任何区别?
        • 在您提到的代码中,仍然存在 for (j2 = 0; j2
        • @shodanex:那么为什么rres[j2] += rmul1[k2] * rmul2[j2]; 没有出现混叠问题呢?假设我打电话给mm(_res, _res, _res)rres[j2]rmul2[j2] 必须在每次循环时重新加载。所以即使&amp;rres[j2] == &amp;rmul2[j2] 也没关系。现在如果&amp;rres[j2] == &amp;rmul1[k2] 在循环执行的某个时刻会怎样?如果这是真的,那么每次循环都必须重新加载rmul1[k2],否则它可以进入寄存器。我猜编译器展开循环,看到这种情况永远不会发生,并且wallah;潜在的混叠无关紧要。
        • @shodanex:所以基本上,Drepper 始终坚持使用restrict 作为最佳实践(考虑到他把它放在代码的矢量化版本中),只是懒得检查是否它与这个特定的代码有任何区别。无论如何,这就是我的样子。
        【解决方案8】:

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

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

        【讨论】:

        • 所以基本上你是说 gcc 中的别名分析非常好,即使没有使用 restrict 关键字的提示,它已经能够检测到它自己没有别名问题?
        • 对。您需要将 double** 作为参数传递并更新它们以引入优化器无法排除的公然别名。
        • 但是在不知道潜在调用者的情况下,结果是一样的,所以我怀疑这就是这里发生的事情
        猜你喜欢
        • 2019-01-16
        • 1970-01-01
        • 2017-01-15
        • 2021-11-14
        • 2023-03-28
        • 1970-01-01
        • 2017-07-02
        • 1970-01-01
        • 2011-04-15
        相关资源
        最近更新 更多