”危险“的restrict与GCC的编译优化

时间:2023-01-11 18:13:05

restrict是C99标准中新添加的关键字,对于从C89标准开始起步学习C语言的同学来说(包括我),第一次看到restrict还是相当陌生的。Wikipedia给出的解释如下:

In the C programming language, as of the C99 standard, restrict is a keyword that can be used in pointer declarations. The restrict keyword is a declaration of intent given by the programmer to the compiler. It says that for the lifetime of the pointer, only it or a value directly derived from it (such as ​pointer + 1​) will be used to access the object to which it points. This limits the effects of pointer aliasing, aiding caching optimizations. If the declaration of intent is not followed and the object is accessed by an independent pointer, this will result in undefined behavior.

简单说来,restrict关键字是编程者对编译器所做的一个“承诺”:使用restrict修饰过的指针,它所指向的内容只能经由该指针(或从该指针继承而来的指针,如通过该指针赋值或做指针运算而得到的其他指针)修改,而不会被其他不相干的指针所修改。

那么这个承诺有什么用呢?有了编程者的承诺,编译器便可以对一些通过指针的运算进行大胆的优化了。

观察编译器优化的最好办法当然是查看编译后的汇编代码。Wikipedia上有一个很好的例子,我移植如下,特地在自己的机器上测试了一下,结论很有趣。

测试环境:Ubuntu 11.04 (x86-64) + Linux 2.6.38  + gcc 4.5.2

测试代码如下:

”危险“的restrict与GCC的编译优化
 1 #include <stdio.h>
3
4 #ifdef RES
5 void multi_add(int* restrict p1, int* restrict p2, int* restrict pi)
6 #else
7 void multi_add(int* p1, int* p2, int* pi)
8 #endif
9 {
10 *p1 += *pi;
11 *p2 += *pi;
12 }
13
14 int main()
15 {
16 int a = 1, b = 2;
17 int inc = 1;
18
19 // increase both a and b by 1
20 multi_add(&a, &b, &inc);
21
22 // print the result
23 printf("a = %d, b = %d\n", a, b);
24 }
”危险“的restrict与GCC的编译优化

multi_add函数的功能很简单,将p1和p2指针所指向的内容都加上pi指针的内容。为了测试方便,使用了条件编译指令:如果定义RES宏,则使用带restrict的函数声明。(对于gcc,要开启默认关闭的c99支持:--std=c99)

分别编译出两个版本的程序:

gcc restrict.c -o without_restrict
gcc restrict.c -o with_restrict -DRES --std=c99

使用objdump查看目标文件的汇编代码(-d选项表示disassemble):

objdump -d without_restrict

PS:gcc默认使用的是AT&T汇编,与很多同学在初次学习汇编时接触的Intel x86汇编有些不同

除了表示上的细微符号差别,最大的区别是src/dest的顺序,两者恰好相反:

Intel : mov  eax  2      (先dest后src)

AT&T  : mov  %2   %eax   (先src后dest)

 

然而这次的结果让人失望:两个版本的程序拥有一模一样的multi_add函数,汇编代码如下:

”危险“的restrict与GCC的编译优化
push   %rbp
mov %rsp,%rbp
mov %rdi,-0x8(%rbp)
mov %rsi,-0x10(%rbp)
mov %rdx,-0x18(%rbp)
mov -0x8(%rbp),%rax
mov (%rax),%edx
mov -0x18(%rbp),%rax
mov (%rax),%eax
add %eax,%edx
mov -0x8(%rbp),%rax
mov %edx,(%rax)
mov -0x10(%rbp),%rax
mov (%rax),%edx
mov -0x18(%rbp),%rax
mov (%rax),%eax
add %eax,%edx
mov -0x10(%rbp),%rax
mov %edx,(%rax)
leaveq
retq
”危险“的restrict与GCC的编译优化

其中寄存器rdi存放p1的地址,rsi存放p2的地址,rdx存放的是pi的地址。大段的汇编代码,无非是将寄存器中的内容mov到栈上的临时变量上,再把临时变量的值mov进寄存器进行加法运算。

难道restrict关键字没有任何作用?我怀疑很可能是编辑器优化程度不够。这次,使用-O1重新编译源代码并反汇编,终于观察到差别:

未使用restrict的版本:

mov (%rdx), %eax
add %eax, (%rdi)
mov (%rdx), %eax
add %eax, (%rsi)

使用了restrict的版本:

mov (%rdx), %eax
add %eax, (%rdi)
add %eax, (%rsi)

可以看出,-O1的编译优化还是很给力的,所有运算直接在寄存器中进行,不再蛋疼地先mov进栈变量,再mov进寄存器进行add运算(在这个简单的例子中,确实没有必要)。

最大的区别在于将rdx寄存器间接引用的值mov进eax的语句只在一开始执行了1次。可以理解,当程序员“承诺”这些指针都是相互独立不再干扰时,pi指针的内容在函数范围内可以视之为常量,只需要load进寄存器一次。

而没有restrict关键字时,即使程序中没有对pi的内容进行操作,编译器仍然不能保证pi的内容在函数范围内是常量:因为有pointer aliasing的可能,即p1和p2指向的内容和pi相关(简单情况:p1和pi实际是同一个指针)。

需要注意的是,restrict是程序员给出的“承诺“,编译器没有指针的合法使用进行检查的职责,也没有这样的能力。

事实上,打开restrict关键字,如果这样调用:

multi_add(&a, &b, &a);

编译器不会报错。(事实上编译期完全有能力检查出简单alias的pointer)

而使用不同的编译优化级别(不优化,-O1, -O2),则产生了相当不同的结果。

不优化   : a = 2, b = 4

-O1      : a = 2, b = 3

-O2以上: a = 2, b = 4

