Read MIPS Run2nd MIPS cache
激烈一些,没有cache的MIPS就不是RISC..... MIPS reset 后cache的大部分状态都是未定义的, 这也是bootstrap需要初始化cache的原因.
Cache 和cache 管理
L1 cache的重要作用就是在固定的时间内完成操作以使CPU的pipeline无间歇的运转. MIPS采用哈佛结构的cache即I,D分开.
X86的做法是隐藏cache, 隐藏绝大多数细节. 而MIPS认为, cache是让CPU更快,而不是帮助系统程序员. 无论什么CPU, cache对于应用都是透明的.
下面, see MIPS run的作者给出图示来讲cache的原理. 这几乎每个CPU都相同. 这里把我从其他CPU里(ARM讲的最好)搞到的解释放到这里. 也可以参考拙作:
下面的两个例子和图形分析下啥是index,啥是tag:
K8 4k 2-way cache, cache line 64B
0. 共4k, cache line 长度是64B, 共64个cache line
1. byte select : 64B, 2^6, 即 [0-5] bit 选择一个cache line内的byte
2.
64 个cache line, 需要2^6个index去索引,6-11bit, 但是2way的含义在于一个index选择2个cache
line(数据可以存储于这2个cache line的任一个), 即只要2^6/2, 5bit 作index 就够了, [10-6] bit
就是k8 cache 的index
3.剩下的[31:11] bit 用于确定这2个cache line是否包含了要存取的数据
-----------------------------------
4.
上图是2way cache 实现原理的一种示意图. 左边的tag ram存储的是cache 的tag 即地址的[31:11]bit,
当再cache中查找的时候,用index [10:6]bit 同时和tag ram中的way0, way0 相比较,
如果有一个相同就代表命中了. 我们就知道命中的是那个way的cache.
5. 左边存储cache的具体数据, 却并不是以cache line的形式来存储的,而是每个way的data SRAM 各存储了一个word, 总共有512个entry, 所以这些数据的index 是[10:2]bit.
6. 结合第4步骤选出来的way number, 就知道到底命中了那个way的数据
这
种实现, cache line 的index (way 选择), 和date的'index ' 分别具有不同的bit数目.
n-way的cache意味着命中cache的时候需要同时和n个 cache index 进行比较.
(肯定是同时,如果比两次还不如用1:1的cache呢).
如果n比较大, 用类似CAM的实现方式显然比较靠普, 就下面要说的ARM920T的I-cache.
ARM920T (SAM SUNG 2410a) I-cache :16k 64-way cache, 32B cache line
0. 共16k, cache line 长度是32B, 共512个cache line
1. byte select : 64B, 2^6, 即 [0-4] bit 选择一个cache line内的byte
2. 512 个cache line, 需要2^9个index去索引, 64 way就是一个index选择64个cache
line, 即只要2^9/64, 3bit 作index 就够了, 据就是[7-5] bit是ARM920T I-cache的index (这里把cache index 叫做seg)
3.剩下的[31:8] bit 用于确定某个way内的64个cache line是否包含了要存取的数据
-----------------------------------
4. 上图是用CAM 实现的64 way cache的一种示意图. 0-7 这7个平面是7个CAM内存组. CAM中存的是cache TAG即地址的[31:8]bit,
当再cache中查找的时候,用cache index [7-5]bit 同时和CAM 中的tag相比较, 如果命中就直接获取到了一个完整的cache line.
5. 0-4 bit 可以选中这个cache line 中的byte, 如果是word 操作, [4:2]bit就是word index
n-way的cache意味着命中cache的时候需要同时和n个 cache index 进行比较. 这次是用CAM.
MIPS cache 的几句话:
---1992年前的MIPS多是采用Direct Map的cache
---2 Way, 4-way 较为常见. 因为比较简单,易于实现.
---大于4way的cache,在替换策略上就要折衷实现, LRU是比较难(慢), 或许采用LRF(least recently filled).
---多way的cache 需要的线就比较多,难以集成到CPU内部,对频率提生同样产生负面影响.
--- 早期MIPS 采用direct map的write-through的cache, 用一个write buffer 来加速CPU写操作.
--- 到了R4000, CPU已经快到有必要让内存系统吸收每个write操作的地步.(一般认为10个指令中会有一个写操作, write
through 只要内存操作在5-7倍指令cycle之内,就能满足要求,当时DRAM cycle大约180ns,
CPU在30-40Mhz之内还行).
----所以引入了cache write back 模式. 这中模式有一种叫做write alloc,即写入内存的时候同时分配一个cache条目给数据.
SMP系统的的cache设计在后面.
Cache 设计的一些其他决定(选择)
--- Physical address 和 virtually address
----line size的选择
----split/unified : MIPS 总是采取哈佛结构的分离I,D cache.
对于cache的index和cache的有关选择,再次自我推荐:
以下内容,就copy自<<从ARM VIVT看linux的cache 处理>>
Cache Alias
Virtual index cache 的速度是快了, 但是会带来其他的问题: 一个物理地址的内容可以出现在多个cache line中, 这就需要更多的cache flush操作. 反而影响了速度. 这就是cache alias.
随
着技术进步, TLB查找速度提高了, 在cache 用index查找cache的过程中TLB可以完成虚拟地址到物理地址的转换工作,
在cache比较tag的时候物理地址已经可以使用了, 就是说采用physical tag可以和cache并行工作, virtual
tag已经不怎么使用了, 特别是在容量比较大的cache中(比如l2 cache). 只有小,并且延迟很小的cache
还或许采用virtual tag.
alias就是同一个物理地址被映射到两个或多个相同或者不同的虚拟地址的时候,cache 中存在多于一个的cache line 包含这个物理地址的数据, 具体来讲, alias 是这样产生的:
(强烈推荐 )
一个PA 被映射到不同的虚拟地址, 如linux下, 内核和用户访问页面的虚拟地址可能不同. 这些虚拟地址有个特点,因为映射到同pa , 其低几位bit 必然相同.
PA--> VA1 |31 12 | PA's low 12 bit |
VA2 |VA2 ie, 31-11 12 | PA's low 12 bit |
这种情况下是否有alias, 取决于index采用哪几个bit:
cache index: |n m| word|byte|
1. 如果bit [n..m]中包含了Vitual 地址的几个比特,那么因为VA1/VA2不同, 就会有两个cache line 可以包含这个地址的数据, 这一点对于VIVT,和VIPT 都相同.
2.
如果 bit [n..m] 完全落在了PA的 低几个bit, 对于direct map的cache, 不会有alias出现,
但是对于n-way的情况, 就要看是PT还是VT了. 如果是VIVT, 因为 VT 不同,那么alias不可避免. 如果VIPT,
则因为Pysical的高几位地址也相同, alias就不会出现了.
Cache Ambiguity
这里顺便提
下, 和cache alias相类似的问题是 Ambiguity: 不同的物理地址映射到相同的VA. 这种情况下在linux内,
只有不同进程的用户空间的页面才可能(fix me). 这种情况会造成同一个cache line在不同的进程中代表不同的数据,
切换进程的时候看似invalid user space 的cache必须进行. 实际上这个也要看是VIVT还是VIPT.
Process 1: |31 VA 20| phy address low bit| => PA1 | phy address low bit|
Process 2: |31 VA 20| phy address low bit| => PA2 | phy address low bit|
注:两个进程的虚拟地址完全相同, 就是说phy address low bit 也相同.
====================
地址切换:
1.
在VIPT 的时候, 或者要像i386那样切换整个pgd, 或者像mips, 有ASID作为区分. 这样TLB 不会有Ambiguity,
同时VIPT 的cache 要比较phy address 来确定 cache hit. 故不用flush cache.
2.
VIVT 的时候就就比较麻烦, 尽管MMU 不会给出错误的PA, 但是因为是vitual tag 也相同,
就会命中上一个进程的对应于不同物理地址cache了, flush cache 就必须进行了. 具体的例子, 如
arm/mm/proc-arm926.S
=====================
TLB 撤销: exit_mm
还
有就是撤销TLB的时候, 撤销的时候,VIVT 仍然需要把cache flush, 否则到心的mm后就会有Ambiguity错误了, 见
do_exit -> exit_mm->mmput->exit_mmap->flush_cache_mm.
MIPS CPU Cache
kseg0 是物理内存低512M的cache "映射", kseg1用于非cache 访问. 64bit CPU有另外的窗口可以访问高于512M的cache, 在32bit CPU上, 只能通过TLB了. TLB可以指定是否可以cache.
X86类似的CPU使用硬件的种种手段是cache对系统软件保持透明, 这样的cache也称为"coherent"(snoopy). 除了初始化cache, 正常运行的系统需要关注cache的地方不是太多:
--- DMA
设备读取内存前需要flush dcache. (write buffer). DMA向内存传输数据完成后要invalid相关的cache line.
--- 动态产生指令
先flush dcache, 然后invalid I cache. 现代MIPS CPU实现了synci来完成这个工作, user level 程序也可以使用这个指令.
2006年的时候大家趋向于采用硬件手段来消除cache带来的复杂度, 从而减少犯错误的几会. MP下cache更为复杂, 还好,1980s中期的时候一个叫做Futurebus 的标准的产生让我们放心多了.
2006年以来,出了桌面和极为高端的嵌入式设备, 很少见到L3cache, 一般只到L2而已. 近代CPU的发展也触及到了MIPS的基本原则,
CPU变得飞快, 不得不延长pipeline,容许2个cycle的 cache access. 有L2cache的CPU, L1 cache
一般较小, 16kx2是一个常见配置.
cache的配置接口在MIPS早期有两个版本一个是R3000类型的32bitCPU, 另一个是R4000类型的64bitCPU, 好在MIPS32采纳了R4000的接口,现在统一到了一起.
Programming MIPS32/64 Caches
--- Invalidate range of locations
指令cache HitInvalidate 接受一个虚拟地址, invalid 包含这个虚拟地址的cache line.
--- Write-back range of locations
cache HitWritebackInvalidate 接受一个虚拟地址, 回写数据,同时invalid 包含这个虚拟地址的cache line.
---- Invalidate entire cache
cache IndexInvalidate 接受一个索引.
---- Initialize caches
初始化tag, 清零Taglo,对每个line进行cache Index-StoreTag 操作. 注: 填充I cache 可以使用cache Fill 指令.
cache 指令的格式
cache 命令和load 的格式类似. 一个16bit的偏移加上一个base寄存器. 但是在base 寄存器内需要一个5bit的操作数. 当用如下格式的汇编时, OP就是那5个bit的操作数的一个助记符:
cache OP,addr
另外还有高2个bit需要选择要操作的cache:
0 = L1 I-cache
1 = L1 D-cache
2 = L3 cache, if fitted
3 = L2 cache, if fitted
cache 指令选择cache line的方式有三种:
--- hit type 指定一个(虚拟)地址, 如果hit一个cache line则操作这个cache line.
--- adress type 也是指定一个地址, 如果cache line 中无此数据, 先加载进来再进行操作.
--- index type 指定一个cache line, 地址格式如下: ( way, index in way, index in cache line), 图示bit数只是例子,需要查询特定CPU的寄存器来确定bit边界.
如果提供了synci(D-cache write back + I cache invalid), 要优先使用synci(而不是分开的两个cache 操作)
Cache 初始化和TagLo/TagHi
TagLo, taghi用于CPU和cache交换cache的tag数据. 除非大于36bit的地址空间, tagHi一般用不着.
Taghi,taglow的定义是CPU特定的,只有一个是通用的: 全0代表了合法,正确的一个没有有效数据的cache. cache
IndexStoreTag 就是copy taghi/lo到cache 中, 这样用全0值,即可初始化CPU 的cache.
具体使用需要参考CPU手册.
memory/cache CacheErr, ERR, and ErrorEPC Registers
作为构建可信赖的计算系统,数据一致性检查非常有必要,因此,用于构建高可靠性系统的MIPS
CPU常选择在cache这个级别来提供数据一致性检测, 比如简单的Parity或者ECC.
现在的内存系统倾向于提供多个8bit的数据传输,常见的是64bit的数据和8bit的校验bit, 各种校验的实现大多基于此.
Parity比较简单,为每个byte提供一个校验bit,他之可以告诉你数据是不可信的.但是Parity的准确度大约是50%.(全垃圾数据的校验位
也可能是正确的). ECC使用全部的8个bit来校验64bit的数据,
可以校正1个bit的错误,并且2个bit的错误检测的准确率是100%.但是ECC不能支持
half-word的写操作,必须写完64bit后重新计算ECC,MIPS对于不可cache的系统要求内存系统提供这个操作.
所以简单的系统多提供parity或者没有校验.
不必要在装入cache的时候就进行校验,在数据被使用的时候去校验反而能准确的定位使用这个数据的指令.对一个uncache的数据读取,也是报告的cache parity error,这点或许有点误导人,需要注意.
处理校验错误的中断向量位于uncache区域,这个好理解. 对于ECC的错误,有的CPU自己能够校正错误,有的则交给软件处理. CacheErr 寄存器是特定于CPU的实现的,需要参考CPU手册来确定.
初始化cache
1. 设置一些可以fill cache的内存: 比较有用的技术是预留最低端的32k专门用于初始化cache, 目的是可以保证我们拥有正确的校验位()(至少保留到cache 初始化完成之后). (RFC)
2. 把TagLo(TagHi如果需要)设置为0, 然后用cache IndexStoreTag来初始化cache,这样cache的valid bit是清空的并且Tag数据的Parity是正确的.
3.禁止中断.
4.先初始化I-cache, 然后初始化D-cache.(RFC)
for (addr = KSEG0; addr < KSEG0 + size; addr += lnsize)
/* clear tag to invalidate */
Index_Store_Tag_I (addr);
for (addr = KSEG0; addr < KSEG0 + size; addr += lnsize)
/* fill once, so data field parity is correct */
Fill_I (addr);
/*有些CPU会在cache是invalid的情况下产生数据校验异常,故fill是有必要的,这个指令并不是mandentory的,但是每个实现了
ECC或者parity的CPU都应该有Fill I指令. 没有就不用这个loop了*/
for (addr = KSEG0; addr < KSEG0 + size; addr += lnsize)
/* invalidate again---prudent but not strictly necessary */
Index_Store_Tag_I (addr);
分三个loop是有道理的,比如是2-way cache, sotre tag I存储一个全0的tag的时候会清除LRUbit,这样导致一个loop只能初始化一半的cache.
5.D-cache 初始化因为没有fill指令,需要依赖于cache miss.
/* clear all tags */
for (addr = KSEG0; addr < KSEG0 +
Index_Store_Tag_D (addr);
/* load from each line (in cached
for (addr = KSEG0; addr < KSEG0 +
junk = *addr;
/* clear all tags */
for (addr = KSEG0; addr < KSEG0 +
Index_Store_Tag_D (addr);
Invalidating or Writing Back a Region of Memory in the Cache
invalid cache用hit style的寻址模式比较高效,如果是大面积的invalid,不如就全部flush cache算了.
PI_cache_invalidate (void *buf, int nbytes)
{
char *s;
for (s = (char *)buf; s < buf+nbytes; s += lnsize)
Hit_Invalidate_I (s);
}
如果传递的buf是程序地址,用hit style就没有任何问题,如果是基于物理地址, 对于头512M的物理地址,需要把地址调整到kseg0
PI_cache_invalidate (p + 0x80000000, nbytes);
Cache的效率问题
自从1990s on chip cache开始流行以来,cache的有效使用就成为发挥cpu效能的一个关键. 特别对于嵌入式系统.
统计来讲,CPU常常花费%50-60的时间来等待cache refill,导致CPU效率翻倍只能提高应用25的性能.
以下两个指标可以作为描述cache效能的参数:
--- Cache misses per instruction: cache miss除以执行的指令条数. 也常用 cache misses per thousand instructions来描述.
--- Cache miss/refill penalty : 内存系统填充cache直到CPU恢复执行所需要的时间.
cache miss rate—the number of misses per CPU memory access:
受到多方面影响不是一个好的参数.
比如x86,比较缺少寄存器,故而编译器用堆栈作为一个替代品,这样产生非常密集的数据访问:只访问堆栈那一小段内存,从而cach的效果较好.而
mips的同样代码访存指令就没有那么多,这个参数上就吃亏了. 相比而下,cache misses per thousand
instructions受此影响就较少.
提高使用效率的方法:
--- 减少cache miss次数
增大cache(贵阿,64k的cache占据几乎和cpu其他部分一样面积的硅片). 增加cache set
associativity,但是4way以上就效果不明显了. 多级别cache,L2需要足够大(L1的8倍以上,并且比内存访问至少快2倍.).
优化软件:没有很好的工具.
--- Decrease the cache refill penalty
快速取回第一个word(就是减少延迟,呵呵),增加带宽比较费钱,相比之下,较少延迟比较实惠,但CPU设计者好似不愿意改变.
增加内存burst的带宽,比如bank interleaving,但是现在已经少见了(However, while memory
latency has reduced only very slowly as the chips shrink, the
band-width available from a single standard memory component has
exploded. And within a DRAM bank, chips expose separate internal banks
where accesses can be pipelined to reduce overall latency. As a result,
physical bank interleaving is now rare.)
参考资料:
bank interleaving
--- Restart the CPU earlier
许多MIPS cpu实现了critical word first的优化,告知内存管理器把需要的数据优先传回,便于尽早开始计算.
--- Don’t stop the CPU until it must have the data
load指令不halt CPU,只把数据送达内存系统,推迟到真正访问数据的时候才根据需要决定是否等待,这叫做nonblocking
load现在也很流行.更激进的做法是继续运行其他不需要这个数据的指令,就是OOO(乱序执行,比如R10000).OOO不仅用于load优化,还用
于计算优化和分支. 但是OOO即复杂又费电.
---Multithread the CPU
MIPS MT. 把CPU等待的时间用于运行另一个硬件线程.
Cache的软件优化
比较适合于嵌入式系统,为一个特定应用优化cache访问. 从cache miss的原因入手:
--- 第一次访问miss: 必须的
--- Replacement : 超越cache的大小限制,途径是用大的cache或者小的程序.
--- Thrashing: 超越了set-associative. 使用多way cache,或者优化程序.
优化方法:
--- 减小程序
--- 减小常用部分大小(2/8原则,呵呵)
--- 锁定常用数据到cache: 多路cache的好选择,特别是多路大容量,呵呵. 不过也没有有效手段确定到底锁定什么.
--- 避免trash : 多way.
--- 让极少使用的数据不可cache:几乎不正确,极少用就一般不会进入cache阿,谨慎使用这个第五公理(欧式几何平行公理,呵呵).
一般要现排除软件问题, 最后去看硬件问题,硬件问题需要硬件自己搞定(比如超级慢的refill).
cache Aliases
一个物理地址的内容可以出现在多个cache line内.对于VIPT的MIPS这个比较常见.原理看看上面俺的"大作"即可. MIPS
L2(其他CPU也大多是)PIPT,不用担心. 硬件人员应该铭记:这个东西确实是15年前(2006-15)一个硬件bug,而不是什么特色.