实际使用C99'restrict'关键字吗?


182

我浏览了一些文档和问题/答案,并看到其中提到的内容。我读了一个简短的说明,指出程序员基本上会保证不会使用指针指向其他地方。

谁能提供一些实际的案例,值得使用它吗?


4
memcpyvs memmove是一个典型的例子。
Alexandre C.

@AlexandreC .:我认为这不是一个特别适用的方法,因为缺少“限制”限定符并不意味着程序逻辑可以在源和目标超载的情况下工作,而且这种限定符的存在也不会阻止调用方法确定源和目标是否重叠,如果是,则将dest替换为src +(dest-src),由于它是从src派生的,因此可以对其进行别名。
超级猫

@supercat:这就是为什么我将其作为评论。但是,1)- restrict限定参数的使用memcpy原则上可以使幼稚的实现得到积极的优化,2)仅调用memcpy可使编译器假定为其提供的参数不混叠,从而可以在memcpy调用周围进行一些优化。
Alexandre C.

@AlexandreC .:对于大多数平台上的编译器而言,要优化幼稚的memcpy(即使带有“ restrict”限制)要使其效率与针对目标定制的版本几乎一样,是非常困难的。呼叫方优化不需要“ restrict”关键字,在某些情况下,促进这些优化可能会适得其反。例如,许多memcpy的实现都可以以零额外成本memcpy(anything, anything, 0);视为无操作,并确保if p是至少n可写字节的指针memcpy(p,p,n);不会有不利的副作用。可能会出现这种情况...
supercat

...自然地在某些类型的应用程序代码中(例如,将其自身交换项目的排序例程),以及在它们没有不利副作用的实现中,让这些情况由一般情况下的代码处理可能比具有添加特殊情况的测试。不幸的是,一些编译器作者似乎认为最好要求程序员添加编译器可能无法进行优化的代码,以促进“优化机会”,而无论如何编译器都很少利用。
超级猫

Answers:


180

restrict说指针是唯一访问基础对象的东西。它消除了指针混叠的可能性,从而使编译器可以进行更好的优化。

例如,假设我有一台带有专门指令的机器,可以将内存中的数字向量相乘,并且我有以下代码:

void MultiplyArrays(int* dest, int* src1, int* src2, int n)
{
    for(int i = 0; i < n; i++)
    {
        dest[i] = src1[i]*src2[i];
    }
}

编译器需要正确处理if destsrc1和和是否src2重叠,这意味着它必须从头到尾一次执行一次乘法。通过使用restrict,编译器可以使用矢量指令自由地优化此代码。

Wikipedia上有一个条目restrict这里还有另一个示例。


3
@Michael-如果我没有记错的话,那么问题只会出在dest两个源向量重叠时。如果src1src2重叠,为什么会出现问题?
2015年

1
限制通常仅在指向已修改的对象时才有效,在这种情况下,它断言无需考虑任何隐藏的副作用。大多数编译器使用它来促进向量化。为此,Msvc将运行时检查用于数据重叠。
蒂姆

将register关键字添加到for循环变量中还可以增加限制,从而使其更快。

2
实际上,register关键字仅是建议性的。从2000年左右开始,在编译器中,无论您是否使用register关键字,示例中的i(以及用于比较的n)都将被优化为一个寄存器。
马克·费斯勒

152

维基百科例子非常照明。

它清楚地显示了如何保存一条汇编指令

没有限制:

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

伪汇编:

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 a may be equal to x.
load R1  *x
load R2  *b
add R2 += R1
set R2  *b

有限制:

void fr(int *restrict a, int *restrict b, int *restrict x);

伪汇编:

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
load R2  *b
add R2 += R1
set R2  *b

海湾合作委员会真的做到了吗?

GCC 4.8 Linux x86-64:

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) 

对于未启动的用户,调用约定为:

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

GCC的输出甚至比Wiki文章更清晰:4条指令对3条指令。

数组

到目前为止,我们节省了一条指令,但是如果指针代表要循环的数组(一种常见的用例),那么可以保存一堆指令,如supercat所述

考虑例如:

