为何GCC在-O3优化(x86)下每次循环都mov数组首地址访问元素?
为何GCC -O3优化后循环仍重复加载全局数组首地址?
问题背景
我为研究TLB访问/缺失统计设计了测试样本:向数组的每第4096个元素写入1,数组大小为10000*4096字节。启用-O3优化后,我预期只会产生10000次TLB存储操作,但实际生成的汇编代码中,每次循环都会加载数组首地址,额外带来10000次TLB加载。
查看汇编发现循环流程为:
- 将数组首地址移入寄存器
- 向偏移后的数组地址写入1
- 增加索引
- 跳回步骤1
数组首地址并未改变,为何步骤1要在每次循环都执行?我期望首地址仅加载一次,循环跳回写入步骤即可。
测试代码
#include <stdlib.h> #define REPEAT 10000 #define PAGESIZE 4096 #define PAGES 10000 char *data = NULL; inline void test_function() { for (int i = 0; (i < PAGES * PAGESIZE); i += (PAGESIZE)) { data[i] = 1; } } int main() { data = (char *) malloc(PAGES * PAGESIZE); for (int idx = 0; idx < REPEAT; idx++) test_function(); free(data); return 0; }
GCC生成的汇编
1090: mov rdx,QWORD PTR [rip+0x2f81] # 4018 <data> 1097: mov BYTE PTR [rdx+rax*1],0x1 109b: add rax,0x1000 10a1: cmp rax,0x2710000 10a7: jne 1090 <main+0x30>
perf统计结果
当重复执行100000次后,每次函数调用会产生:
- 10K次L1缓存加载、10K次L1缓存存储
- 10K次TLB加载、10K次TLB存储
- TLB加载缺失几乎为0,TLB存储缺失10K次
数组首地址的加载始终命中TLB缓存,但仍会被重复访问。
平台信息
- CPU:Intel(R) Core(TM) i7-10610U
- 系统:Ubuntu 22.04.3 LTS
- 编译器:g++ 11.4.0
- 编译命令:
g++ ./tlb.cpp -O3 -g -o gcc.out
原因分析
全局变量的内存可见性约束
data是全局变量,C++内存模型要求编译器必须保证每次访问全局变量时都读取其最新值——即使你的代码里没有显式修改它,编译器也无法排除其他线程、信号处理函数等外部因素修改它的可能。因此无法将data的值持久保存在寄存器中跨循环复用。寄存器分配与优化限制
虽然理论上寄存器可以缓存data的地址,但受限于全局变量的可见性规则,编译器不敢做这种优化。即使寄存器资源充足,内存可见性的优先级也高于寄存器复用优化。
解决方法
方法1:将全局变量改为局部参数传递
把data作为参数传入test_function,编译器能确定它在函数执行期间不会被外部修改,从而优化掉重复读取:
#include <stdlib.h> #define REPEAT 10000 #define PAGESIZE 4096 #define PAGES 10000 inline void test_function(char *data) { for (int i = 0; i < PAGES * PAGESIZE; i += PAGESIZE) { data[i] = 1; } } int main() { char *data = (char *) malloc(PAGES * PAGESIZE); for (int idx = 0; idx < REPEAT; idx++) test_function(data); free(data); return 0; }
方法2:使用__restrict__关键字
如果必须保留全局变量,用__restrict__告诉编译器该指针不会被其他指针别名引用,且不会被外部修改:
char *__restrict__ data = NULL;
这会让编译器认为data的值在循环期间不会改变,从而将其缓存到寄存器中。
内容的提问来源于stack exchange,提问作者Rostislav Povelikin
相关产品推荐
相关产品推荐