前面已经提到,没有开启-O选项时,gcc没有对restrict关键字进行优化(至少在这个例子中),所以应当是正确的行为(尽管此行为可能与编写multi_add函数的初衷不符合)

在O1下,restrict被优化,pi的值一开始即被缓存,所以产生了a和b都增加了1的结果

那么为什么O2以上,行为又开始变得正确了呢?

继续反汇编代码,发现-O2以上时,multi_add函数本身代码保持不变(确实在O1已经优化的相当简洁了),但main函数已经面目全非了:调用multi_add的代码已经改变,准确地说:

multi_add函数已经不再被main调用了

这里不再列出相关的汇编代码,因为这里的优化策略是相当复杂的。在这个例子中,由于a和b都是常量,a和b的值直接在编译期被算了出来,并放入寄存器中进行后续printf的调用。

可以看出,restrict确实是优化的利器。但是如果不仔细使用,它还是相当危险的,甚至能够导致在不同的优化级别下,出现完全不同的程序行为。

转自: https://www.cnblogs.com/promise6522/archive/2012/01/08/c99_restrict_and_gcc_optimization.html

”危险“的restrict与GCC的编译优化的更多相关文章

  1. ”危险&OpenCurlyDoubleQuote;的RESTRICT与GCC的编译优化(编程者对编译器所做的一个&OpenCurlyDoubleQuote;承诺”:使用restrict修饰过的指针,它所指向的内容只能经由该指针修改)

    restrict是C99标准中新添加的关键字,对于从C89标准开始起步学习C语言的同学来说(包括我),第一次看到restrict还是相当陌生的.Wikipedia给出的解释如下: In the C p ...

  2. GCC 编译优化指南&lpar;转&rpar;

    GCC 编译优化指南(转) http://www.jinbuguo.com/linux/optimize_guide.html 作者:金步国 版权声明 本文作者是一位开源理念的坚定支持者,所以本文虽然 ...

  3. GCC 编译优化指南

    转自: http://www.jinbuguo.com/linux/optimize_guide.html GCC 编译优化指南 作者:金步国[www.jinbuguo.com] 版权声明 本文作者是 ...

  4. GCC编译优化指南【作者:金步国】

    GCC编译优化指南[作者:金步国] GCC编译优化指南 作者:金步国 版权声明 本文作者是一位*软件爱好者,所以本文虽然不是软件,但是本着 GPL 的精神发布.任何人都可以*使用.转载.复制和再分 ...

  5. GCC 编译优化指南【转】

    转自:http://www.jinbuguo.com/linux/optimize_guide.html 版权声明 本文作者是一位开源理念的坚定支持者,所以本文虽然不是软件,但是遵照开源的精神发布. ...

  6. gcc&sol;g&plus;&plus; -O 优化选项说明

    查查gcc手册就知道了,每个编译选项都控制着不同的优化选项 下面从网络上copy过来的,真要用到这些还是推荐查阅手册 -O设置一共有五种:-O0.-O1.-O2.-O3和-Os. 除了-O0以外,每一 ...

  7. Gcc的编译流程分为了四个步骤&colon;

    http://blog.csdn.net/xiaohouye/article/details/52084770(转) Gcc的编译流程分为了四个步骤: 1.预处理,生成预编译文件(.文件): Gcc ...

  8. GCC的编译和安装 很好的资料

    http://blog.csdn.net/yrj/article/details/492404 1.GCC的编译和安装2.预处理    #define 可以支持不定数量的参数.    例子如下:    ...

  9. 学习总结:gcc&sol;g&plus;&plus; 编译与链接

    gcc/g++ 编译与链接 编译与链接的过程可以分解为四个步骤:预处理.编译.汇编.链接 预处理:源代码文件和相关的头文件,被预处理器cpp预处理成一个后缀为 .i 的文件(选项:-E) 编译:把预处 ...

随机推荐

  1. Struts2 验证码图片实例

    本文转载于DongLiYang的博客http://www.cnblogs.com/dongliyang/archive/2012/08/24/2654431.html 其中修改过一部分,针对使用注解而 ...

  2. EL使用:打印集合

    <%@page import="java.util.HashMap"%><%@page import="java.util.Map"%> ...

  3. C&num;&lowbar;deepCopy

    using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Run ...

  4. 2017JAVA必读书籍

    1.深入理解Java虚拟机:JVM高级特性与最佳实践 2.Oracle查询优化改写技巧与案例 3.Effective Java 4.Spring3.x企业应用开发实战 5.Spring技术内幕:深入解 ...

  5. nginx配置学习文章

    partOne 自我释义部分 我的是阿里云的ubuntu *******实际上感觉这里是基本配置,很用不到*********#定义其用户或用户组user www-data;#nginx的进程数,应当为 ...

  6. HTML系列(一):创建HTML文档

    从本学期开始我打算把我以前学的知识点系统地总结一下,先从HTML开始.(本系列内容总结自博文视点出版社•代码逆袭系列书籍,包括代码片段.) 一.HTML文档类型 HTML版本众多,浏览器如何得知使用的 ...

  7. 找出链表中倒数第K个结点

    思路:两个指针,也是快指针和慢指针,先让快指针走k -1步,这时慢指针开始和快指针一起走到尾部.慢指针停止的点就是倒数第k个节点. public static ListNode findCountDo ...

  8. js--map函数的使用

    map( )  属于操作数组的方法. 包含三个参数,item,index,arr 看一份代码: let arr = [ {title:'aaa',hot:true}, {title:'fff',hot ...

  9. java反射出字段信息和值

    /** * */ package test; import java.lang.reflect.Field; import java.lang.reflect.Modifier; /** * @aut ...

  10. vm虚拟机黑屏解决办法

    以管理员打开cmd 输入 netsh winsock reset --------------------- 然后 reboot