void f(char *restrict p1, char *restrict p2) {
    for (int i = 0; i < 50; i++) {
        p1[i] = 4;
        p2[i] = 9;
    }
}

由于restrict,一个聪明的编译器(或人类)可以将其优化为:

memset(p1, 4, 50);
memset(p2, 9, 50);

这可能会更有效,因为它可以在像样的libc实现(例如glibc)上进行汇编优化:就性能而言,使用std :: memcpy()或std :: copy()更好吗?

海湾合作委员会真的做到了吗?

GCC 5.2.1.Linux x86-64 Ubuntu 15.10:

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

-O0,两者相同。

-O3

  • 有限制:

    3f0:   48 85 d2                test   %rdx,%rdx
    3f3:   74 33                   je     428 <fr+0x38>
    3f5:   55                      push   %rbp
    3f6:   53                      push   %rbx
    3f7:   48 89 f5                mov    %rsi,%rbp
    3fa:   be 04 00 00 00          mov    $0x4,%esi
    3ff:   48 89 d3                mov    %rdx,%rbx
    402:   48 83 ec 08             sub    $0x8,%rsp
    406:   e8 00 00 00 00          callq  40b <fr+0x1b>
                            407: R_X86_64_PC32      memset-0x4
    40b:   48 83 c4 08             add    $0x8,%rsp
    40f:   48 89 da                mov    %rbx,%rdx
    412:   48 89 ef                mov    %rbp,%rdi
    415:   5b                      pop    %rbx
    416:   5d                      pop    %rbp
    417:   be 09 00 00 00          mov    $0x9,%esi
    41c:   e9 00 00 00 00          jmpq   421 <fr+0x31>
                            41d: R_X86_64_PC32      memset-0x4
    421:   0f 1f 80 00 00 00 00    nopl   0x0(%rax)
    428:   f3 c3                   repz retq

    memset按预期的两个电话。

  • 没有限制:没有stdlib调用,只有16个迭代宽的循环展开,我不想在这里重现:-)

我没有耐心对它们进行基准测试,但我相信限制版本会更快。

C99

为了完整起见,让我们看一下标准。

restrict说两个指针不能指向重叠的内存区域。最常见的用法是用于函数参数。

这限制了函数的调用方式,但允许进行更多的编译时优化。

如果呼叫者未遵循restrict合同,则为未定义的行为。

C99 N1256草案 6.7.3 / 7 “类型的限定”说:

限制限定符(如寄存器存储类)的预期用途是促进优化,并且从构成符合程序的所有预处理翻译单元中删除限定符的所有实例不会改变其含义(即,可观察到的行为)。

6.7.3.1“极限的形式定义”给出了详细内容。

严格的别名规则

restrict关键字仅影响兼容类型的指针(例如,两个int*),因为严格的别名规则规定,默认情况下,别名不兼容类型是未定义的行为,因此编译器可以假定它不会发生并进行优化。

请参阅:严格的别名规则是什么?

也可以看看


9
“限制”限定词实际上可以节省更多的钱。例如,给定void zap(char *restrict p1, char *restrict p2) { for (int i=0; i<50; i++) { p1[i] = 4; p2[i] = 9; } },限制限定符将使编译器将代码重写为“ memset(p1,4,50); memset(p2,9,50);”。Restrict大大优于基于类型的别名。遗憾的是,编译器将重点放在后者上。
超级猫

@supercat很好的例子,添加了答案。
西罗Santilli郝海东冠状病六四事件法轮功

2
@ tim18:“ restrict”关键字可以启用许多优化,即使是积极的基于类型的优化也无法实现。此外,语言中“限制”的存在(不同于基于类型的积极别名)永远不会像没有任务时那样高效地完成任务(因为被“限制”破坏的代码可以简单地不使用它,而经常被攻击性TBAA破坏的代码通常必须以低效的方式重写)。
超级猫

2
@ tim18:包围在反引号中包含双下划线的内容,如中所示__restrict。否则,双下划线可能会被误解为您在喊叫。
supercat

1
比起大喊大叫更重要的是,下划线的含义与您要提出的要点直接相关。
回收
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.