网站有备案号,引流推广犯法吗,光之翼可以做网站吗,工程公司logo图标设计本文主要针对C的性能优化方法展开讨论。虽然这些方法也适用于一些其他语言#xff0c;但由于C经常用于底层操作#xff0c;提供了更多的优化空间#xff1b;相比之下#xff0c;诸如Python、Kotlin等高级语言由于其抽象程度更高#xff0c;优化空间较少。
性能优化原理
…本文主要针对C的性能优化方法展开讨论。虽然这些方法也适用于一些其他语言但由于C经常用于底层操作提供了更多的优化空间相比之下诸如Python、Kotlin等高级语言由于其抽象程度更高优化空间较少。
性能优化原理
要实现性能优化需要从硬件和软件层面了解优化的实现原理尤其是围绕运算和存储两个方面。
CPU三级缓存机制
首先来看CPU的三级缓存机制这是了解性能优化的关键概念。下图展示了CPU三级缓存的结构 从图中可以看出越靠近CPU的内存存储速度越快但空间越小。这涉及到一个关键概念——缓存命中率。CPU对内存的调用逻辑是首先查看L1数据高速缓存是否命中数据如果没有再查看L2高速缓存最后是L3级调用此时命中率最低。因此如何提高CPU的缓存命中率非常重要。
提高缓存命中率
为了提高缓存命中率可以考虑以下几种方法
数据局部性尽量让数据使用集中在一小块内存区域内。批量处理一次处理尽可能多的数据以减少缓存的更新频率。避免跨行访问尽量避免数据结构导致跨行访问。
CPU对于不同运算的运算速度
不同的运算在CPU上的速度是不同的
逻辑运算如AND, OR, NOT, XOR这些运算由专用电路实现速度最快。比较运算使用专门的数字比较器速度较快。赋值运算由于处理器频繁调用也被高度优化。算术运算 加、减-、移位操作, 速度较快接近逻辑运算速度。乘法*相对较慢。除法/最慢。 浮点运算速度比整数运算慢得多。
CPU流水线
在大多数CPU中命令的执行依赖于CPU流水线Pipeline。下图展示了一个4级流水线缓存行Cache Line一般是L1一级缓存上存放等待执行的指令 Clock 1取出绿色指令放入流水线。Clock 2取出紫色指令解码绿色指令。Clock 3取出蓝色指令解码紫色指令执行绿色指令。Clock 4取出红色指令解码蓝色指令执行紫色指令回写绿色指令。Clock 5流水线空出一级继续执行类似的流程。
CPU会尝试预测接下来的操作并将其置入流水线中。如果操作非线性且包含频繁的逻辑判断可能会中断流水线使所有操作失效从而降低处理速度。
利用GPU和NPU进行加速
GPU图形处理单元擅长处理大量简单的并行任务适合图像处理等操作。
NPU神经网络处理器则是为加速深度学习算法而构建的可以在AI工作负载中提供比CPU和GPU更高的性能。
代码处优化
优化代码的方法包括
减少循环开销精简循环体减少不必要的操作。减少函数调用使用内联函数inline减少函数调用开销。避免不必要的内存分配尽量减少动态内存分配使用栈内存或预分配内存。
编译时优化
修改编译时优化级别也能提升性能
-O0调试版本生成的汇编代码与实际代码更好对应附加调试信息。-O2发布版本启用内联、循环展开、指令顺序优化、常量替换等技术优化代码性能。-O3激进优化过度循环展开、内联等可能导致可执行文件变大甚至栈溢出。
对于ARM内核使用armcc编译器通常比gcc有更好的性能优化。
性能优化方法
基于上述性能优化原理可以采用以下优化方法
1. 结合代码处性能优化和CPU三级缓存中提高命中率的方法
以矩阵计算为例常见的边界问题会用到判断如if但if判断是CPU流水线的敌人会中断CPU的分支预测拖慢处理速度。以下是一个优化示例
原始代码
#include stdio.h#define ROWS 10
#define COLS 10void matrix_calc(int matrix[ROWS][COLS]) {for (int i 0; i COLS; i) {for (int j 0; j ROWS; j) {if (i 0 || i ROWS - 1 || j 0 || j COLS - 1) {matrix[i][j]--;} else {matrix[i][j];}}}
}优化后代码
#include stdio.h
#include immintrin.h#define ROWS 16
#define COLS 16void matrix_calc_simd(int matrix[ROWS][COLS]) {__m128i mask _mm_set1_epi32(0x0000FFFF); // 用于判断是否为边界__m128i inc _mm_set1_epi32(1);__m128i dec _mm_set1_epi32(-1);for (int i 0; i ROWS; i 4) {for (int j 0; j COLS; j 4) {__m128i data _mm_loadu_si128((__m128i*)matrix[i][j]);__m128i row_mask _mm_set1_epi32(i 0 || i ROWS - 1 ? 0xFFFFFFFF : 0);__m128i col_mask _mm_set1_epi32(j 0 || j COLS - 1 ? 0xFFFFFFFF : 0);__m128i boundary_mask _mm_or_si128(row_mask, col_mask);__m128i result _mm_blendv_epi8(data, _mm_add_epi32(data, inc), boundary_mask);result _mm_blendv_epi8(result, _mm_sub_epi32(data, dec), boundary_mask);_mm_storeu_si128((__m128i*)matrix[i][j], result);}}
}在优化后的代码中利用了SIMD指令集一次处理多个数据提高了计算效率。SIMD指令集单指令多数据是性能优化的一个好办法。更多信息可以参考相关文章。
如果去掉SIMD操作后代码如下
#include stdio.h#define ROWS 10
#define COLS 10void matrix_calc(int matrix[ROWS][COLS]) {for (int i 0; i ROWS; i) {matrix[i][0]--;matrix[i][COLS-1]--;matrix[0][i]--;matrix[ROWS-1][i]--;for (int j 1; j COLS - 1; j 2) {matrix[i][j];matrix[i][j1];}}
}该代码的优化在于
首先将边界值的计算放在循环外面处理这样就不必在每次迭代中检查是否在边界上减少了判断次数。然后对于非边界上的元素采用批量增加的方式每次处理两个数据加快了计算速度。
2. 滑动窗口
有聪明的小伙伴就发现了这个词好像在我们计算机网络这门课听说过但是听的迷迷糊糊的我们可以理解为一个一直移动的框举个例子我们如果要计算一个很长的无序数组的某n个数和比如说一个数组a内容为{1,7,3,2,4,5,8,0,2,9,1,3,5,7,8,1......} 而我们需要计算每5个数的和。 正常而言我们需要计算第一个数就是17324第二个数就是73245第三个数就是32458…大家发现了什么其实每次计算哦独有一部分是重复的这一部分就是滑动窗口。 于是乎我们可以先做一次计算比如第一个数是17324 17那么第二个数是多少呢就是第一个数-a[0]a[5]也就是17-15 21第三个数是第二个数-a[1]a[6]22我们会惊奇的发现通过这种方法我们省下了3个运算 给个例子比如在计算无重复字符的最长子串问题中原代码如下 问题描述 给定一个字符串 s 请你找出其中不含有重复字符的 最长子串 的长度。
#include iostream
#include unordered_set
#include stringusing namespace std;int lengthOfLongestSubstring(string s) {int n s.size();unordered_setchar set;int ans 0, i 0, j 0;while (j n) {if (set.count(s[j]) 0) {set.insert(s[j]);ans max(ans, j - i);} else {set.erase(s[i]);}}return ans;
}int main() {string s abcabcbb;cout lengthOfLongestSubstring(s) endl; // 输出 3return 0;
}
我们完全可以利用滑动窗口优化每次滑动窗口就是最长子串
#include iostream
#include unordered_map
#include vector
#include stringusing namespace std;vectorint findAnagrams(string s, string p) {vectorint res;if (s.size() p.size()) return res;unordered_mapchar, int need, window;for (char c : p) need[c];int left 0, right 0, valid 0;while (right s.size()) {char c s[right];right;// 窗口内字符数量变化if (need.count(c)) {window[c];if (window[c] need[c]) valid;}// 判断窗口是否有效while (right - left p.size()) {if (valid need.size()) res.push_back(left);// 左指针向右移动char d s[left];left;if (need.count(d)) {if (window[d] need[d]) valid--;window[d]--;}}}return res;
}
3. 映射表
这也是一种常见的优化手段多用于图像处理比如我想让RGB图像的绿色值都加1封顶是绿色为256 传统方法下操作如下
void adjustBrightness(uint8_t* imageData, int width, int height, int brightness) {for (int y 0; y height; y) {for (int x 0; x width; x) {for (int c 0; c 3; c) { // RGB 三个通道int pixel imageData[(y * width x) * 3 c];pixel std::min(std::max(pixel brightness, 0), 255);imageData[(y * width x) * 3 c] pixel;}}}
}
可以看到再循环中每次循环都进行了比较和运算操作虽然单次的运算操作速度很快但是架不住数量多啊 这时候我们可以将已知值进行与计算计算256次
vectoruint8_t createGreenLUT() {vectoruint8_t lut(256);for (int i 0; i 256; i) {lut[i] min(i 1, 255);}return lut;
}// 假设 imageData 是一个 uint8_t 的数组存储了图像的像素数据
void applyGreenLUT(uint8_t* imageData, int width, int height, vectoruint8_t lut) {for (int y 0; y height; y) {for (int x 0; x width; x) {int index (y * width x) * 3 1; // 假设 RGB 格式绿色通道在第二个位置imageData[index] lut[imageData[index]];}}
}此时我们就大大降低了计算数量
4. 量化数据
前面提到浮点数运算会大大拖慢运算速度因此我们可以考虑对浮点数据进行量化将其量化为更小的数据类型。 比如我们已知浮点数的范围为0.1-0.8那么我们就可以将其量化为8位无符号整数取值范围为0-255 代码如下
#include iostreamint quantize(float value) {// 假设浮点数范围为0.1-0.8量化为8位无符号整数float min_val 0.1;float max_val 0.8;int quant_level 255;int quantized_value round((value - min_val) / (max_val - min_val) * quant_level);return quantized_value;
}int main() {float f 0.5;int q quantize(f);std::cout Quantized value: q std::endl;return 0;
}
此时对循环读取等操作都变得更加友好了这种方法被广泛用于诸如ai模型优化等方面可以将一个浮点数40mb的ai模型给优化成10mb但是在使用时需要将其反量化为40mb的大小这就涉及一个点——精度损失。
float dequantize(int quantized_value, float min_val, float max_val, int quant_level) {// 反量化公式// dequantized_value quantized_value * (max_val - min_val) / quant_level min_valreturn quantized_value * (max_val - min_val) / quant_level min_val;
}我们将其中的数字进行反量化后发现原本的0.1-0.8经过这一过程后变为了
Original: 0.1, Quantized: 0, Dequantized: 0.1, Error: 0
Original: 0.2, Quantized: 36, Dequantized: 0.198824, Error: 0.00117648
Original: 0.3, Quantized: 73, Dequantized: 0.300392, Error: 0.000392139
Original: 0.4, Quantized: 109, Dequantized: 0.399216, Error: 0.000784338
Original: 0.5, Quantized: 146, Dequantized: 0.500784, Error: 0.000784338
Original: 0.6, Quantized: 182, Dequantized: 0.599608, Error: 0.000392139
Original: 0.7, Quantized: 219, Dequantized: 0.701177, Error: 0.00117648可以看到或多或少都出现了误差但是误差范围都在小数点后三位以后对精度要求不是很高的情况下可以使用该方法。
5. 其他优化策略
可以多用内敛函数inline减少函数调用多用引用少用指针提高内存利用率在做运算时用乘法移位代替除法用加法移位代替乘法等等
6. 最后的手段多线程
现代计算机的处理器大都支持多线程操作可以在同一时间处理多个任务这也能大大提高处理效率。
7. 硬件层面的善用GPU和NPU加速
利用OpenGL等硬件加速手段来辅助加快运行速度
8. 编译器编译时参数优化
优化选项
-O1, -O2, -O3
GCC编译器的-O1, -O2, -O3优化选项可以自动进行一些优化操作例如内联函数、循环展开、指令调度等。你可以根据实际需求选择合适的优化级别。一般来说-O3优化选项提供最高级别的优化但同时可能增加代码的编译时间和执行文件的体积。
-flto
链接时优化Link Time Optimization, -flto可以在链接阶段进一步优化代码特别是当多个源文件链接在一起时。这个选项可以在编译时使用-flto参数然后在链接时也需要使用这个选项。例如
g -O3 -flto -o myprogram main.cpp module1.cpp module2.cpp-marchnative
-marchnative选项会让编译器针对当前机器的硬件架构生成优化的代码。这个选项可以启用处理器支持的所有优化指令例如SIMD指令集。使用示例
g -O3 -marchnative -o myprogram main.cpp-ffast-math
-ffast-math选项启用对浮点运算的激进优化。这个选项会让编译器忽略一些浮点运算的标准规则以提高代码执行速度。但需要注意使用这个选项可能导致结果精度下降或不符合IEEE 754标准。
结论
性能优化是一个复杂的过程需要综合考虑硬件架构、软件算法和编译器优化。不同的应用场景需要不同的优化策略。通过结合数据局部性、批量处理、编译器优化选项以及使用SIMD指令等方法可以大大提高程序的运行效率。这次只是浅探一下更深入的内容还需要再进行探索。