C99 'restrict' 关键字的实际用法?
-
09-09-2019 - |
题
我正在浏览一些文档和问题/答案,并看到提到了它。我读了一个简短的描述,指出这基本上是程序员的承诺,指针不会被用来指向其他地方。
谁能提供一些值得实际使用的实际案例?
解决方案
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;
}
伪组装:
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)
对于不熟悉的人来说, 调用约定 是:
rdi
= 第一个参数rsi
= 第二个参数rdx
= 第三个参数
GCC 的输出比 wiki 文章更清晰:4 条指令与 3 条指令。
数组
到目前为止,我们节省了单条指令,但是如果指针表示要循环的数组(一个常见的用例),那么可以节省一堆指令,如 超级猫.
例如考虑:
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真的做到了吗?
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*
)因为严格的别名规则规定,默认情况下,别名不兼容类型是未定义的行为,因此编译器可以假设它不会发生并进行优化。
看: 严格的别名规则是什么?
也可以看看
- C++14 还没有类似的
restrict
, ,但海湾合作委员会有__restrict__
作为扩展: C++中的restrict关键字是什么意思? - 问很多问题:根据血淋淋的细节,这个代码是否为UB?
- 一个“何时使用”的问题: 何时使用限制,何时不使用
- 相关海湾合作委员会
__attribute__((malloc))
, ,它表示函数的返回值没有任何别名: 海湾合作委员会:__属性__((malloc))