一、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