【问题标题】:Does the restrict keyword provide significant benefits in gcc/g++?限制关键字是否在 gcc/g++ 中提供了显着的好处?
【发布时间】:2010-12-30 05:53:12
【问题描述】:

有没有人见过任何关于在 gcc/g++ 中使用 C/C++ restrict 关键字是否在现实中(而不仅仅是理论上)显着提升性能的数字/分析?

我已经阅读了各种推荐/贬低其使用的文章,但我没有遇到任何实际证明任何一方论点的实数。

编辑

我知道 restrict 不是 C++ 的正式组成部分,但它受到一些编译器的支持,我阅读了 Christer Ericson 的一篇论文,强烈推荐使用它。

【问题讨论】:

  • 混叠问题通常被认为是 C/C++ 在许多计算任务中的效率低于 Fortran 的第一大原因。所以我想说任何有助于避免混叠的功能都会产生的不同。

标签: c++ c gcc g++ restrict-qualifier


【解决方案1】:

restrict 关键字有所不同。

在某些情况下(图像处理),我看到了 2 倍甚至更多的改进。大多数时候,差异并没有那么大。大约 10%。

这里有一个小例子来说明差异。我写了一个非常基本的 4x4 向量 * 矩阵变换作为测试。请注意,我必须强制该函数不被内联。否则 GCC 会检测到我的基准代码中没有任何别名指针,并且由于内联,restrict 不会产生影响。

我也可以将转换函数移动到不同的文件中。

#include <math.h>

#ifdef USE_RESTRICT
#else
#define __restrict
#endif


void transform (float * __restrict dest, float * __restrict src, 
                float * __restrict matrix, int n) __attribute__ ((noinline));

void transform (float * __restrict dest, float * __restrict src, 
                float * __restrict matrix, int n)
{
  int i;

  // simple transform loop.

  // written with aliasing in mind. dest, src and matrix 
  // are potentially aliasing, so the compiler is forced to reload
  // the values of matrix and src for each iteration.

  for (i=0; i<n; i++)
  {
    dest[0] = src[0] * matrix[0] + src[1] * matrix[1] + 
              src[2] * matrix[2] + src[3] * matrix[3];

    dest[1] = src[0] * matrix[4] + src[1] * matrix[5] + 
              src[2] * matrix[6] + src[3] * matrix[7];

    dest[2] = src[0] * matrix[8] + src[1] * matrix[9] + 
              src[2] * matrix[10] + src[3] * matrix[11];

    dest[3] = src[0] * matrix[12] + src[1] * matrix[13] + 
              src[2] * matrix[14] + src[3] * matrix[15];

    src  += 4;
    dest += 4;
  }
}

float srcdata[4*10000];
float dstdata[4*10000];

int main (int argc, char**args)
{
  int i,j;
  float matrix[16];

  // init all source-data, so we don't get NANs  
  for (i=0; i<16; i++)   matrix[i] = 1;
  for (i=0; i<4*10000; i++) srcdata[i] = i;

  // do a bunch of tests for benchmarking. 
  for (j=0; j<10000; j++)
    transform (dstdata, srcdata, matrix, 10000);
}

结果:(在我的 2 Ghz Core Duo 上)

nils@doofnase:~$ gcc -O3 test.c
nils@doofnase:~$ time ./a.out

real    0m2.517s
user    0m2.516s
sys     0m0.004s

nils@doofnase:~$ gcc -O3 -DUSE_RESTRICT test.c
nils@doofnase:~$ time ./a.out

real    0m2.034s
user    0m2.028s
sys     0m0.000s

那个系统上,执行速度提高了 20%。

为了说明它在多大程度上取决于架构,我让相同的代码在 Cortex-A8 嵌入式 CPU 上运行(稍微调整了循环计数,因为我不想等那么久):

root@beagleboard:~# gcc -O3 -mcpu=cortex-a8 -mfpu=neon -mfloat-abi=softfp test.c
root@beagleboard:~# time ./a.out

