毫秒必争之如何搞定cache(下)
本文以SmartPro 6000F使用的nios ii内核为例,详述了如何搞定cache,将程序的运行时间从最开始的30s优化到25s,再从25s优化到最终的24s。尤其是那最后1s的优化,遇到了很多问题,而这些问题在嵌入式系统里,任何一款配置了cache的处理器都可能会碰到,所以特撰此文献给那些还在倍受cache折磨的工程师们。全文分上下两部,上部为如何搞定指令cache,下部为如何搞定数据cache。
在上一篇的“毫秒必争之如何搞定cache(上)”里,详述了在SmartPro 6000F上,我们是如何搞定cache,将客户芯片的编程时间从30s优化到了24s。本以为,cache已经完全被我们驯服了,可惜好景不长,在给客户芯片增加了一个小功能后,在O2优化等级下编程时间又莫名其妙的掉回到了28s,调出逻辑分析仪观察内存sram的读取和写入信号线,如下图所示,在运行编程时序的时候,cache竟然又不乖了,出现了频繁的更新操作。
图一
放大上面的波形图可以看到,这次访问的内存地址从0x20FE8开始,到汇编结果里查看,0x20FE8并不在指令区域,而在数据区,对应的是一个全局变量guiTime,看来,我们原来只搞定了指令cache,数据cache还有问题(配置了cache的系统里,为了进一步提高效率,一般都会分为指令cache和数据cache)。
图二
guiTime确实有在编程时序里频繁用到,但是我们在nios ii内核里分配给数据cache的空间有2KB,而时序里所有的全局变量才不到1KB。理论上说,运行时,这些全局变量都应该存在数据cache里,不应该再频繁更新cache了。
仔细观察图二,nios ii内核在读完0x20FE8开始的8个数据后,立刻又向0x3FFE8开始的地址写入了8个数据。查看汇编文件,0x3FFE8指向的是指令的stack栈区域,光凭这些暂时还找不到问题的根结。在逻辑分析仪的数据包里,我们又发现了图三所示的一段波形,nios ii内核干了两件事,从0x3FFE8地址开始读取8个数据,然后向0x20FE8地址写入8个数据,刚好和图二里干的事情相反。nios ii内核没事在0x20FE8和0x3FFE8两个地址之间导来导去是在干嘛呢? 而且两个地址的末尾都是FE8,一个假象瞬间蹦了出来———难道这两个地址在cache里发生冲突了?(10分钟内,不能从"两个地址末尾都是FE8"这条线索联想到cache冲突的同学,请先面壁好好反省cache原理,想不起来的请先阅读前文“毫秒必争之如何搞定cache(上)”)
图三
根据直接映射cache的散列函数:cache_addr = ram_addr mod cache_size,0x3FFE8和0x20FE8在2KB的cache里的散列地址都是0x7E8,两个地址在cache里发生了冲突,导致了频繁的cache更新。在给客户增加功能之前,guiTime在内存的分布地址不在0x20FE8上,不会和堆栈区的数据发生冲突,而在增加功能之后,guiTime凑巧被编译器分布在了0x20FE8上,结果冲突就发生了。那么为什么guiTime变量会和看似毫无关联的栈区数据冲突呢?
回想了下堆栈的作用,恍然大悟。调用函数时,返回地址等一些现场信息都会被存入栈中,函数结束后,出栈取出返回地址,PC指针跳转到返回地址继续运行。如果函数被频繁调用,为了减少频繁访问内存的消耗,栈区地址会被直接映射到cache的对应空间,这样函数在调用时,现场信息的存取都将直接在cache里操作。但是,如果一个函数的现场信息和要使用到的全局变量,在cache里的散列映射地址完全相同,悲剧就发生了。
Program函数在被调用时,现场返回信息先存在cache的0x7F8上,当函数执行过程中要用到全局变量guiTime时,内核会先到guiTime对应的cache地址0x7F8上寻找,发现没有存储这个变量,于是就从内存0x20FE8读入guiTime,并同时将其覆盖到cache的0x7F8上,以备日后使用,但是0x7F8之前存储的是非常重要的函数现场信息,如果这些信息缺失,时序将会直接跑飞,所以在覆盖之前,内核会将cache里的现场信息存入内存0x3FFE8开始的真实栈区,到此,已经重现了图二的一幕。函数执行结束后,内核需要返回地址给指明下一条出路。内核先到cache的0x7F8上搜寻,寻找无果后,再从内存的栈区0x3FFE8开始的地址取回返回地址继续跑路,同时准备将现场信息覆盖到cache的0x7F8上,避免以后重复读取内存,但此时0x7F8上存储的是全局变量guiTime,不能被随意篡改,否则就不能称为"全局变量"了,无奈的解决办法是在覆盖前,将guiTime再写回原始的内存地址0x20FE8,至此,图三的波形也再现了。如果Program函数被频繁的调用,图二和图三的波形将频繁交替显现,运行时间就在这交替之间白白的浪费了。
解决办法还得从cache的散列函数入手,但是和“毫秒必争之如何搞定cache(上)”遇到的情况略有不同,栈区的开始地址是0x3FFFF在内存的末尾,和全局变量所在地址0x20FE8的跨度太大了,远远超过了cache_size的2KB,嵌入式系统里根本没法提供这么大的cache_size来消除冲突。但是注意,栈区所在区域虽然很大,但是真正使用到的地址是非常有限的,以本文为例,总共才使用了0x3FFFF到0x3FFE8的24个地址,对应到cache的映射地址从0x7FF到0x7E8,所以只要数据区域在cache的映射空间避开这段地址,就可以避免冲突了。前文所述的第二种方法在此就可派上用处了,可以在bsp里,新增一个从0x1000到0x17E7的段.DataCache,然后将程序里的所有的全局变量强制分布到这个段里,这样数据和栈永远也不会冲突了。
最后还有一种终极的解决办法就是,消除祸根,不用全局变量......
至此,指令cache和数据cache的冲突问题都搞定了,解决办法也都大同小异。虽然最终的优化结果只快了1s,但我们距离全球最快编程器的目标又近了那么一点点。