在很多编程的书籍中会给出这样的建议:
在多重循环中,如果有可能,应当将最长的循环放在最内层,最短的循环放在最外层,以减少CPU 跨切循环层的次数。
这个“跨循环层”的概念本身是说,由外层循环进入内层循环是要重新初始化循环计数器的,包括保存外层循环的计数器和加载内层循环计数器,退出内层的时候再恢复外层循环计数器。把长循环放在里面可以显著减小这些操作的数量。看下面两个循环结构:
>>> 结构1
for(i=0; i < 100; i++)
for(j = 0; j < 20; j++)
sum += a[i][j];
>>> 结构2
for(j=0; j < 20; j++)
for(i=0; i < 100; i++)
sum+= a[i][j];
对于结构1来说,当CPU执行到内层循环的时候会执行sum += a[i][j]20次之后跳到外层循环去判断一次,一共需要跳出去100次;对于结构2来说,当CPU执行到内层循环的时候会执行sum += a[i][j]100次才跳到外层,一共需要跳出去20次。很明显对于结构1来说跳出的次数明显大于结构2需要跳出的次数。这就是对于上面这个建议的理解。
但是,这种情况只是在这个二维数组比较小的时候是有效的。另一方面还要注意数据结构本身的效率。这里涉及到数据的Cache命中的概念。如果你的“跳读”会跨越cache交换块,甚至page边界的话,就会造成CPU数据cache重新批量装载数据,甚至从虚拟内存中恢复磁盘数据,这当然严重影响效率。
数组分配之后在内存中是线性存放的,是一整块连续的空间,如果数组的规模大到一定的程度,这个时候操作系统的cache在这个巨大的数组面前显得比较微小的时候就应该从另一个角度去考虑调高效率,计算机中的cache是用来解决主存(也就是通常所说的内存)和CPU之间速率差异的机制,一般为SRAM,速度比主存快,造价比较高。一般情况下操作系统都有一些调度算法去预测CPU下一次所要使用的数据,从而提前把主存中的一部分数据取到cache中,当下次CPU要取的数据就在cache中的话就直接从cache中得到,提高系统效率,具体的cache置换算法请参考网络。所以再看下面两个例子:
>>> 结构3
for(i=0; i < 1000; i++)
for(j=0; j < 500; j++)
for(k=0; k < 100; k++)
sum += a[i][j][k];
>>> 结构4
for(k=0; k < 100; k++)
for(j=0; j < 500; j++)
for(i=0; i < 1000; i++)
sum += a[i][j][k];
从切换层数上计算的话,结构4好像要比结构3好很多,但是如果考虑上CPU置换cache所消耗的时间的话结构3要好于结构4,因为按照结构3的访问顺序,i是最不常变化的,j次之,k是最容易变化的,然而k和k+1在实际的内存中就是挨着的这大大增加了cache命中的机会;然而结构4中,i是最常变化的,而且i变化之后,i+1和i(k和j保持不变)的单元相差了500*100*sizeof(数据类型)个字节,这使得cache命中率下降,因此需要做很多cache的置换工作,这部分时间如果加上的话,结构3比结构4就要好很多。