C++:简单优化编程语言

程序优化不仅包括编程语言,还涉及其他方面。程序运行速度还取决于硬件、编译器以及所选的计算机算法。

无用代码

在编写一个解决方案时,你沿着一个逻辑分支写下去,发现最终行不通。或者你出于调试的目的添加了额外代码,但这段代码并不是解决方案的一部分。无用代码虽然是程序的一部分,但不影响程序实现。它可能拖慢程序。这些代码虽然不会使用,但仍然会在 CPU 上执行,并且可能涉及内存读写操作。

还有两个问题也与无用代码相关,它们有时候也被看作无用代码——冗余代码和执行不到的代码。

冗余代码

//冗余代码案例
int x = 6;
if (x > 5) {
   return true;
}
else {
   return false;
}

本质而言,先说 x > 5 然后返回 True 这部分是冗余的。你可以删除 if 语句:

return x > 5;

另一个例子

 if (x < 5) {
           x = x + 1;
    }
    else if (x >= 5 && x < 10) {
        x = x + 2;
    }

检查x >= 5毫无必要。第一个 if 语句已经证明了 x 和 5 的大小。

执行不到的代码:执行不到的代码,是指永远不会执行的代码。这些代码可能对代码运行速度影响不大,但仍会占用内存空间,导致内存管理效率降低。

unsigned int x;

... 

if (x >= 0) {
   do_something ..
}
else {
  do_something ..
}

未分配的整数始终为零或正数,因此 else 语句永远都不会执行。else 语句里的代码是不可执行的。

If语句

int x = 7;

if (x > 0) {
  return y;
}
if (x <= 0) {
   return z;
}

优化后可能是:

int x = 7;
if (x > 0) {
   return y;
}
else {
   return z;
}

编译器不会真正把你的 C++ 代码重写为优化后的代码。它在输出汇编语言或机器语言时或进行优化。但总体来说,如果 if 的逻辑分支对 CPU 资源占用较多,需要避免过多的逻辑分支。考虑到 if 语句的执行过程,建议把大多数的用例放到更高层次的逻辑分支上,这样更高效。

for (int i = 0; i < 1000; i++) {
        if (i > 0 && i < 5) {
            cout <<  "low \n";
        }
        else if (i >= 990) {
            cout << "high \n";
        }
        else {
            cout << "normal \n";
        }
    }

在大多数情况下,上述代码都会输出单词 “normal”。在大多数时间里,这段代码需要遍历所有的 if 和 else 分支,因此,CPU 会比较每个分支中的i。如果把 “normal” 放到分支顶部,而不是底部,效率会更高。

for (int i = 0; i < 1000; i++) {
    if (i >= 5 && i < 990) {
        cout <<  "normal \n";
    }
    else if (i >= 990) {
        cout << "high \n";
    }
    else {
        cout << "low \n";
    }
}

这样修改后,CPU 在大多数时间里就能跳过 else if 和 else 分支了。

If语句在CPU上的执行:使用 C++ 等高级语言时,if 语句还有一方面不太好控制。CPU 会运行同步运算,试图优化 if 语句的运算。进行运算时,CPU 会提前执行后面的代码,并行开启另一个运算。对 if 语句而言,CPU 会尝试预测接下来进行哪个分支,然后在预测的分支内运行运算。需要评估逻辑表达时,CPU 可能会意识到,预测不够准确。如果预测错误,则预测的运算会停止,CPU 开始运行正确的运算。因此,重排或删除 if 语句可能不会显著缩短时间。因为编译器和 CPU 已经在尝试优化相关操作了。

for循环

使用嵌套 for 循环 (即在 for 循环内部使用 for 循环 ) 没什么问题。有时候,进行 C++ 向量操作时,我们需要这么写但是,不需要的时候,不要这么用!

如果你正在遍历或初始化一个 m 乘以 n 的矩阵,你可能忍不住使用嵌套 for 循环,就像这样:

