You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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行为共同导致的,具体可以拆成三个部分说明:

  1. 全局/静态数组为什么快
    全局零初始化数组会被编译器放到PE文件的.bss段,程序加载时,系统不会为.bss段的所有虚拟页分配独立物理内存,而是通过写时复制机制,把所有虚拟页都映射到系统全局共享的同一个4KB零物理页——这个页是全内存常驻的,会提前加载到CPU缓存中。
    测试代码中values和error两个数组全程只读、从未写入,因此永远不会触发写时复制的私有页分配:遍历这两个数组时,不管访问的是哪个虚拟地址,最终都命中同一个4KB的共享零页,地址转换缓存(TLB)只需要1个条目就能覆盖所有访问,完全没有缺页异常、TLB失效、缓存失效开销,自然运行速度极快。

  2. 未初始化局部数组为什么慢
    按照C/C++标准,未初始化的局部自动变量值是未定义的,编译器不会为其生成初始化代码,也不会提前触达对应的栈内存:

    • Windows栈空间是按需提交的,一次性分配240KB栈空间后,这些虚拟地址对应的页表项一开始是无效的,也没有提前加载到TLB
    • 程序进入三层循环后第一次访问数组时,会跨多个页触发缺页异常,内核需要逐页做栈合法性检查、分配物理页、填充页表,这个过程需要频繁陷入内核,直接打断CPU流水线
    • 缺页加载的物理页是冷页,没有提前进入CPU缓存,首次遍历数组时会产生大量缓存失效,开销被10亿次量级的内层循环直接放大,最终耗时达到15秒级别。
      所谓“局部变量访问比全局变量快”的常识,仅适用于小尺寸、已经完成物理内存提交、常驻缓存的局部变量,完全不适用这个冷内存首次访问的场景。
  3. 显式初始化/开编译优化后为什么速度恢复正常

    • 手动显式初始化局部数组时,编译器会生成顺序写内存的指令,在进入循环前就把三个数组的所有内存从头到尾写一遍:这个过程是线性顺序访问,会一次性完成所有栈页的提交、页表填充、TLB加载,配合硬件预取机制把所有数组内存拉到CPU L2缓存中(三个数组总大小仅240KB,完全可以被i3-7100的256KB单核心L2缓存装下),进入循环后所有内存访问都命中缓存,自然和全局数组速度一致。
    • 开启-O2及以上编译优化时,编译器会自动识别大尺寸栈数组分配,自动插入栈预触达代码,在进入循环前完成所有栈页的提交和缓存预热,同时会做循环展开、内存访问重排优化,进一步提升缓存和预取效率,不需要手动初始化也能达到最优性能。

内容的提问来源于stack exchange,提问作者Moises Cavcalcanti

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 12:54:34