C++高性能优化编程之如何语句级优化(二)

系列文章目录

C++高性能优化编程系列
深入理解设计原则系列
深入理解设计模式系列
高级C++并发线程编程

优化热点语句

  • 系列文章目录
  • 1、循环优化 - 从循环中移除哪些代码才能提高性能呢?
  • 2、函数优化 - 从函数又是如何优化的呢?
  • 3、表达式优化 - 原来可以这样做
  • 4、控制流程优化 - 我们来看看
  • 5、小结

心态决定行为,行为决定态度,态度决定命运!
Attitudes Towards Success

1、循环优化 - 从循环中移除哪些代码才能提高性能呢?

一个循环由两部分组成的:

  • 一段被重复执行的控制语句;
  • 一个确定需要进行多少次循环的控制分支。

通常情况下,移除C++语句中的计算指的是移除循环中控制语句的计算。不过在循环中,控制分支也有额外的优化机会,因为从某种意义上说,它产生了额外的开销。

代码示例:接下来,遍历字符串,找出空格用信号代替,这是未优化的for循环。

    // 1、未优化的for循环
    //描述: 遍历字符串,找出空格用信号代替
    {
        Stopwatch sw("For_Un_Optimization");
        cout << ">>未优化的For循环" << endl;
        sw.Start();
        for (int i = 0; i < 100000; ++i) {
            char s[] = "This string many space chars. ";
            for (size_t i = 0; i < strlen(s); ++i) {
                if (s[i] == ' '){
                    s[i] = '*';
                }
            }
        }
        sw.Stop();
    }

运行输出:

>>未优化的For循环
[ For_Un_Optimization Start ]
[ For_Un_Optimization Stop  ] Use Time : 50ms

优化循环 1.1 - 缓存循环结束(不变的)条件值

我们可以通过进入循环时预计算并缓存循环的条件值,即调用开销昂贵的strlen()的返回值,来提高程序性能。修改循环代码如下:

    //优化1.1:缓存循环结束条件值
    {
        Stopwatch sw("For_Optimization_1");
        cout << ">>优化1.1:缓存循环结束条件值" << endl;
        sw.Start();
        for (int i = 0; i < 100000; ++i) {
            char s[] = "This string many space chars. ";
            for (size_t i = 0, len = strlen(s); i < len; ++i) {
                if (s[i] == ' '){
                    s[i] = '*';
                }
            }
        }
        sw.Stop();
    }

运行输出:

>>优化1.1:缓存循环结束条件值
[ For_Optimization_1 Start ]
[ For_Optimization_1 Stop  ] Use Time : 17ms

优化循环 1.2 - 使用更高效的循环语句

将一个for循环简化为do循环通常可以提高循环处理速度。以下是一个将遍历字符串的for循环代码转换为do循环的例子。

    //优化2:使用更高效的循环语句
    {
        Stopwatch sw("For_Optimization_2");
        cout << ">>优化2:使用更高效的循环语句do-while" << endl;
        sw.Start();
        for (int i = 0; i < 100000; ++i) {
            char s[] = "This string many space chars. ";
            size_t j = 0, len = strlen(s);//for循环初始化表达式
            do {
                if (s[j] == ' '){
                    s[j] = '*';
                }
                ++j; //for循环继续表达式

            } while (j < len); //for循环条件
        }
        sw.Stop();
    }

运行输出:

>>优化1.2:使用更高效的循环语句do-while
[ For_Optimization_2 Start ]
[ For_Optimization_2 Stop  ] Use Time : 16ms

优化循环 1.3 - 用递减代替递增

缓存循环结束条件的另一种方法是用递减代替递增,将循环结束条件缓存在循环索引变量中,许多循环都有一种结束条件判断起来比其他结束条件更高效。eg: for(i = 10;i > 0; i–)。

优化循环 1.4 - 从循环中移除不变性代码

当代码不依赖循环的归纳变量时,它就具有循环不变性。
以下代码是含有循环不变性的代码循环:

    //未优化的For循环:未从循环体中移除不变性的代码
    {
        Stopwatch sw("For_Un_Optimization");
        cout << ">>未优化的For循环:未从循环体中移除不变性的代码" << endl;
        sw.Start();
        int i, x = 234, a[10];
        double j;
        for (int i = 0; i < 100000; ++i) {
            for (int i = 0; i < 10 ; ++i) {
                j = 12.22;
                a[i] = i + j * x * x;
            }
        }
        sw.Stop();
    }

运行输出:

