C++中全局数组处理速度为何快于main函数内局部数组?
问题现象
我编写了一段执行简单运算的C++测试代码,用于验证各类缓存优化手段、研究代码性能提升规律,测试过程中发现了反常性能差异:
- 将测试数组声明为
static静态类型、或定义在main函数外部作为全局变量时,代码平均运行耗时仅0.5秒 - 若仅将数组移动到
main函数内部作为局部自动变量声明、且不做显式初始化,相同处理逻辑的平均运行耗时高达15秒
公开资料普遍提及局部变量访问速度快于全局变量,无法解释该量级的性能差异。
测试环境
- 操作系统:Windows
- 编译器:MinGW套件g++,默认编译命令为
g++ -o,未添加任何优化参数 - 硬件:搭载i3-7100处理器的台式机
补充说明
- 本次测试的核心目标并非做业务层面的性能优化,而是通过移动、移除、合并数组等操作研究CPU缓存的工作机制。开启编译优化选项后,代码可达到最优运行速度,但需要明确优化选项具体修正了什么问题、数组定义位置带来的性能缺陷是如何被优化选项修复的。
- 按照建议对局部数组做显式初始化后,测试确认未初始化的局部数组确实运行速度极慢,显式初始化后的局部数组运行速度与全局数组完全一致,需要了解该现象的底层实现原理。
- 对照测试使用的代码如下:
#include <chrono> #include <iostream> #define TAM 10 #define N 10000 #ifdef USE_GLOBAL volatile double output[N] = {}, values[N] = {}, error[N] = {}; #endif int main() { #ifndef USE_GLOBAL volatile double output[N] = {}, values[N] = {}, error[N] = {}; #endif std::cout << "Starting" << std::endl; auto t1 = std::chrono::high_resolution_clock::now(); { for (int total = 0; total < TAM; total++) { for (int i = 0; i < N; i++) { for (int j = 0; j < N; j++) { output[i] += (values[j] + error[j]) / i + 1; } } } } auto t2 = std::chrono::high_resolution_clock::now(); auto duration = (std::chrono::duration_cast<std::chrono::microseconds>(t2 - t1) .count()); float time = (float)duration / 1000000; std::cout << "Processing time = " << time << " seconds." << std::endl; }
原理解释
这个性能差异和“局部变量/全局变量的寻址速度”没有关系,核心是Windows虚拟内存机制、编译时内存初始化策略、CPU缓存/TLB行为共同导致的,具体可以拆成三个部分说明:
全局/静态数组为什么快
全局零初始化数组会被编译器放到PE文件的.bss段,程序加载时,系统不会为.bss段的所有虚拟页分配独立物理内存,而是通过写时复制机制,把所有虚拟页都映射到系统全局共享的同一个4KB零物理页——这个页是全内存常驻的,会提前加载到CPU缓存中。
测试代码中values和error两个数组全程只读、从未写入,因此永远不会触发写时复制的私有页分配:遍历这两个数组时,不管访问的是哪个虚拟地址,最终都命中同一个4KB的共享零页,地址转换缓存(TLB)只需要1个条目就能覆盖所有访问,完全没有缺页异常、TLB失效、缓存失效开销,自然运行速度极快。未初始化局部数组为什么慢
按照C/C++标准,未初始化的局部自动变量值是未定义的,编译器不会为其生成初始化代码,也不会提前触达对应的栈内存:- Windows栈空间是按需提交的,一次性分配240KB栈空间后,这些虚拟地址对应的页表项一开始是无效的,也没有提前加载到TLB
- 程序进入三层循环后第一次访问数组时,会跨多个页触发缺页异常,内核需要逐页做栈合法性检查、分配物理页、填充页表,这个过程需要频繁陷入内核,直接打断CPU流水线
- 缺页加载的物理页是冷页,没有提前进入CPU缓存,首次遍历数组时会产生大量缓存失效,开销被10亿次量级的内层循环直接放大,最终耗时达到15秒级别。
所谓“局部变量访问比全局变量快”的常识,仅适用于小尺寸、已经完成物理内存提交、常驻缓存的局部变量,完全不适用这个冷内存首次访问的场景。
显式初始化/开编译优化后为什么速度恢复正常
- 手动显式初始化局部数组时,编译器会生成顺序写内存的指令,在进入循环前就把三个数组的所有内存从头到尾写一遍:这个过程是线性顺序访问,会一次性完成所有栈页的提交、页表填充、TLB加载,配合硬件预取机制把所有数组内存拉到CPU L2缓存中(三个数组总大小仅240KB,完全可以被i3-7100的256KB单核心L2缓存装下),进入循环后所有内存访问都命中缓存,自然和全局数组速度一致。
- 开启-O2及以上编译优化时,编译器会自动识别大尺寸栈数组分配,自动插入栈预触达代码,在进入循环前完成所有栈页的提交和缓存预热,同时会做循环展开、内存访问重排优化,进一步提升缓存和预取效率,不需要手动初始化也能达到最优性能。
内容的提问来源于stack exchange,提问作者Moises Cavcalcanti
相关产品推荐
相关产品推荐

