循环变量顺序与缓存缺失:交换循环顺序能否提升性能?
这个说法是错误的——恰恰相反,交换x和y的循环顺序反而会让性能下降,核心原因确实和你怀疑的缓存缺失脱不了干系,我给你讲明白这里面的门道:
首先得搞懂二维数组在内存里是怎么存的:在C/C++这类语言里,二维数组是行优先存储的,说白了就是先把第一行的所有元素挨个儿连续存在内存里,存完第一行再存第二行,以此类推。拿你的level[x][y]举例,内存里的排列顺序是:level[0][0] → level[0][1] → ... → level[0][31] → level[1][0] → level[1][1] → ... → level[31][31]
咱们先看教授默认教的写法:
for (int x = 0; x < 32; x++) { for (int y = 0; y < 32; y++) { level[x][y].update(); } }
这是按行遍历:外层固定行号x,内层把这一行的所有列y挨个遍历。每次访问的level[x][y]在内存里是连续挨着的,CPU的缓存有个聪明的特性——它会自动把当前访问地址附近的内存块预加载到缓存里(因为程序通常有“局部性”,刚访问了这个地址,接下来很大概率会访问附近的)。所以当你访问level[x][0]时,缓存已经把这一行的其他元素都加载进来了,后面访问level[x][1]、level[x][2]...都是直接从高速缓存读,速度快得飞起,缓存命中率极高,根本不用频繁去慢得要死的主内存取数据,性能自然好。
再看交换顺序后的写法:
for (int y = 0; y < 32; y++) { for (int x = 0; x < 32; x++) { level[x][y].update(); } }
这变成了按列遍历:外层固定列号y,内层遍历所有行x。这时候每次访问的level[x][y]在内存里是跳着来的——比如从level[0][y]到level[1][y],中间隔了整整31个元素(因为一行有32个元素)。缓存预加载的那些内存块里根本没有下一个要访问的元素,每次都得重新从主内存加载数据,缓存缺失率直接拉满。而主内存的速度比缓存慢好几个数量级,这种频繁的缓存缺失会让性能大打折扣。
当然也有例外:如果你的数组特别小(比如32x32,整个数组可能才几KB),缓存能一次性装下整个数组,这时候两种写法的性能差异可能不太明显。但如果数组规模变大(比如1024x1024),这种性能差距会非常显著。
内容的提问来源于stack exchange,提问作者Markall

