C++程序员应了解的那些事(48)关键字 mutable ~ volatile ~ 编译器常用优化方法

(1)mutable
          在C++中,mutable是为了突破const的限制而设置的。被mutable修饰的变量,将永远处于可变的状态,即使在一个const函数中,甚至结构体变量或者类对象为const,其mutable成员也可以被修改。

struct  ST
{
    int a;
    mutable int b;
};
 
const ST st={1,2};
st.a=11;//编译错误
st.b=22;//允许

        mutable在类中只能够修饰非静态数据成员。mutable 数据成员的使用看上去像是骗术,因为它能够使const函数修改对象的数据成员。然而,明智地使用 mutable 关键字可以提高代码质量,因为它能够让你向用户隐藏实现细节,而无须使用不确定的东西。我们知道,如果类的成员函数不会改变对象的状态,那么这个成员函数一般会声明成const的。但是,有些时候,我们需要在const的函数里面修改一些跟类状态无关的数据成员,那么这个数据成员就应该被mutalbe来修饰。

class  ST
{
    int a;
    mutable int showCount;
    void Show()const;
    …
};
ST::Show()
{
    …//显示代码
    a=1;//错误,不能在const成员函数中修改普通变量
    showCount++;//正确
}

       const承诺的是一旦某个变量被其修饰,那么只要不使用强制转换(const_cast),在任何情况下该变量的值都不会被改变,无论有意还是无意,而被const修饰的函数也一样,一旦某个函数被const修饰,那么它便不能直接或间接改变任何函数体以外的变量的值,即使是调用一个可能造成这种改变的函数都不行。这种承诺在语法上也作出严格的保证,任何可能违反这种承诺的行为都会被编译器检查出来。
        mutable的承诺是如果某个变量被其修饰,那么这个变量将永远处于可变的状态,即使在一个const函数中。这与const形成了一个对称的定义,一个永远不变,而另外一个是永远可变。
       看一个变量或函数是否应该是const,只需看它是否应该是constant或invariant,而看一个变量是否应该是mutable,也只需看它是否是forever mutative。

    这里出现了令人纠结的3个问题:
①为什么要保护类的成员变量不被修改?
②为什么用const保护了成员变量,还要再定义一个mutable关键字来突破const的封锁线?
③到底有没有必要使用const 和 mutable这两个关键字?
       保护类的成员变量不在成员函数中被修改,是为了保证模型的逻辑正确,通过用const关键字来避免在函数中错误的修改了类对象的状态。并且在所有使用该成员函数的地方都可以更准确的预测到使用该成员函数的带来的影响。而mutable则是为了能突破const的封锁线,让类的一些次要的或者是辅助性的成员变量随时可以被更改。没有使用const和mutable关键字当然没有错,const和mutable关键字只是给了建模工具更多的设计约束和设计灵活性,而且程序员也可以把更多的逻辑检查问题交给编译器和建模工具去做,从而减轻程序员的负担。

(2)volatile
       像const一样,volatile是一个类型修饰符。volatile修饰的数据,编译器不可对其进行执行期寄存于寄存器的优化。这种特性,是为了满足多线程同步、中断、硬件编程等特殊需要。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的直接访问。
        volatile原意是“易变的”,但这种解释简直有点误导人,应该解释为“直接存取原始内存地址”比较合适。“易变”是相对与普通变量而言其值存在编译器(优化功能)未知的改变情况(即不是通过执行代码赋值改变其值的情况),而是因外在因素引起的,如多线程,中断等。编译器进行优化时,它有时会取一些值的时候,直接从寄存器里进行存取,而不是从内存中获取,这种优化在单线程的程序中没有问题,但到了多线程程序中,由于多个线程是并发运行的,就有可能一个线程把某个公共的变量已经改变了,这时其余线程中寄存器的值已经过时,但这个线程本身还不知道,以为没有改变,仍从寄存器里获取,就导致程序运行会出现未定义的行为。并不是因为用volatile修饰了的变量就是“易变”了,假如没有外因,即使用volatile定义,它也不会变化。而加了volatile修饰的变量,编译器将不对其相关代码执行优化,而是生成对应代码直接存取原始内存地址。

一般说来,volatile用在如下的几个地方:
※①  中断服务程序中修改的供其它程序检测的变量需要加volatile;
※②  多任务环境下各任务间共享的标志应该加volatile;
※③  存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能有不同意义;

使用该关键字的例子如下:

volatile int i=10;
int a = i;
...
//其他代码,并未明确告诉编译器,对i进行过操作
int b = i;

       volatile 指出 i是随时可能发生变化的,每次使用它的时候必须从i的地址中读取,因而编译器生成的汇编代码会重新从i的地址读取数据放在b中。而优化做法是,由于编译器发现两次从i读数据的代码之间的代码没有对i进行过操作,它会自动把上次读的数据(即10)放在b中,而不是重新从i里面读。这样以来,如果i是一个寄存器变量或者表示一个端口数据就容易出错,所以说volatile可以保证对特殊地址的直接访问。

