程序优化不仅包括编程语言,还涉及其他方面。程序运行速度还取决于硬件、编译器以及所选的计算机算法。
无用代码
在编写一个解决方案时,你沿着一个逻辑分支写下去,发现最终行不通。或者你出于调试的目的添加了额外代码,但这段代码并不是解决方案的一部分。无用代码虽然是程序的一部分,但不影响程序实现。它可能拖慢程序。这些代码虽然不会使用,但仍然会在 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;
}