real    0m 7.64s
user    0m 7.62s
sys     0m 0.00s

root@beagleboard:~# gcc -O3 -mcpu=cortex-a8 -mfpu=neon -mfloat-abi=softfp -DUSE_RESTRICT test.c 
root@beagleboard:~# time ./a.out

real    0m 7.00s
user    0m 6.98s
sys     0m 0.00s

这里的差异只有 9%(顺便说一句,相同的编译器。)

【讨论】:

  • 干得好。这里有一篇关于在 Cell 处理器上使用限制的文章:cellperformance.beyond3d.com/articles/2006/05/…,可能与讨论架构特定的好处有关。
  • @Nils Pipenbrinck:为什么必须禁用函数的内联?编译器自动内联似乎是一个非常大的功能。
  • @Nils Pipenbrinck:顺便说一下,Ulrich Drepper 发布了超优化矩阵乘法的代码,作为他关于优化缓存和内存使用的讨论的一部分。它在这里:lwn.net/Articles/258188。他对他为达到该解决方案所经历的每一步的讨论在这里:lwn.net/Articles/255364。与标准 MM 相比,他能够将执行时间减少 90%。
  • @Nils Pipenbrinck:我运行了你的测试。当我用 -O3 编译时,我得到了和你一样的结果,大约提高了 20% 的速度。但是,当我在没有任何优化标志的情况下编译时,两者运行的地方相同。这很有趣:No Op flag = 0m5.022s for both, -O3 0m3.186s & 0m2.583s, -Os 0m2.391s & 0m2.314s。优化尺寸可以得到最好的结果。在这种情况下添加限制只购买了额外的 3.2% 的性能。想知道为什么会这样吗?
【解决方案2】:

restrict 关键字在 gcc / g++ 中是否提供了显着的好处?

可以减少如下示例所示的指令数量,因此请尽可能使用它。

GCC 4.8 Linux x86-64 示例

输入:

void f(int *a, int *b, int *x) {
  *a += *x;
  *b += *x;
}

void fr(int *restrict a, int *restrict b, int *restrict x) {
  *a += *x;
  *b += *x;
}

编译和反编译:

gcc -g -std=c99 -O0 -c main.c
objdump -S main.o

-O0 是一样的。

-O3:

