C 16_C语言restrict的用法详解

一、C语言restrict关键字的作用

1.1 restrict关键字简介

restrict 是c99标准新增的一个关键字,是一种类型限定符(Type Qualifiers)。

restrict 关键字修饰一个指针,并告诉编译器,它不会和其它指针指向同一个地址,从而让编译器优化代码。

C程序代码通过该关键字告诉编译器,代码中 restrict指针 独占其指向的内存,所有访问/修改其内存的操作都是基于该指针的,没有其它直接或间接的方式(其它变量或指针),以便编译器进行更好的代码优化和生成更高效的汇编代码。

restrict的优化效果是不一定的,只是帮助编译器优化特定场景,出现完全没有优化也是可能的,如果不使用restrict,在特定场景下编译器是不会进行优化。

restrict指针的独占作用是由程序员代码编写保证的,不是由编译器保证的,并不准确和稳妥。

Tips: 关键字restrict只用于限定指针。

Tips: 需要注意的是,在C++中,并无明确统一的标准支持restrict关键字。但是很多编译器实现了功能相同的关键字,例如g cc 和 clang 中的 __restrict关键字

1.2 restrict 指针的编译优化

restrict 的出现是为了解决指针的一部分缺陷(功能强大,限制方法较少,使用过于自由),例如:

  • 两个指针指向同一块内存或者指向的内存存在重叠,编译器无法获知这些情况,为了保证程序运行的正确性,编译结果可能会多次读取同一地址内存,如下:
1
2
3
4
5
6
int foo(int *a, int *b) 
{
    *a = 5;
    *b = 6;
    return *a + *b;
}

上述代码中,如果 a 和 b 指向同一块内存(a == b),运行结果是12;如果是不同内存(a != b),运行结果是11。 编译器为了保证运行结果的正确性,就需要每次都从内存中读取,汇编代码如下:

1
2
3
4
5
6
foo:
    movl    $5, (%rdi)    # 存储 5 至 *a
    movl    $6, (%rsi)    # 存储 6 至 *b
    movl    (%rdi), %eax  # 重新读取 *a (因为有可能被上一行指令造成改变)
    addl    $6, %eax      # 加上 6
    ret
  • 如果可以确保两个指针不指向同一数据,就可以用 restrict修饰指针类型:
1
2
3
4
5
6
int foo(int *restrict a, int *restrict b) 
{
    *a = 5;
    *b = 6;
    return *a + *b;
}

编译器就可以根据这个信息,做出如下优化:

1
2
3
4
5
foo:
    movl      $11, %eax   # 在编译期已计算出 11
    movl      $5, (%rdi)  # 存储 5 至 *a
    movl      $6, (%rsi)  # 存储 6 至 *b
    ret

通过无restrict和有restrict两种情况下的汇编指令可看到,后者比前者少访问一次内存,且少执行一条指令。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

int add1(int* a, int* b)
{
    *a = 10;
    *b = 12;
    return *a + *b;
}

int add2(int* __restrict  a, int* __restrict b) 
{
    *a = 10;
    *b = 12;
    return *a + *b ;
}


int main() 
{
    int * a = new int;
    int * b = new int;


    {
        std::chrono::steady_clock::time_point begin = std::chrono::steady_clock::now();
        for (size_t i=0; i<100000000; i++)
            add1(a, b);
        std::chrono::steady_clock::time_point end = std::chrono::steady_clock::now();
        std::cout << "Time difference = " << std::chrono::duration_cast<std::chrono::nanoseconds> (end - begin).count() << "[ns]" << std::endl;
    }


 {
        std::chrono::steady_clock::time_point begin = std::chrono::steady_clock::now();
        for (size_t i=0; i<100000000; i++)
            add2(a, b);
        std::chrono::steady_clock::time_point end = std::chrono::steady_clock::now();
        std::cout << "Time difference = " << std::chrono::duration_cast<std::chrono::nanoseconds> (end - begin).count() << "[ns]" << std::endl;
    }
    return 0;
}

二、restrict关键字的使用

2.1 restrict关键字的使用场景

  • 非常需要性能: 使用restrict关键字需要程序员来保证,指针对内存的独占性,有些情况下,程序员也不能保证,如果无法保证,就会出现问题,并且问题不好确认,所以可能为了性能增加了代码风险,得不偿失。
  • 明确知道某两个指针在业务逻辑上不会、也不能重叠.

示例1. 函数参数限制

1
2
3
4
5
6
int xxx(int *restrict a, int *restrict b) 
{
    // do some work

    return *a + *b
}

示例2. 变量限制

1
int * restrict xxx = (int *)malloc(10 * sizeof(int));

示例3. C标准库示例

1
2
3
4
void *memcpy( void * restrict dest ,const void * restrict src,size_t n) 
void memmove(void *dest,const void * src,size_t n)

// memcpy是内存复制函数,由于两个参数都加了restrict限定,所以两块区域不能重叠,memmove 则可以重叠。

2.2 restrict关键字的编译选项

编译时, 需要加上 –std=c99 编译选项,不然编译器识别不了 restrict,必须要加上优化选项,否则编译器会忽略 restrict。

1
gcc --std=c99 -O1 xxx.c

Tips: 注意使用restrict的时候,编程者必须确保不会出现 pointer aliasing, 即同一块内存无法通过两个或以上的指针变量名访问。不满足这个条件而强行指定restrict, 将会出现 undefined behavior

Licensed under CC BY-NC-SA 4.0