>>未优化的For循环:未从循环体中移除不变性的代码
[ For_Un_Optimization Start ]
[ For_Un_Optimization Stop  ] Use Time : 7ms

将循环不变性代码移动至循环外:

    //优化4:从循环体中移除不变性的代码
    {
        Stopwatch sw("For_Optimization_4");
        cout << ">>优化4:从循环体中移除不变性的代码" << endl;
        sw.Start();
        int i, x = 234, a[10];
        double  j = 12.222;
        int tmp = j * x * x;
        for (int i = 0; i < 100000; ++i) {
            for (int i = 0; i < 10 ; ++i) {
                a[i] = i + tmp;
            }
        }
        sw.Stop();
    }

运行输出:

>>优化1.4:从循环体中移除不变性的代码
[ For_Optimization_4 Start ]
[ For_Optimization_4 Stop  ] Use Time : 4ms

优化循环 1.5 - 从循环中移除无谓的函数调用
一次函数调用可能会执行大量的指令,如果函数具有循环不变性,那么将它移除循环外有助于改善性能。
以下是包含具有循环不变性的函数。

    //未优化的For循环:未从循环体中移除无谓的函数调用
    {
        Stopwatch sw("For_Un_Optimization");
        cout << ">>未优化的For循环:未从循环体中移除无谓的函数调用" << endl;
        sw.Start();
        double x, y;
        double data = 34.6;
        vector<float> vec{1.2, 3.4, 4.5, 5.7, 12.2, 12.4, 23.5};
        for (int i = 0; i < 100000; ++i) {
            for (int j = 0; j < vec.size() ; ++j) {
                x = vec[j] + sin(data);
                y = vec[j] + cos(data);
            }
        }
        sw.Stop();
    }

运行输出:

>>未优化的For循环:未从循环体中移除无谓的函数调用
[ For_Un_Optimization Start ]
[ For_Un_Optimization Stop  ] Use Time : 55ms

将具有循环不变性的函数移动至循环体外。

    //优化5:从循环体中移除无谓的函数调用
    {
        Stopwatch sw("For_Optimization5");
        cout << ">>优化4:从循环体中移除无谓的函数调用" << endl;---
        sw.Start();
        double x, y;
        double data = 34.6;
        double sin_data = sin(data);
        double cos_data = cos(data);
        vector<float> vec{1.2, 3.4, 4.5, 5.7, 12.2, 12.4, 23.5};
        for (int i = 0; i < 100000; ++i) {
            for (int j = 0; j < vec.size() ; ++j) {
                x = vec[j] + sin_data;
                y = vec[j] + cos_data;
            }
        }
        sw.Stop();
    }

运行输出:

>>优化1.5:从循环体中移除无谓的函数调用
[ For_Optimization5 Start ]
[ For_Optimization5 Stop  ] Use Time : 11ms

优化循环 1.6 - 从循环中移除隐含的函数调用

