C99'restrict'关键字的现实用法?
我正在浏览一些文档和问题/答案,并看到它提到。 我读了一个简短的描述,指出这基本上是程序员的承诺,指针不会被用来指向别的地方。
任何人都可以提供一些现实的情况下,其价值实际上使用?
restrict
表示指针是访问底层对象的唯一东西。 它消除了指针混叠的可能性,使编译器能够更好地进行优化。
例如,假设我有一台机器,它具有可以在内存中乘以数字向量的专用指令,并且具有以下代码:
void MultiplyArrays(int* dest, int* src1, int* src2, int n) { for(int i = 0; i < n; i++) { dest[i] = src1[i]*src2[i]; } }
如果dest,src1和src2重叠,编译器需要正确处理,这意味着它必须从开始到结束一次执行一次乘法运算。 通过restrict
,编译器可以自由地优化这个代码来使用向量指令。
编辑:维基百科有restrict
条目,另一个例子, 在这里 。
维基百科的例子 非常有启发性。
它清楚地显示了它如何保存一条汇编指令 。
没有限制:
void f(int *a, int *b, int *x) { *a += *x; *b += *x; }
伪assembly:
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);
伪assembly:
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
=第三个参数
海湾合作委员会的输出比维基文章更清晰: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; } }
由于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“types限定符”说:
限定限定符(比如寄存器存储类)的用途是促进优化,并且从组成合格程序的所有预处理翻译单元删除限定词的所有实例不会改变其含义(即,可观察到的行为)。
和6.7.3.1“限制的正式定义”给出了细节。
严格的锯齿规则
restrict
关键字只影响兼容types的指针(例如两个int*
),因为严格的别名规则说默认的别名不兼容types是未定义的行为,所以编译器可以认为它不会发生并优化。
请参阅: 什么是严格的别名规则?
也可以看看
- C ++ 14还没有一个模拟的
restrict
,但是GCC有__restrict__
作为扩展: C ++中的restrict关键词是什么意思? - 许多问题提出:根据细节,这个代码UB或不?
- 通过示例了解限制限定符
- 限制指针问题
- 将受限制的指针指定给另一个指针并使用第二个指针来修改该值是否合法?
- 一个“何时使用”的问题: 什么时候使用限制,什么时候不使用
- 相关的GCC
__attribute__((malloc))
,它表示函数的返回值不是别名的: GCC:__attribute __((malloc))