存储器层次结构
存储技术
计算机技术的成功很大程度来源于存储技术的巨大进步。早期的电脑甚至没有磁盘。现在电脑上的磁盘都已经按T算了。
随机访问存储器(Random-Access Memory, RAM)
随机访问存储器(Random-Access Memory, RAM)分两类:
- 静态的:SRAM,高速缓存存储器,既可以在CPU,也可以在片下。
- 动态的:DRAM,用于主存或者图形系统帧缓冲区。
通常情况下,SRAM的容量都不会太大,而相比之下DRAM容量可以大得离谱。
静态RAM
SRAM将每个位存储在一个双稳态存储器单元里,每个单元用一个六晶体管电路实现。
这种电路有一个属性,它可以无限期地保持两个不同的状态的其中一个,其他状态都是不稳定的。
如上图,它能稳定在左态和右态,如果处于不稳定状态,它就像钟摆一样立刻变成两种稳态的其中一种。
也因为它的双稳态特性,即使有干扰,等到干扰消除,电路就能恢复成稳定值。
动态RAM
DRAM的每个存储是一个电容和访问晶体管组成,每次存储相当于对电容充电。
该电容很小,大约只有30毫微微法拉。
因为每个存储单元比较简单,DRAM可以造的非常密集。但它对干扰非常敏感,被干扰后不会恢复。
因此它必须周期性地读出重写来刷新内存的每一位。或者使用纠错码来纠正任何单个错误。
两者总结
传统的DRAM
DRAM芯片内的每一个单元被叫做超单元。
在芯片内,总共有(d) 个超单元,它们被排列成一个(r imes c) 大小的矩阵,也就是说(d = r imes c),每个超单元都可以用类似((i, j)) 之类的地址定位
而每个超单元则是由(w) 个DRAM单元组成。因此一个DRAM芯片可以存储(dw) 位的信息。
上图是一个(16 imes 8) 的DRAM芯片的组织。
首先由两个addr
引脚依次传入行地址i
和列地址j
。每个引脚携带一个信号。由于这是(4 imes 4) 的矩阵,因此两个就够了。
然后定位到((i, j)) ,将该地址的超单元信息传出去。
信息是由引脚data
传出去的。由于一个超单元里有8个DRAM单元,因此使用了8个引脚
每个DRAM芯片被连接到一个内存控制器。该控制器可以读入或者读出(w) 位的数据。
整个读出过程是这样的:
- 内存控制器发送行地址(i) 到DRAM
- 内存控制器发送行地址(j) 到DRAM
- DRAM发送((i, j)) 的内容作为响应
其中行地址被称为(RAS(Row space Access space Strobe space 行访问选通脉冲)) ,列地址被称为(CAS(Column space Access space Strobe space 行访问选通脉冲)) 。
两个地址是共用一个addr
引脚的。
举个实际例子:
首先,内存控制器发送行地址2
,DRAM做出的响应则是将一行的内容都复制到内部行缓冲区。
其次,内存控制器发动列地址1,DRAM做出的响应则是赋值行缓冲区的1列中的8位,然后发送到内存控制器。
将DRAM设计成矩阵的一个原因是降低芯片地址上的引脚数量,而缺点是必须分两步分发地址,增加访问时间。
内存模块
DRAM芯片封装在内存模块中,插到主板的拓展槽上。
看下图:
整个读取过程是这样的:
- 首先,内存控制器将一个内存地址翻译成一个超单元地址((i, j))
- 内存控制器将地址广播到每一个DRAM芯片上。
- 每一块DRAM芯片作出响应,传出一个8位的字作为1个字节.
- 电路收集这些信息,将其合并成64位的字,再将信息返回给内存控制器
增强的DRAM
实际上就是为了迎合需求,对普通的DRAM进行特定的优化以满足需求。
- 快页模式(FPM DRAM):与传统的DRAM不同的地方在于,它可以一个RAS之后接过多个CAS,即可以连续的读取同行的数据,不需要重复发送RAS
- 扩展数据输出DRAM(EDO DRAM):实际上就是FPM DRAM的增强形式,他让CAS信号可以发送地更紧密一些。
- 同步DRAM(SDRAM):传统的DRAM是异步传输地址的。而这个利用一些技术达成了同步传输地址,它能比传统的异步存储器更快地输出单元信息。
- 双倍数据速率同步DRAM(DDR SDRAM):对SDRAM的一种增强,能使DRAM速度翻倍。
- 视频RAM(VRAM):它用于图形系统的帧缓冲区中。
非易失性存储器
如果断电,DRAM和SRAM会丢失它们的信息,他们属于易失性存储器。
因此,非易失性存储器就是关电之后仍然保存它们的信息。他们统称只读存储器(Read-only space Memory space,ROM) 。
它们是以能够被重编程的次数和重编程的机制来区分的。
- PROM((space Programmable space ROM space)) 可编程ROM:只能被编程一次,PROM的每个存储器单元有一种熔丝,只能用高电流熔断一次。
- 可擦写可编程ROM((Erasable space Programmable space ROM, space EPROM)) :可反复擦写编程1000次以上。
- 电子可擦除PROM((Electrically space Erasable space PROM, space EEPROM)) :可擦写编程的数量级为(10^5) 次。
- 闪存((flash space memory)) :非易失性存储器,基于EEPROM。
访问主存
数据流在处理器和DRAM主存之间的来来回回是通过总线(bus)的共享电子电路实现的。
数据传送的步骤被称为总线事务,读事务从主存传送数据到CPU,写事务从CPU传送数据到主存。
-
总线是一行并行的导线,能携带地址,数据和控制信号。
-
数据和地址信号能否共享同一组导线取决于总线的设计。且两台以上的设备也能共享总线。
-
总线上的控制信号会同步事务,且能标识出事务类型。
考虑如下操作时会发生什么:
movq A, %rax
-
CPU将地址A放到系统总线上,I/O桥把信号传到内存总线
-
主存发现了内存总线上的地址,从内存总线中读取地址,再从DRAM中将数据x放到内存总线上
-
I/O桥把内存总线上的信号翻译成系统总线信号,再放上系统总线传递。最后CPU发现了数据的传递,从总线上读取数据,并将数据复制到寄存器
%rax
。
磁盘存储
磁盘是被大量使用来存储信息的设备。
磁盘构造
磁盘由盘片构成,每个盘片有两面,都称为表面,表面覆盖着磁性记录材料。
盘片中央有一个可以旋转的主轴,它能使盘片以固定的旋转速率旋转,通常是5400~15000转每分钟。
通常磁盘包含一个或者多个这样的盘片,并封装在一个密封的容器内。
上图展示了一个典型的磁盘表面的结构。
每个表面是由一组称为磁道的同心圆组成的。每个磁道被划分成为一组扇区。每个扇区会有相等数量的数据位。
扇区之间由一些间隙隔开,间隙间不存储数据位,间隙存储用来标识扇区的格式化位。
磁盘由一个或者多个叠放在一起的盘片组成的,它们被封装在一个密封的包装里,简称磁盘,或旋转磁盘,使之区别于固态硬盘。
柱面用来描述盘片驱动器的构造,用来表示所有表面到上离主轴距离相同的磁道集合。
磁盘容量
磁盘的容量是磁盘上可以记录的最大位数。
磁盘容量由以下技术因素决定:
- 记录密度:磁道一英寸的段可以放入的位数
- 磁道密度:从盘片中心出发半径上一英寸内可以有的磁道数
- 面密度:记录密度和磁道密度的乘积
磁盘密度容量:
磁盘操作
RPM ————–> 硬盘转速
磁盘用读写头来读写在磁性表面的位,而读写头连接到一个传动臂一端。
通过移动传动臂,读写头可以移动到任意一个磁道上,这样的机械运动叫寻道(seek)。
一旦移动到合适的位置,读写头可以读出这个位的值,或者修改这个位的值。
读写头垂直排列,一旦读写头移动,一致行动。所有的读写头都位于同一柱面上。
磁盘总是密封包装的。读写头仅仅在表面约0.1微米处以80km/h的速度飞翔。在这极小的空隙中,一粒灰尘都是一块巨石。因此磁盘总是密封包装的。
对扇区的访问时间有三个主要部分:寻道时间,旋转时间和传送时间
-
寻道时间:指将读写头移动到对应的磁道上。最坏的寻道时间在(T_{max space seek} = 20ms) ,平均寻道时间为(T_{avg space seek} = 3 -- 9ms)
-
旋转时间:当读写头移动到对应的磁道上后,驱动器等待目标扇区第一个位旋转到读写头下。最坏情况是读写头刚刚错过第一个位:(T_{max space rotation} = frac{1}{RPM} imes frac{60s}{1min}) ,平均情况是最坏情况的一半。
-
传送时间:驱动器开始读和写。一个扇区的传送时间依赖于旋转速度和每条磁道的扇区数目。平均传送时间:(T_{avg space transfer} = frac{1}{RPM} imes frac{1}{(平均扇区数/磁道)} imes frac{60s}{1min})
-
整个读写的过程中,访问字节几乎不需要花费时间,但是寻道与旋转时间花费了大量的时间。
-
寻道时间与旋转时间大致相等,寻道时间乘2就是估计磁盘访问时间简单而合理的方法。
逻辑磁盘块
为了对操作系统隐藏这样的复杂性,现代磁盘抽象出一个简单的视图,一个B个扇区大小的逻辑块序列,编号0,1,2,3……。
磁盘里封装着一个小的硬件/固件设备,磁盘控制器,维护者逻辑块号和实际磁盘扇区的映射关系。
系统进行一个I/O操作的过程:
- 发送一个命令到磁盘控制器,让它读某个逻辑块号。
- 控制器将逻辑块号翻译成一个可以唯一标识对应物理扇区的三元组。
- 控制器的硬件会解释这个三元组,将读写头移动到对应的位置。
- 读写头感知到的位放到控制器上的一个小缓冲区中,然后将它复制到主存中。
连接I/O设备
访问磁盘
固态硬盘(Solid State Disk, SSD)
一种基于闪存的存储技术。
它与其他硬盘的行为一样。
- 通常硬盘用USB或者SATA插槽。
- 同样是处理来自CPU读写逻辑磁盘的请求。
- 一个SSD封装一个或者多个闪存芯片和闪存翻译层。
- 闪存芯片与机械驱动器作用相同
- 闪存翻译层与磁盘控制器相同
如上图:
- 一个闪存由B个块组成
- 一个块由P个页组成
通常来说,一个页大小为512B ~ 4KB,一个块有32 ~ 128页,一个块的大小即为16KB ~ 512KB。
只有在一个块被擦除之后,才能写其中一页。不过一旦被擦除,块中的每一个页都不用再擦除。
大约在100000重复写之后,块就会磨损,不能再使用。
随机写的速度明显比随机读要慢。原因是:
- 擦除块要很久,1ms级。
- 如果写入的页已经有有用的数据在块中的其他页中,则需要先将块中的数据复制到别的块,再对进行擦写。
SSD的优点
- 结实
- 能耗低
- 随机访问速度极快
SSD的缺点
-
再反复写之后,容易磨损。
对与这个问题,实际上已经在闪存翻译层进行平均磨损逻辑处理,SSD实际上能用非常长的时间了。
-
价格比较昂贵。
现在价格也降下去了,反正SSD牛逼。
存储技术趋势
- 不同的存储技术有不同的价格个性能折中。
- 不同的存储技术的价格和性能属性以截然不同的速率变化着。
- DRAM和磁盘的性能滞后于CPU的性能
局部性
一种更喜欢引用最近引用过的数据项,或者邻近其他最近引用过的数据项的数据项,的倾向性,被称为局部性原理。
局部性通常由两种不同的形式:
- 时间局部性:被引用过一次的数据很可能在不远的将来再次被引用
- 空间局部性:被引用过一次的数据很可能在不远的将来引用其附近的数据
有良好局部性的程序比局部性差的程序运行的更快。
对程序数据引用的局部性
先看一段程序:
int sumvec(int v[N]){
int i, sum = 0;
for(i = 0; i < N; i++)
sum += v[i];
return sum;
}
对于sum
来说,它拥有良好的时间局部性,不过由于是标量,不具备空间局部性。
数组v
是一组向量,在这段程序中,它拥有良好的空间局部性,因为每个数据都是被顺序读取的。但是它的时间局部性很差,因为每个数据只会被读取一次。
在这个函数中,循环体的内的变量不是具有良好的时间局部性,就是具有良好的空间局部性,因此我们认为该函数拥有良好的局部性。
像这样访问一个向量的函数,我们称其是步长为1的引用模式,也叫顺序引用模式。
每隔k个元素进行访问,我们称其是步长为k的引用模式。
一般来说,步长越大,空间局部性越差。
再看一个例子:
int sumarrayrows(int a[M][N]){
int i, j, sum = 0;
for(i = 0; i < M; i++)
for(j = 0; j < N; j++)
sum += a[i][j];
return sum;
}
我们知道,二维数组是按行放置的,这种写法实际上就是顺序引用模式,具有良好的空间局部性。
而下面这段程序:
int sumarrayrows(int a[M][N]){
int i, j, sum = 0;
for(j = 0; j < N; j++)
for(i = 0; i < M; i++)
sum += a[i][j];
return sum;
}
看上去和上面那一段区别不大,但实际上这里已经变成了步长为N的引用模式,空间局部性很差,它的运行效率会比写法慢得多。
取指令的局部性
由于程序指令是放在内存中的,CPU必须读出这些指令,所以我们也希望能评价取指令的局部性。
而对于这部分来说,有循环体,循环体越小,迭代次数越多,其空间局部性和时间局部性越好。
局部性小结
- 重复引用相同变量的程序具有良好的时间局部性。
- 对于具有k步的引用模式的程序,步长越小,空间局部性越好。反之越差。
- 对于取指令来说,循环体越小,迭代次数越多,其空间局部性和时间局部性越好。
存储器层次结构
软件和硬件的这些基本属性互相补充得很完美。利用这种互补关系,我们想到一种组织存储器系统的方法,称为存储器层次结构。
从高处往低处走,存储设备变得更慢,更便宜和更大。
存储器层次结构中的缓存
高速缓存(cache,读作“cash”)是一种小而快速的存储设备,作为更大,更慢的设备中的数据作缓冲区域。使用高速缓存的过程称为缓存。
见上图。第K+1层被划分成数据对象组块,称为块,每个块都有唯一的地址和名字。其大小可以固定,也可变。
到了第K层则是被划分成较少的块,块的大小一样,包含着第K+1层的一个子集副本
缓存命中
如果程序需要在第K+1层寻找数据块d,则现在第K层寻找d,如果刚好在第k层,就叫缓存命中。这样读取速度更快。
缓存不命中
与缓存命中相反,并没有在第K层找到d,那我们需要从第K+1层找到d,放到第K层。如果K层已满,则会选择覆盖其中的一个块。
覆盖一个现存的块叫替换或者驱逐,被驱逐的块有时也称为牺牲块。
决定替换哪个块由缓存的替换策略来控制。例如:随机替换策略等。
缓存不命中的种类
区分不同种类的缓存不命中
- 冷缓存:第K层的缓存是空的。
- 强制性不命中/冷不命中:由于冷缓存,出现缓存不命中。该情况非常短暂,缓存暖身后就不会出现这种情况。
- 放置策略:当出现的不命中,第k层的缓存就必须执行放置策略,确定应该放在哪里。
- 随机替换策略:可以放在第k层的任何块中。速度最优但是实现代价昂贵,定位代价高。
- 冲突不命中:由于替换策略,块映射到同一个块,在需要一直调用时,会一直发生不命中。
- 容量不命中:由于缓存太小,没法把数据全部放下,导致出现不命中。
缓存管理
缓存管理是指,我们某个东西要将缓存划分为块,在不同的层之间传送块,并判定命中还是不命中,最后处理它们。
存储器层次概念小结
实际上就是遵循两个局部性设计的存储器层次结构。将时间局部性,空间局部性好的数据放在小但快且接近处理器的地方。
高速缓存存储器
早期计算机系统存储器层次结构只有三层:CPU寄存器,DRAM主存储器和磁盘存储。
由于CPU和主存之间的性能差距增大,系统设计者在CPU寄存器文件和主存之间插入一个小的SRAM高速缓存器,称为L1高速缓存。
性能差距依然在增大。系统设计者在L1高速缓存与与主存之间又塞了一个更大的高速缓存,称为L2高速缓存。
到现在,可能还包括一个更大的高速缓存,称为L3高速缓存。
通用的高速缓存存储器组织结构
每个存储器地址有m位,形成(M = 2^m) 个不同的地址。如下图
该高速缓存被组织成一个有(S = 2^s) 个高速缓存组的数组。
每个组包含E个高速缓存行。
每行由一个(B=2^b) 字节数据块组成。
一个有效位标记该行是否包含有有意义的信息。
t个标记位唯一标识存储在这个高速缓存行中的块。
可以用元组((S, E,B,m)) 来描述高速缓存的结构。
高速缓存的大小不包括标记位和有效位,高速缓存的容量大小为(C = S imes E imes B) 。
当一条加载指令要从主存地址A中读一个字出来时,CPU将地址A发送给高速缓存。如果高速缓存保存着地址A的副本,他就立即将器发送给CPU。
参数S和B将m个地址位分为了3个字段。
符号总结:
直接映射高速缓存
根据每个组的高速缓存行数E,高速缓存被分为不同的类。每个组只有一行的高速缓存称为直接映射高速缓存。
高速缓存确定一个请求是否命中,然后抽取出被请求的字的过程,分为3步:
- 组选择
- 行匹配
- 字抽取
直接映射高速缓存中的组选择
高速缓存从w的地址中间抽取出s个组索引位。索引位被解释成一组无符号数。就像是数组的索引一样。
直接映射高速缓存中的行匹配
由于一个组只有一行,当我们完成了组选择,就只需要判断有效位是否有效,有效则命中,无效则不命中。
直接映射高速缓存中的字选择
一旦命中,最后一步就是确定需要的字在块中是从哪里开始的。
块位移位提供了所需字第一个字节的偏移,就像是第一个字的索引一样。
直接映射高速缓存中不命中时的行替换
当缓存不命中,那就需要在下一层找到想要的块,然后将其放在组索引位指示的位置上。
运行中的直接映射高速缓存(实例)
我们先假设有一个直接映射的高速缓存:
解释:高速缓存有4个组,每组一行,每个块两个字节,地址4位。
将16个地址一一划分得出:
对上面的表做一些小小的总结:
- 标记位和索引位合起来标识了内存中的每一个块。
- 多个块会有同一个索引,即它们有相同的索引值。
- 映射到同一个高速缓存组的块由标记位唯一标识。
简单模拟:
初始时,高速缓存长这样:
-
读地址0的字。
现在组0的有效位是0,不命中。高速缓存从内存中取出块0,存储在组0,再返回内容。
-
读地址1的字。
刚刚将组1存好了,这次缓存命中。高速缓存立即返回数据,状态没有变化。
-
读地址13的字。
组2的标记位不是有效的,缓存不命中,加载块6,返回地址13的字。
-
读地址8的字。
组8的有效位是有效的,但是标记位不匹配,因此将原数据牺牲掉。
-
读地址0的字。
又发生缓存不命中,因为刚刚才把它牺牲掉,发生了冲突不命中,只能再重新复制一份。
直接映射高速缓存中的冲突不命中
冲突不命中经常发生,举一个向量点积的函数例子:
float dotprod(float x[8], float y[8]){
float sum = 0.0;
int i;
for(i = 0; i < 8; i++)
sum += x[i] * y[i];
return sum;
}
看起来具有良好的空间局部性,实际上并非如此。
我们假设每个块是16字节,整个高速缓存有两组。x和y数组内存是紧跟着的。
在运行时,先取出x[0]
缓存不命中,在块0中存储x[0] ~ x[3]
。接着引用y[0]
的时候,又是一次缓存不命中,结果由于地址问题,块0被覆盖成y[0] ~ y[3]
。接下来可想而知,x[1]
也同样的不命中。
这种情况就是典型的冲突不命中,,我们描述这种情况为抖动,即高速缓存反覆地加载和驱逐相同的高速缓存块的组。这种抖动导致速度下降2到3倍都不稀奇。
一旦发现抖动现象,程序员也能很好的解决。一个简单的办法就是在数组后面填充B个字节,消除抖动冲突不命中。
为什么高速缓存的索引不设置在高位
如果将索引设置在高位,那一段连续内存会一直只使用一个块造成冲突不命中而降低效率。
组相联高速缓存
组相联高速缓存放宽了限制,每个组都保存有超过一个的高速缓存行。一个(1<E<frac{C}{B}) 的高速缓存通常称为E路组相联高速缓存。
组相联高速缓存中的组选择
与直接映射高速缓存的组选择相同,组索引位标识组。
组相联的高速缓存中的行匹配和字选择
由于组相联高速缓存一个组存放了多个块,行匹配则必须检查多个行的标记位和有效位,才能确定是否在集合中。
组相联高速缓存中不命中时的行替换
如果CPU请求的字不在组里的任何一行,就叫做缓存不命中。
接下来我们需要取出需要的行放在组中。如果组中有空行,那就是一个好的选择。如果并没有空行,我们选择一个没有背经常使用的非空的行。
全相联高速缓存
一个组包含着所有高速缓存行的组,即(E= C/B) 。
全相联高速缓存中的组选择
它只有一个组,不存在组选择,因此也没有索引位。
全相联高速缓存中的行匹配和字选择
它的匹配方式和组相联高速缓存是相同的,主要就是规模问题。
有关写的问题
高速缓存有关读的操作非常简单,但是写操作就不是那样的了。
假设我们要写一个已经缓存的字,在高速缓存更新了它的w副本,怎么更新w在层次结构中紧接着低一层中的副本?
最简单的方法就是直写,立即将更新的副本写回紧接的低一层。缺点是会引起总线流量。
另一种方法是写回,尽可能推迟更新,只有在更新算法要驱逐这个更新过的块时才把它写到紧接的低一层。它能显著的降低总线流量,但是它的缺点时增加了复杂性,需要额外维护一个修改位。
另一个问题时怎么处理写不命中。
一种方法时,写分配。在低一层加载块到高速缓存,然后更新这个高速缓存块。写分配利用局部性,但是每次不命中都会导致一个块从低一层传送到高速缓存。
另一种方法,称为非写分配,直接把这个字写到低一层中。
直写时非写分配,写回时写分配。
一个真实的高速缓存层次结构的解剖
高速缓存即保存数据,也保存指令。只保存指令的高速缓存称为i-cache
,只保存数据的高速缓存称为d-cache
,既保存指令又包括数据的高速缓存称为统一高速缓存(unified cache
)。
现代大部分处理器都有两个独立的高速缓存,原因是处理器可以同时读一个指令字和一个数据字。
缺点是虽然可以确保指令和数据不会形成冲突不命中,但却容易造成容量不命中。
下图是Intel Core i7
处理器的高速缓存层次结构。
高速缓存参数的性能影响
有许多指标来衡量高速缓存的性能:
- 不命中率:一个程序执行的过程中,内存引用不命中的比率。
- 命中率:1-不命中率
- 命中时间:从高速缓存传送到一个字到CPU所需的时间。
- 不命中惩罚:由于不命中所需要的额外的时间。
高速缓存大小的影响
一方面,较大的缓存可能会提高命中率。
另一方面,使大存储器运行得更快总是要难一些。
块大小的影响
一方面,较大的块能利用程序中可能存在的空间局部性,帮助提高命中率。
另一方面,较大的块会导致高速缓存的行数较少,这会损害时间局部性。
还有,块越大,传输越慢,会导致不命中的惩罚增加。
相联度的影响
较高的相联度能够降低由于冲突不命中出现的抖动的可能性。而高相联度会导致速度变慢,且实现较难。
最终就是变成了不命中惩罚和命中时间之间的折中。
写策略的影响
高速缓存越往下层,越可能使用写回而不是直写。
编写高速缓存友好的代码
一个好的程序员应该总是试着去编写高速缓存友好的代码。
让常见情况运行的更快
注意力集中在核心函数里的循环上。
尽量减少每个循环内部的缓存不命中数量
不命中较少的循环运行得更快。
int sumvec(int v[N]){
int i, sum = 0;
for (i = 0; i < N; i++)
sum += v[i];
return sum;
}
观察该函数是否高速缓存友好。
我们假设一个高速缓存的块为B字节,那么一个步长为k的引用模式平均每次循环迭代会有(min(1, (wordsize imes k)/B)) 次缓存不命中。
假设V是块对齐的,字是4个字节的,高速缓存块为4个字,初始为空。
然后整个程序中,每一次不命中,都会让接下来3次都命中。这是我们能做的最好的情况了。
总之:
- 对局部变量的反复引用是好的
- 步长为1的引用模式是好的
下面讨论一下二维数组:
int sumarrayrows(int a[M][N]){
int i, j, sum = 0;
for(i = 0; i < M; i++)
for(j = 0; j < N; j++)
sum += a[i][j];
return sum;
}
假设和上面的函数一样,由于C语言数组是行优先,所以和上面的几乎是同样的情况。
但我们如果交换了i
和j
。
int sumarrayrows(int a[M][N]){
int i, j, sum = 0;
for(j = 0; j < N; j++)
for(i = 0; i < M; i++)
sum += a[i][j];
return sum;
}
如果数组不是特别大,我们是可以得到和上面的情况一样的命中率,可是如果数组比较大,那么整个过程就将全部都不命中,效率极低。