void f(int *a, int *b, int *x) {
    *a += *x;
   0:   8b 02                   mov    (%rdx),%eax
   2:   01 07                   add    %eax,(%rdi)
    *b += *x;
   4:   8b 02                   mov    (%rdx),%eax
   6:   01 06                   add    %eax,(%rsi)  

void fr(int *restrict a, int *restrict b, int *restrict x) {
    *a += *x;
  10:   8b 02                   mov    (%rdx),%eax
  12:   01 07                   add    %eax,(%rdi)
    *b += *x;
  14:   01 06                   add    %eax,(%rsi) 

对于外行来说,calling convention 是:

  • rdi = 第一个参数
  • rsi = 第二个参数
  • rdx = 第三个参数

结论:3条指令而不是4条

当然,指令can have different latencies,但这提供了一个好主意。

为什么 GCC 能够对其进行优化?

上面的代码取自Wikipedia example,它非常很有启发性。

f 的伪汇编:

load R1 ← *x    ; Load the value of x pointer
load R2 ← *a    ; Load the value of a pointer
add R2 += R1    ; Perform Addition
set R2 → *a     ; Update the value of a pointer
; Similarly for b, note that x is loaded twice,
; because x may point to a (a aliased by x) thus 
; the value of x will change when the value of a
; changes.
load R1 ← *x
load R2 ← *b
add R2 += R1
set R2 → *b

对于fr

load R1 ← *x
load R2 ← *a
add R2 += R1
set R2 → *a
; Note that x is not reloaded,
; because the compiler knows it is unchanged
; "load R1 ← *x" is no longer needed.
load R2 ← *b
add R2 += R1
set R2 → *b

真的更快吗?

Ermmm...不适合这个简单的测试:

.text
    .global _start
    _start:
        mov $0x10000000, %rbx
        mov $x, %rdx
        mov $x, %rdi
        mov $x, %rsi
    loop:
        # START of interesting block
        mov (%rdx),%eax
        add %eax,(%rdi)
        mov (%rdx),%eax # Comment out this line.
        add %eax,(%rsi)
        # END ------------------------
        dec %rbx
        cmp $0, %rbx
        jnz loop
        mov $60, %rax
        mov $0, %rdi
        syscall
.data
    x:
        .int 0

然后:

as -o a.o a.S && ld a.o && time ./a.out

在 Ubuntu 14.04 AMD64 CPU Intel i5-3210M 上。

我承认我仍然不了解现代 CPU。让我知道你是否:

  • 发现我的方法有缺陷
  • 找到了一个汇编程序测试用例,它变得更快
  • 了解为什么没有区别

【讨论】:

    【解决方案3】:

    文章Demystifying The Restrict Keyword 指的是论文Why Programmer-specified Aliasing is a Bad Idea (pdf),它说它通常没有帮助,并提供了测量来支持这一点。

    【讨论】:

    • 有许多种代码几乎没有什么好处,但有些代码却提供了巨大的好处。您是否知道有任何论文表明明智地使用“限制”不会比那些编译器通过基于类型的别名实现的好处更大?
    【解决方案4】:

    请注意,允许 restrict 关键字的 C++ 编译器可能仍会忽略它。例如here就是这种情况。

    【讨论】:

    • 实际上,如果您仔细阅读该页面,您会注意到虽然由于与同名用户变量的潜在冲突,C++ 中忽略了限制,但 C++ 支持 __restrict__。跨度>
    • @Robert:被忽略了。不同之处仅在于带有双下划线的标识符保留给系统使用。因此 __restrict__ 不应与任何用户声明的标识符发生冲突。
    • @Martin:你怎么知道它被忽略了?从文档中并不完全清楚 - 似乎您可以通过任何方式阅读它。
    • 我同意不清楚,但忽略restrict 而不是__restrict__ 似乎不一致。无论哪种方式,它仍然是不可移植的,并且在非常特定的情况下是有益的。我建议,如果您知道它在特定情况下是有益的,并且您需要这种好处才能取得成功,那么就使用它;否则为什么要无缘无故地使代码不可移植?我不会习惯性地使用它,但作为最后的手段并在测试实际好处之后。
    • @Clifford:当然,但是几乎所有优化都是这样的——以这种方式和那种方式测试,然后使用有效的方法。
    【解决方案5】:

    我测试了this C 程序。没有restrict 需要12.640 秒才能完成,restrict 12.516。看起来它可以节省一些时间。

    【讨论】:

    • 这种差异几乎可以肯定是微不足道的,但是,您还应该将 c 声明为受限制,因为每次写入 c 时,编译器可能会考虑 *a *b 和 *inc 可能已经改变了。
    • 在您的示例中,优化器可以检测到参数没有别名。尝试禁用内联,您会看到更大的差异。
    • 但如果禁用内联,则会人为地削弱编译器,因此您无法准确了解restricthelps 在实际代码中的作用。
    • @raphaelr:您似乎需要使用优化标志来限制限制。尝试 -O3 或 -Os。
    • @DrewHall — 1/8 秒的差异确实可能是噪音,但不一定是噪音。确定该声明的唯一方法是确定测量的执行情况不佳,例如在加载的系统上进行单次运行。但据我们所知,raphaelr 平均了十多次运行的运行时间,1% 的差异是有效的。不幸的是,他提供的链接现在已经失效,因此无法对他的代码进行独立测试。
    猜你喜欢
    • 2012-05-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-04-24
    相关资源
    最近更新 更多