Restricted Pointer概念在 C99 标准中被首次引入,用以缓建 C-style 编程语言存在的 别名 问题(Aliasing)。这个问题主要表现为,为了保证代码的功能正确性,会在一定程度上限制编译器优化代码的尺度,影响的优化包括代码重排(code re-ordering)、表达式消除(sub-expression elimination)等等。
举个例子
一段 C++ 示例代码如下:
void v_add (const int* a,
const int* b,
int* c) {
c[0] = a[0] + b[0];
c[1] = a[0] + b[0];
c[2] = a[0] + b[0] + c[1];
c[3] = a[0] + b[0];
}
具体来说,在计算c[0]
的时候,需要使用a[0]
和b[0]
,而由于后续还会使用到这两个地址的数据,因此可以在第一次计算的时候将这两个值放到寄存器里,之后的计算可以直接复用寄存器里的值,从而实现优化。
然而现实是,这仅仅是在这个函数的作用域内部看,当我们把视角放大到函数外面,由于别名机制的存在,指针a
、b
、c
有可能指向的对象是相同的。
假如c
是a
的别名,那么在第一次更新完c[0]
后,其实a[0]
的值也更新了。这种情况下由于编译器并不知道程序员到底是怎么想的,为了保持程序的正确性以及编程语言抽象模型的一致性。只能放弃之前的优化,也就是不能把a[0]
放在寄存器并复用,每次计算都要根据地址重新取值。
加入__restrict__关键字
那么如何才能让编译器去执行优化呢?那就是用__restrict__
去修饰指针。这个关键字显式告诉了编译器,这几个指针变量并不是别名,而是独立分隔开的对象。于是编译器就能够知道,并进行相应的优化。
void v_add (const int* __restrict__ a,
const int* __restrict__ b,
int* __restrict__ c) {
c[0] = a[0] + b[0];
c[1] = a[0] + b[0];
c[2] = a[0] + b[0] + c[1];
c[3] = a[0] + b[0];
}
这里,我分别对上述代码使用 gcc(aarch64-linux-gnu-c++) 进行编译,并加-O2
优化flag。
加了__restrict__
:
_Z5v_addPKiS0_Pi:
.LFB33:
.cfi_startproc
// main.cc:7: c[0] = a[0] + b[0];
ldr w1, [x1] //, *b_7(D)
ldr w0, [x0] //, *a_6(D)
add w0, w0, w1 // _3, *a_6(D), *b_7(D)
// main.cc:8: c[1] = a[0] + b[0];
stp w0, w0, [x2] // _3, _3, *c_8(D)
// main.cc:9: c[2] = a[0] + b[0] + c[1];
lsl w1, w0, 1 // tmp101, _3,
// main.cc:10: c[3] = a[0] + b[0];
stp w1, w0, [x2, 8] // tmp101, _3, MEM[(int *)c_8(D) + 8B]
// main.cc:11: }
ret
.cfi_endproc
没加__restrict__
:
_Z5v_addPKiS0_Pi:
.LFB33:
.cfi_startproc
// main.cc:7: c[0] = a[0] + b[0];
ldr w4, [x1] //, *b_16(D)
ldr w3, [x0] //, *a_15(D)
add w3, w3, w4 // tmp108, *a_15(D), *b_16(D)
// main.cc:7: c[0] = a[0] + b[0];
str w3, [x2] // tmp108, *c_17(D)
// main.cc:8: c[1] = a[0] + b[0];
ldr w3, [x1] //, *b_16(D)
ldr w4, [x0] //, *a_15(D)
add w4, w4, w3 // _6, *a_15(D), *b_16(D)
// main.cc:8: c[1] = a[0] + b[0];
str w4, [x2, 4] // _6, MEM[(int *)c_17(D) + 4B]
// main.cc:9: c[2] = a[0] + b[0] + c[1];
ldr w3, [x0] //, *a_15(D)
ldr w5, [x1] //, *b_16(D)
add w3, w3, w5 // tmp113, *a_15(D), *b_16(D)
// main.cc:9: c[2] = a[0] + b[0] + c[1];
add w3, w3, w4 // tmp116, tmp113, _6
// main.cc:9: c[2] = a[0] + b[0] + c[1];
str w3, [x2, 8] // tmp116, MEM[(int *)c_17(D) + 8B]
// main.cc:10: c[3] = a[0] + b[0];
ldr w0, [x0] //, *a_15(D)
ldr w1, [x1] //, *b_16(D)
add w0, w0, w1 // tmp117, *a_15(D), *b_16(D)
// main.cc:10: c[3] = a[0] + b[0];
str w0, [x2, 12] // tmp117, MEM[(int *)c_17(D) + 12B]
// main.cc:11: }
ret
.cfi_endproc
可以看到差别还是很明显的,加了关键字的代码直接使用寄存器做复用,没加关键字的代码需要每次都去重新取值。