for (unsigned int i = 0; i < matrix.size(); i++) {
        for (unsigned int j = 0; j < matrix[0].size(); j++) {
            do something...
        }
    }

遍历整个矩阵需要 m 乘以 n 次运算。但是,如果你的目标不同,你也可以尝试以下操作:

for (unsigned int i = 0; i < matrix.size(); i++) {
        do something
    }

    for (unsigned int j = 0; j < matrix[0].size(); j++) {
        do something
    }

这段代码只需要 m+n 次运算,不需要 m 乘以 n 次运算。记住,CPU 要运算的指令数越少,代码执行速度越快!

#include "initialize_matrix.hpp"
using namespace std;
vector < vector<int> > initialize_matrix(int num_rows, int num_cols, int initial_value) {
    
    vector < vector<int> > matrix;
    vector<int> new_row;
    
    for (int i = 0; i < num_rows; i++) {
        new_row.clear();
        for (int j = 0; j < num_cols; j++) {
            new_row.push_back(initial_value);
        }
        matrix.push_back(new_row);
    }
    return matrix;
}
#include "initialize_matrix_improved.hpp"
using namespace std;
vector < vector<int> > initialize_matrix_improved(int num_rows, int num_cols, int initial_value) {
    
    vector < vector<int> > matrix;
    vector<int> new_row;
    
    for (int j = 0; j < num_cols; j++) {
      	new_row.push_back(initial_value);
    }
  
   	for (int i = 0; i < num_rows; i++) {
            matrix.push_back(new_row);
        } 
    return matrix;
}

中间变量

中间变量可以看作冗余代码。

float x = 5.8;
float y = 7.1;

float area = x * y;
float reciprocal_area = 1/(area);

如果只需要计算倒数,可以这么写:

float x = 5.8;
float y = 7.1;
float reciprocal_area = 1/(x*y);

基本的变量类型,如单精度浮点数、整数和字符,相对高效。因此,这两个版本的代码运行起来基本感觉不到差别。实际上,编译器可能已经消除了第一个和第二个版本中低效率的地方。

中间矩阵变量:向量很方便,但效率方面没有优势。编译器会为一个新向量分配一定的内存量,然后再多加几个字节,作为缓冲。缓冲部分能够多保存几个元素,方便你在向量末端添加新元素。但是,当向量超出了分配大小后,整个向量就会被复制到 RAM 中的另一个地方。这非常低效!因此,如果你已经有一个动态改变容量的向量容器,请避免复制向量!

 

#include "scalar_multiply.hpp"
using namespace std;
vector< vector<int> > scalar_multiply(vector< vector<int> > matrix, int scalar) {
    vector< vector<int> > resultmatrix;
    vector<int> new_row;
    
    vector<int>::size_type num_rows = matrix.size();
    vector<int>::size_type num_cols = matrix[0].size();
    
    for (int i = 0; i < num_rows; i++) {
        new_row.clear();
        for (int j = 0; j < num_cols; j++) {
            new_row.push_back(matrix[i][j] * scalar);
        }
        resultmatrix.push_back(new_row);
    }
    return resultmatrix;
}

优化后

#include "scalar_multiply_improved.hpp"
using namespace std;
vector< vector<int> > scalar_multiply_improved(vector< vector<int> > matrix, int scalar) {
    
  	// OPTIMIZATION: Instead of creating a new variable
    // called resultmatrix, update the input matrix directly

    vector<int>::size_type num_rows = matrix.size();
    vector<int>::size_type num_cols = matrix[0].size();
    
    for (int i = 0; i < num_rows; i++) {
        for (int j = 0; j < num_cols; j++) {
            matrix[i][j] = matrix[i][j] * scalar;
        }
    }    
    return matrix;
}

向量内存存储

