little小蔡

【嵌入式】cache,你给我听话点

0
阅读(3860)

在上一篇“为什么程序越优化越慢?”里,详述了程序指令在cache里发生冲突后另运行效率变的完全不可预测的问题,并提出了两种将不老实的cache变乖的良方。本以为cache已经完全被驯服了,没过多久,已经优化到23s的程序,在我修改了一段代码后,又掉回了30s,调出逻辑分析仪查看主存sram的读取和写入信号线,在运行关键程序段时,cache竟然又不乖了,导致CPU频繁的访问主存,拉低了效率。



图一

不过这次访问的主存地址不在程序指令区,而是数据区域。在编译的map文件里,0x20FE8对应的是一个全局变量guiTime。

图二

guiTime此变量确实有在关键函数里频繁使用到,但是数据cache总共有2KB的空间,而程序所有的全局变量不到1KB。理论上说,运行时,这些全局变量都应该存在数据cache里,不应该去频繁访问主存呀。

再看一看程序,guiTime被定义为volatile类型。

volatile  uint32  guiTime

“为了提高存取速度,编译器会将一些变量存放到一个寄存器内,下次使用的时候直接从寄存器里读取,当原变量被外界(中断,其他线程)改变后,寄存器里的值是不会变的,这样程序里用的寄存器值和实际的变量值就不一致了,错误随之而来。将变量定义成volatile类型,就是为了避免这种错误,每次使用数据都从原始地址读取,而不是寄存器。”

上述是对volatile类型一种被广泛认可的解读,注意红色的标注“原始地址”,这是否意味着定义成volatile类型的变量,使用时,都会无视cache,而去访问主存?抱着强烈的好奇心,做了一个小测试,去掉了针对guiTime的volatile定义,测试关键程序的运行时间———还是30s,从逻辑分析仪上仍能看到CPU的频繁访问0x20FE8地址开始的主存。

看来不是volatile的问题,至于"每次都要从原始地址读取"里的原始地址到底是哪里,先搁一搁,解决问题要紧。

回头看一看图二,CPU在读完0x20FE8开始的8个32位数据后,立刻又向0x3FFE8开始的地址写入了8个32位数据,而在map文件里,0x3FFE8指向的是程序的stack栈区域,光凭这些暂时还找不到问题的根结。

在逻辑分析仪的数据包里,又发现了下面一段波形,图三里,CPU干了两件事,从0x3FFE8地址开始读取8个数据,然后向0x20FE8地址写入8个数据,刚好和图二里干的事情相反。CPU没事在0x20FE8和0x3FFE8两个地址之间导来导去是在干嘛呢? 而且两个地址的末尾都是FE8,一个假象在脑子了蹦了出来-------难道是两个地址在cache里发生冲突了?(10分钟内,不能从"两个地址末尾都是FE8"这条线索联想到cache冲突的同学,请先面壁好好反省cache原理,想不起来的请先阅读前文“为什么程序越优化越慢”)

图三

根据直接映射cache的散列函数:cache_addr = ram_addr  mod  cache_size,0x3FFE8和0x20FE8在2KB的cache里的散列地址都是0x7E8,两个地址上存放的数据肯定会冲突,导致频繁的读取主存,这也解释了为什么很长一段时间没有出现关键函数运行迟缓的现象,因为之前的guiTime的存储地址不在0x20FE8上,没有和栈区数据发生冲突,直到我修改一段代码后,guiTime凑巧被编译器分布在了0x20FE8,结果冲突就发生了。那为什么guiTime变量会和看似毫无关联的栈区数据冲突呢?


回想了下堆栈的作用,恍然大悟。函数调用时,返回地址等一些现场信息都会存入栈中,调用结束后,出栈取出返回地址,PC指针跳转到返回地址继续运行。如果函数被频繁调用,为了减少存取消耗,栈区地址会被直接映射到cache的对应空间,这样函数在调用时,现场信息的存取都将直接在cache里操作,而不再是主存的栈区。但是,如果一个函数的现场信息和要使用到的全局变量,在cache里的映射地址完全相同,悲剧就发生了。

假设,Key函数在调用时,现场返回信息先存在cache的0x7F8上,当Key函数在执行过程中要用到一个全局变量guiTime,CPU先到guiTime对应的cache地址0x7F8上寻找,发现没有存储这个变量,于是就从主存0x20FE8读入guiTime,并同时准备将其覆盖到cache的0x7F8上,以备日后使用。但是0x7F8之前存储的是非常重要的函数现场信息,如果这些信息缺失,CPU将不知程序去向——直接跑飞,所以在覆盖之前,CPU会将cache里的现场信息存入主存0x3FFE8开始的真实栈区,到此,已经重现了图二的一幕。函数执行结束后,CPU需要返回地址给以指明一条出路。CPU先到cache的0x7F8上搜寻,寻找无果后,从主存的栈区取回返回地址继续跑路,同时准备将现场信息覆盖到cache的0x7F8上,避免以后重复读取主存,但此时0x7F8上存储的是全局变量guiTime,不能被随意篡改,否则就不能称为"全局变量"了,无奈的解决办法是在覆盖前,将guiTime再写回原始的主存地址,至此,图三的波形也再现了。如果Key函数被频繁调用,图二和图三的波形将频繁交替显现,运行时间就在这交替之间白白的浪费了。

解决办法还得从cache的散列函数入手,但是和前文遇到的情况略有不同,栈区的地址一般都从主存的末尾开始,是一个相当大的数值0x3FFFF,和数据所在区域0x20FE8的跨度太大了,远远超过了cache_size的2KB,而且嵌入式系统里也没法提供这么大的cache_size来消除冲突。但是注意,栈区所在区域虽然很大,但是真正使用到的地址是非常有限的,以本文为例,总共使用了0x3FFFF到0x3FFE8的24个地址,对应到cache的映射地址从0x7FF到0x7E8,所以只要数据区域的在cache的映射空间避开这段地址,就可以避免冲突了。前文所述的第二种方法在此就可派上用处了,比如在bsp里,新增一个从0x1000到0x17E7的段.DataCache,然后将程序里的所有的全局变量强制分布到这个段里,这样数据和栈永远也不会冲突了。

最后还有一种终极的解决办法就是消除祸根,不用全局变量....

再回到文中关于volatile的解读,解决了数据和栈的冲突后,无论在全局变量前是否加volatile的定义,都不会操作主存了。在加入cache的系统后,原始地址的主存已经被映射到了cache上,所以这里“原始地址”的含义其实就是cache了。

cache确实是一个不安分的家伙,作风爽快,干事利落,但性格经常会反复无常,很难伺候,对它是又爱又恨,要想用好它,需要好好的持久调教。翻篇后,希望cache能听话点,别再惹事了啊。