如果复制语句和初始化声明具有循环不变性,那么我们可以将它们移动到循环外部。有时,即使需要每次都将变量传递到循环中,你也可以将声明移动到循环外部。
例如,std::string是一个含有动态分配内存的字符数组的类,在以下代码中:

    {
        for(...){
            string s("

"); ... s += "

"; } }

在for循环中声明s的开销是昂贵的。在循环语句的块的反大括号的位置将会调用s的析构函数,而析构函数会释放为s的动态分配的内存,因此当下一次进入循环时,一定会重新分配内存。这段代码可以被优化为:

    {
        string s("

"); for(...){ s.clear(); s += "

"; ... s += "

"; } }

现在,不会再每次循环中都调用s的析构函数了。这不仅仅是每次循环中都节省了一次函数调用,同时,还带了其他效果,由于s内部的动态数组被复用,因此当s中添加字符串时可能会移除一次内存管理器的调用。

2、函数优化 - 从函数又是如何优化的呢?

函数优化 - 2.1 简短的声明内联函数

内联函数定义:
非类成员函数,在函数定义处(不是声明处)增加inline关键字
类成员函数,在类体内部(类定义头文件中)定义的函数默认就是内联函数

内联函数的优缺点:
常规函数调用时,程序需要由主程序根据函数地址跳转到函数执行处,函数执行完后再跳转回主程序,函数来回跳转需要一定的开销。
而内联函数在编译过程中将函数代码和主程序代码“内联”起来,对于内联代码,程序在执行时无需跳转,运行速度比常规函数较快,但是代价是需要占用更多的内存。如果程序多个地方调用同一个内联函数,就需要将内联函数代码拷贝多份,加载到内存中。所以内联函数一般不超过10行。

函数优化 - 2.2 在使用之前定义的函数

在第一次调用函数之前定义函数(提供函数体)给了编译器优化函数调用的机会。当编译器编译对某个函数的调用时发现该函数已经被定义了,那么编译器能够自主选择内联这次函数调用。如果编译器能够同时找到函数体,以及实例化那些发生虚函数调用的类变量、指针或是引用的代码,那么这样也同样适应于虚函数。

函数优化 - 2.3 移除未使用的多态性

虚成员函数多用来实现运行时多态。通过虚函数表选择哪种实现,虚函数表也是操作内存的。多态仍然可能带来不必要的开销。对于设计者来说接口封装虚函数是必要的,但是考虑性能移除未使用的多态,是需要设计和开发人员取舍。

函数优化 - 2.4放弃不使用的接口

基类通过声明一组纯虚函数(有函数声明,没有函数体的函数)定义接口。由于纯虚函数没有函数体,因此C++不允许实例化接口基类。继承类可以通过重写接口基类中的所有纯虚函数来实现接口。可以放弃放弃不使用的虚接口。

函数优化 - 2.5 将虚析构函数函数移至基类中

任何有继承类的类的析构函数都应当被声明为虚函数,这是有必要的,这样delete表达式将会引用一个指向基类的指针,继承类和基类的析构函数都会被调用。

另外一个在继承层次关系顶端的基类中声明虚函数的理由是:确保在基类中有虚函数表指针。

当用虚函数实现多态的时候,子类的的析构无法进行。子类的析构函数没有被调用,子类的资源内存没有被释放,这会造成内存泄漏。

所以,为了避免子类的析构无法执行而造成的内存泄漏问题,应该把最远端父类的析构函数定义为虚析构。

虚析构的语法即是在最远端父类的析构函数名前加virtual进行修饰即可。

3、表达式优化 - 原来可以这样做

表达式优化 - 3.1 简化表达式

多项式 y = ax^3 + bx ^2 + cx + d; 在C++中可以写为:
y = a * x * x * x + b * x * x + c *x + d; 6次乘法运算和3次加法运算;
在简化 y = (((a * x + b) * x) + c) * x + d; 3次乘法运算和3次加法运算。

表达式优化 - 3.2 将常量组合在一起

计算常量表达式:
seconds = 24 * 60 * 60 * days;
或是
seconds = days * (24 * 60 * 60 );
编译器会计算表达式中的常量部分,产生类似下面的表达式:
seconds = 86400 * days;
但是,如果这样写:
seconds = 24 * 60 * 60 * days;
编译器只能在运行时进行乘法计算了。

我们应当总是用括号将常量表达式组合到一起,或是将它们放在表达式的最左端,或者是更好的一种做法是将它们独立出来初始化给一个常量。

表达式优化 - 3.3 使用更高效的运算符

整数表达式行:
x * 4可以被重编码高效的x << 2;(指数2的幂)
x * 9 可以被重写为x * 8 + x * 1, 进而可以被重写为(x << 3) + x。

表达式优化 - 3.4 使用整数计算代替浮点型计算

以下是一些替代浮点数计算的整数计算方法:

乘以10的n次幂:例如,将浮点数乘以10的2次幂可以转换为整数乘以100。

消除小数部分:例如,将浮点数转换为整数,然后进行计算,在结果中再将整数转换回浮点数。

使用定点数:将小数部分转换为整数,然后将整数和小数部分分别存储在两个整数变量中,以定点数的形式表示浮点数。

使用位运算:一些浮点数运算可以使用位运算来实现,例如,浮点数乘以2可以通过将二进制数左移一位来实现。

但需要注意的是,在使用整数计算时,需要注意数值溢出的问题,以及如何处理整数的舍入和截断。

表达式优化 - 3.5 双精度类型可能比浮点型更快

在一些计算机体系结构中,双精度类型可能比浮点型更快,因为硬件实现了更快速的双精度浮点运算。这通常是因为双精度类型可以使用更宽的数据通路,在同一时钟周期内处理更多的数据。

在另一些计算机体系结构中,浮点型可能比双精度类型更快,因为硬件对浮点型的优化更好。此外,如果精度不是非常重要,那么使用浮点型可以减少内存和缓存的使用,从而提高性能。

另外,应该注意到,双精度类型通常需要更多的内存和存储器带宽,因此在大规模计算和并行计算中可能会受到限制。

是否双精度类型比浮点型更快取决于具体的情况,包括所使用的计算机体系结构、所进行的计算操作、对精度的要求等等。

4、控制流程优化 - 我们来看看

控制流程优化 - 4.1 用switch语句代替if-else if-else

if-else if-else语句中的控制流程是线性的,条件逐一判断都要判断,直到ture为止,如果这段代码执行频繁,开销将会显著增加。

switch值与一系列常量进行比较,这样编译器可以进行一系列有效的优化。

可以选择用switch语句代替if-else if-else语句。

控制流程优化 - 4.2 用虚函数代替switch或if
虚函数是一种函数,可以在基类中声明,在派生类中重写并实现。在运行时,通过基类指针或引用来调用实际的派生类函数。使用虚函数可以在运行时动态地确定要调用哪个函数,而不是在编译时确定。

为了使用虚函数代替switch或if语句,你可以在基类中声明一个虚函数,让派生类实现该函数以代替switch或if语句。例如,假设你有一个基类Animal,其中有一个虚函数speak(),派生类Cat和Dog分别实现了该函数。你可以使用Animal指针来动态调用该函数,而不是使用switch语句来确定要调用哪个函数。例如:

Animal* animal;
int choice;
cout << "Enter 1 for cat, 2 for dog: ";
cin >> choice;
if (choice == 1) {
    animal = new Cat();
} else if (choice == 2) {
    animal = new Dog();
} else {
    animal = new Animal();
}
animal->speak();

在这个例子中,通过输入的值选择要创建的动物对象,并将其存储在Animal指针中。然后调用虚函数speak(),它将根据派生类实际调用Cat或Dog版本的该函数,而不是使用switch或if语句来确定要调用哪个函数。

总之,虚函数可以动态地调用派生类实现的函数,从而替代使用switch或if语句来确定要调用哪个函数。

控制流程优化 - 4.3 使用无开销的异常处理
无开销的异常处理是指在程序运行时,当出现异常时不会对程序的性能和效率产生很大的影响。以下是使用无开销的异常处理的几个建议:

仅在必要的情况下使用异常处理:异常处理应该被视为一种特殊情况的处理方式,而不是一种常规的处理方式。只有在必须处理错误的情况下才应该使用异常处理。

不要在循环中使用异常处理:在循环中使用异常处理会导致性能下降,因为每次出现异常都需要执行异常处理代码,而循环的次数通常非常大。

避免使用捕获所有异常的语句:使用“捕获所有”的语句会使程序难以调试,因为无法确定是哪种异常导致了问题。

使用try-finally块来释放资源:在处理一些需要释放资源的情况下,使用try-finally块可以确保资源被正确释放,而不会影响程序的性能和可靠性。

避免使用异常来控制程序流程:异常处理不应该用于控制程序流程,因为这会使程序难以理解和调试。应该使用条件语句来控制程序流程。

总之,无开销的异常处理需要在确保程序性能和可靠性的同时,尽可能地减少对程序的影响。

noexcept是C++11引入的一个关键字,用于指示一个函数是否可能抛出异常。当一个函数被标记为noexcept时,意味着它不会抛出任何异常。这个特性可以帮助编译器进行优化,提高程序的性能,并且可以在编译期进行更好的错误处理。

如果一个函数被标记为noexcept,但实际上它确实抛出了异常,那么程序会调用std::terminate来终止程序的执行。因此,在使用noexcept时,需要确保该函数不会抛出异常,否则会导致程序崩溃。

5、小结

优化热点语句也可以从以下几个方面入手:
减少函数调用次数:函数调用会造成额外的开销,因此可以尝试将一些重复调用的函数合并成一个函数,或者将一些简单的计算转化为内联函数,从而减少函数调用次数。

调整代码结构:在算法实现中,尝试将热点语句中的循环结构、分支语句等调整为更加高效的结构。

使用更加高效的数据结构:例如,在查找或排序时,可以使用更加高效的数据结构,例如哈希表、堆等,替代常规的数组或链表。

利用编译器优化:现代编译器已经内置了许多优化功能,例如死代码删除、循环展开、去除空闲代码等,可以根据具体情况设置编译器选项,从而获得更好的优化效果。

并行化处理:对于一些计算密集型的任务,可以尝试使用多线程或GPU并行化处理,以提升计算速度。

你可能感兴趣的:(C++高性能优化系列,c++,性能优化,linux)