我浏览了一些文档和问题/答案,看到它被提到了。我读了一个简短的说明,说明这基本上是程序员的一个承诺,即指针不会被用来指向其他地方。

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


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 dest、src1和src2重叠,这意味着它必须从头到尾每次做一次乘法。通过限制,编译器可以自由地使用向量指令来优化这段代码。

维基百科有一个关于限制的条目,这里有另一个例子。


维基百科的例子很有启发性。

它清楚地显示了它如何允许保存一个汇编指令。

没有限制:

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 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

限制:

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" is no longer needed.
load R2 ← *b
add R2 += R1
set R2 → *b

GCC真的做到了吗?

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条指令vs 3条指令。

数组

到目前为止,我们只节省了一条指令,但是如果指针表示要循环的数组,这是一个常见的用例,那么就可以节省一堆指令,就像supercat提到的那样。

举个例子:

void f(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);

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

GCC真的做到了吗?

GCC 5.2.1。Linux x86-64 Ubuntu 15.10:

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

对于-O0,两者是一样的。

o3:

with restrict: 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 Two memset calls as expected. without restrict: no stdlib calls, just a 16 iteration wide loop unrolling which I do not intend to reproduce here :-)

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

C99

为了完整起见,让我们看看这个标准。

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

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

如果调用方不遵守限制契约,则未定义行为。

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

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

6.7.3.1“限制的形式化定义”给出了血淋淋的细节。

严格混叠规则

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

看:什么是严格的混叠规则?

另请参阅

C++14 does not yet have an analogue for restrict, but GCC has __restrict__ as an extension: What does the restrict keyword mean in C++? Many questions that ask: according to the gory details, does this code UB or not? Understanding restrict qualifier by examples Restricted pointer questions Is it legal to assign a restricted pointer to another pointer, and use the second pointer to modify the value? A "when to use" question: When to use restrict and when not to The related GCC __attribute__((malloc)), which says that the return value of a function is not aliased to anything: GCC: __attribute__((malloc))


下面的C99代码返回0或1,取决于限制限定符:

__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]);
}

您可以使用该代码片段构建实际的示例,特别是当*a是循环条件时。

用gcc -std=c99 -Wall - pedic -O3 main.c编译。