//addr为volatile变量
addr=0x57; 
addr=0x58;

        如果上述两条语句是对外部硬件执行不同的操作,那么编译器就不能像对待普通的程序那样对上述语句进行优化只认为“addr=0x58;”而忽略第一条语句(即只产生一条机器代码),此时编译器会逐一的进行编译并产生相应的机器代码(两条)。
        volatile总是与优化有关,编译器有一种技术叫做数据流分析,分析程序中的变量在哪里赋值、在哪里使用、在哪里失效,分析结果可以用于常量合并,常量传播等优化,进一步可以死代码消除。但有时这些优化不是程序所需要的,这时可以用volatile关键字禁止做这些优化,它有下面的作用:
不会在两个操作之间把volatile变量缓存在寄存器中。在多任务、中断等环境下,变量可能被其他的程序改变,编译器自己无法知道,volatile就是告诉编译器这种情况。
不做常量合并、常量传播等优化,所以像下面的代码,if的条件不会当作无条件真。

volatile int i = 1; 
if (i > 0)
    ... 

对volatile变量的读写不会被优化掉。如果你对一个变量赋值但后面没用到,编译器常常可以省略那个赋值操作,然而对Memory Mapped IO的处理是不能这样优化的。

(3)简单解释 常量传播~常量合并/常量折叠

※<常量传播>
     故名思议,在编译优化时,将能够计算出结果的变量直接替换为常量。   如:

 void main()
 {
     int a = 1;
     printf("%d",a);
 }

    编译器在进行编译的时候,将a直接由1代替。优化后如下:

 void main()
{
     printf("%d",1);
}

※<常量合并> / <常量折叠>
        这是VC++编译器最简单的优化之一。这种优化,是指编译器在编译时(编译期间)直接计算出表达式的结果,在生成的代码中直接用计算结果替换表达式。 这样就避免了程序在运行时执行这些计算花费的成本。

优化前:
void main()
{
    printf("%d",3+1-1*5);
}
优化后:
void main()
{
    printf("%d",-1);
}

/
优化前:
void main()
{
    int a = 3+1-1*5;
    printf("%d",a);
}
优化后:
void main()
{
    printf("%d",-1);
}

※<常量合并> / <常量折叠>示例

int main(int argc, char* argv[])
{
    const int i=0;
    int *j = (int *) &i;
    *j=1;

    cout<<&i<<endl;
    cout<<j<<endl;
    cout<<i<<endl;
    cout<<*j<<endl;
    return 0;
}

输出:
0012ff7c
0012ff7c
0
1

       因为i和j都指向相同的内存地址,所以输出的前两个结果是相同的,但为啥相同的内存里的结果不相同么?这就是常量折叠。
       这个"常量折叠"是 就是在编译器进行语法分析的时候,将常量表达式计算求值,并用求得的值来替换表达式,放入常量表。可以算作一种编译优化。(疑惑:感觉常量传播是常量折叠的一种特殊情况啊)
       我只是改了这个地址内容,但是i还是0,
       因为编译器在优化的过程中,会把碰见的const全部以内容替换掉(跟宏似的: #define pi 3.1415,用到pi时就用3.1415代替),这个出现在预编译阶段;但是在运行阶段,它的内存里存的东西确实改变了!!!

(4)编译器常用优化方法

☆常量传播
   在编译优化时, 能够将计算出结果的变量直接替换为常量。

void main(){
    int a=1;
    printf("%d",a);
}
编译器在进行编译的时候,将a直接由1替换。因此优化后的代码为:
void main(){
    printf("%d",1);
}

☆常量折叠
   在编译优化时,多个变量进行计算时,而且能够直接计算出结果,那么变量将有常量直接替换。

void main(){
    int a=3+1-3*1;
    print("%d",a);
}
编译器在进行编译时,将a直接由1替换,因此优化后的代码:
void mian(){
    print("%d",1);
}

☆复写传播
   两个相同的变量可以用一个代替。

public void main(){
    int x=3;
    int y=4;
    y=x;
}
编译器在进行编译的时候,发现y和x相等,因此用x来代替y,优化后的代码如下:
public void mian(){
    int x=3;
}

☆公共子表式消除

       如果一个表达式E已经计算过了,并且从先前的计算到现在的E中的变量都没有发生变化,那么E的此次出现就成为了公共子表达式。

public void main(){
    int a=3;
    int c=8;
    int x=0;
    x=(a+c)*12+(c+a)*2;//此处的a+c便是公共子表达式
}
经过优化之后的代码可能如下:
public void main(){
    int a=3;
    int c=8;
    int x=0;
    x=E*12+E*2;//此时某些编译器还会进行代数化简x=E*14;
}

☆无用代码消除

   永远不能被执行到的代码或者没有任何意义的代码会被清除掉。

public void main(){
    int x=9;
    x=x;
    ...
}
因此优化过后的代码如下:
public void main(){
    int x=9;
    ...
}

☆数组范围检查消除
   数组边界检查不是必须在运行期间一次不漏的检查,而是可以协商的。如果编译器能根据数据流分析出变量的取值范围在[0,max_length]之间,那么在循环期间就可以把数组的上下边界检查消除。

☆方法内联
   编译器最重要的优化手段,可减少方法调用的成本,并为其他优化做基础。

☆逃逸分析
       分析对象动态作用域,一旦确定对象不会发生方法逃逸和线程逃逸,就可以对这个变量进行高效的优化,比如栈上分配、同步消除、标量替换等。
       逃逸分析:主要是用于分析对象的作用域,例如一个对象在方法内部,外部没有它的引用,说明没有发生方法逃逸,反之发生方法逃逸。
 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值