我正在浏览一些文档和问题/答案并看到它提到了。我读了一个简短的描述,声明它基本上是程序员的承诺,指针不会用于指向其他地方。
任何人都可以提供一些实际情况,其值得实际使用吗?
答案 0 :(得分:163)
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
上有一个条目,另一个例子是here。
答案 1 :(得分:124)
Wikipedia example 非常有启发性。
它清楚地显示了如何保存一条汇编指令。
没有限制:
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真的这么做吗?
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)
对于没有经验的人,calling convention是:
rdi
=第一个参数rsi
=第二个参数rdx
=第三个参数GCC输出甚至比维基文章更清晰: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)上进行程序集优化:Is it better to use std::memcpy() or std::copy() in terms to performance?
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
按预期进行两次memset
次调用。
没有限制:没有stdlib调用,只是一个16迭代宽loop unrolling我不打算在这里重现: - )
我没有耐心对它们进行基准测试,但我相信限制版本会更快。
<强> C99 强>
让我们看一下标准的完整性。
restrict
说两个指针不能指向重叠的内存区域。最常见的用法是函数参数。
这限制了函数的调用方式,但允许更多的编译时优化。
如果调用者未遵循restrict
合同,则定义为未定义的行为。
C99 N1256 draft 6.7.3 / 7“类型限定词”说:
限制限定符(如寄存器存储类)的预期用途是促进优化,并且从构成符合程序的所有预处理转换单元中删除限定符的所有实例不会改变其含义(即,可观察行为)。
和6.7.3.1“限制的正式定义”给出了血淋淋的细节。
严格别名规则
restrict
关键字仅影响兼容类型的指针(例如两个int*
),因为严格的别名规则表明,默认情况下别名不兼容类型是未定义的行为,因此编译器可以假设它不会发生并优化了。
请参阅:What is the strict aliasing rule?
另见
restrict
的模拟,但GCC的__restrict__
作为扩展:What does the restrict keyword mean in C++? __attribute__((malloc))
,它表示函数的返回值没有别名:GCC: __attribute__((malloc))