向量不是 C++ 中最高效的变量。其中一个原因是,声明向量变量时,不需要指定向量长度。因此,编译器不会提前知道需要分配多少内存。一旦向量长度超出了初始分配内存,整个向量就会被复制到空间更大的新 RAM 位置。如果向vector容器插入值之前即可指定向量的长度,向量会更高效。这可以通过 reverse() 方法实现。该方法能保证向量可以存储预留的元素数。

#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> myvector;
    int vector_size = 50;
    myvector.reserve(vector_size);

    for (int i = 0; i < vector_size; i++) {
        myvector.push_back(i);
    }
    return 0;
}

在 C++ 中,有多种方式可以初始化一个二维向量。优化你的程序时,你需要测试不同的初始化方式,确定哪种方式对你的程序效果最好。要保证速度最快,需要考虑向量长度和类型。

引用

每次调用函数时,输入变量都会复制到内存中。要证明这一点,可以使用以下代码:

#include <iostream>
#include <vector>

using namespace std;

//函数声明,打印出矩阵在内存中的地址
void matrix_address(vector< vector<int> > my_matrix);

int main() {

    //初始化矩阵
    vector< vector<int> > matrix(5, vector<int>(6,2));

    //打印出矩阵地址
    cout << "original variable address: " << &matrix << "\n";

    //运行一个函数,打印出矩阵地址
    matrix_address(matrix);


    return 0;
}

//打印出矩阵地址的函数
void matrix_address(vector< vector<int> > my_matrix) {

    cout << "function variable address: " << &my_matrix << "\n";

}

运行这段代码时,你会得到以下这种输出:

original variable address:0x7fff5fbff650

function variable address:0x7fff5fbff608

那么,这段代码的功能是什么?它在一个名为 matrix 的变量中,初始化了一个 5 乘以 6 的二维向量。然后,代码在内存中二维向量开始的地方打印出这个地址。

接下来,代码会调用一个函数,在函数的输入变量的内存中打印出地址。注意,尽管两个变量的值相同,地址并不一样。

这是因为,当你调用 matrix_address() 时,C++ 再次把二维向量复制到了内存中。

&符号:字符:&,给出变量地址,而不是变量值。& 可以轻松地访问变量地址,不会不小心导致内存错误。可以使用 & 提升代码速度!

&提升代码速度:C++ 有几种基本的数据类型,如整数、字符和单精度浮点数,它们速度相对较快。对这些基本的数据类型,我们将要学习的编程策略可能没什么区别。但是,对于占用大量内存的变量,如阵列和向量,& 就会非常有用。

如果上面的 matrix_address 函数是这么定义的呢?

void matrix_address(vector< vector<int> >&my_matrix);

& 符号告诉编译器,根据引用传递输入变量。这表示,在函数内部,你使用的是原始变量,而不是其复制品。对于二维向量,你就可以省略很多读写。

此外,在某些情况下,你还可以直接修改输入向量,无需在函数内部创建一个新向量。例如,如果你准备在某个向量上进行标量乘法编程,而且又不需要保留原始向量,你就可以直接修改原始向量。

#include "matrix_addition.hpp"
using namespace std;
vector < vector <int> > matrix_addition(vector < vector <int> > matrixa, vector < vector <int> > matrixb) {
    
    vector<int>::size_type rows_a = matrixa.size();
    vector<int>::size_type rows_b = matrixb.size();
    vector<int>::size_type cols_a = matrixa[0].size();
    vector<int>::size_type cols_b = matrixb[0].size();
    
    vector < vector <int> > matrix_sum(rows_a, vector<int>(cols_a));
    
    if (rows_a == rows_b && cols_a == cols_b) {
        
        for (unsigned int i = 0; i < rows_a; i++) {
            for (unsigned int j = 0; j < cols_a; j++) {
                matrix_sum[i][j] = matrixa[i][j] + matrixb[i][j];
            }
        }
        return matrix_sum;
    }
    return matrix_sum;
}

优化后

#include "matrix_addition_improved.hpp"
using namespace std;

