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],而由于后续还会使用到这两个地址的数据,因此可以在第一次计算的时候将这两个值放到寄存器里,之后的计算可以直接复用寄存器里的值,从而实现优化。

  然而现实是,这仅仅是在这个函数的作用域内部看,当我们把视角放大到函数外面,由于别名机制的存在,指针abc有可能指向的对象是相同的。

  假如ca的别名,那么在第一次更新完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

  可以看到差别还是很明显的,加了关键字的代码直接使用寄存器做复用,没加关键字的代码需要每次都去重新取值。