vector < vector <int> > matrix_addition_improved(vector < vector <int> > &matrixa, vector < vector <int> > &matrixb) {
    
    vector<int>::size_type rows_a = matrixa.size();
    vector<int>::size_type rows_b = matrixb.size();
    vector<int>::size_type cols_a = matrixa[0].size();
    vector<int>::size_type cols_b = matrixb[0].size();
    
    vector < vector <int> > matrix_sum(rows_a, vector<int>(cols_a));
    
    if (rows_a == rows_b && cols_a == cols_b) {
        
        for (unsigned int i = 0; i < rows_a; i++) {
            for (unsigned int j = 0; j < cols_a; j++) {
                matrix_sum[i][j] = matrixa[i][j] + matrixb[i][j];
            }
        }
        return matrix_sum;
    }  
    return matrix_sum; 
}

静态关键词

要是无论调用多少次函数,这些变量只需要声明并定义一次,会怎么样呢?你可以消除大量的内存读写操作。这是 C++ 静态关键词的一个完美用例。

当你在一个 C++ 函数内部声明并定义一个变量时,值会分配到内存中。

例如:

some_function() {
   int x = 5;
}

这段代码先在内存中为变量 x 分配空间,然后为其赋值 5。接下来,函数完成时,CPU 会从 RAM 中删除变量 x。这意味着每次运行函数时,CPU 都需要为 x 变量分配内存,然后解除分配。

反过来,如果你的代码使用了静态关键词,那么 x 变量就会在函数第一次运行时分配到内存中。这样,x 变量在整个程序的持续期间,都会保留在内存中。这样,你就省去了一些 RAM 读取次数:

some_function() {
    static int x = 5;
}

注意,你需要同时声明和定义变量。在使用静态关键词定义变量时,不给变量赋值是不可能的。

全局变量VS静态变量:实际上,在内存中,静态变量和全局变量放置的位置是相同的。不同之处在于,全局变量在函数外部声明,程序中的任何函数或文件都可以使用。反过来,静态变量是在函数内部的。


#include "blur_factor.hpp"

using namespace std;

vector < vector <float> > blur_factor() {

    // 2D vector reprenting the blur filter
    vector < vector <float> > window;
    vector <float> row;
    float blurring, center, corner, adjacent;

    // calculate blur factors
    blurring = .12;
    center = 1.0 - blurring;
    corner = blurring / 12.0;
    adjacent = blurring / 6.0;

    int i, j;
    float val;

    // create the blur window
    for (i=0; i<3; i++) {
        row.clear();
        for (j=0; j<3; j++) {
            switch (i) {
                case 0:
                    switch (j) {
                        case 0:
                            val = corner;
                            break;
                            
                        case 1:
                            val = adjacent;
                            break;
                            
                        case 2:
                            val = corner;
                            break;
                    }
                    break;
                    
                case 1:
                    switch (j) {
                        case 0:
                            val = adjacent;
                            break;
                            
                        case 1:
                            val = center;
                            break;
                            
                        case 2:
                            val = adjacent;
                            break;
                    }
                    break;
                    
                case 2:
                    switch(j) {
                        case 0:
                            val = corner;
                            break;
                            
                        case 1:
                            val = adjacent;
                            break;
                            
                        case 2:
                            val = corner;
                            break;
                    }
                    break;
            }
            row.push_back(val);
        }
        window.push_back(row);
    }
    return window;
}

优化后

#include "blur_factor_improved.hpp"

using namespace std;

vector < vector <float> > blur_factor_improved() {
  	
  	static float blurring = .12;
    static float center = 1.0 - blurring;
    static float corner = blurring / 12.0;
    static float adjacent = blurring / 6.0;
  
  
    // 2D vector reprenting the blur filter
    vector < vector <float> > window={
      {corner,adjacent,corner},
      {adjacent,center,adjacent},
      {corner,adjacent,corner}
    };
    return window;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值