前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >MIPS架构深入理解10-向MIPS移植软件之内存序

MIPS架构深入理解10-向MIPS移植软件之内存序

作者头像
Tupelo
发布2022-08-15 16:26:04
9150
发布2022-08-15 16:26:04
举报
文章被收录于专栏:嵌入式ARM和Linux嵌入式ARM和Linux
  • 1 内存访问的排序和重新排序
  • 2 访存顺序和写缓存
  • 3 写缓存的flush

站在巨人的肩膀上,才能看得更远。 If I have seen further, it is by standing on the shoulders of giants. 牛顿

这是向MIPS架构移植软件的问题系列之第三篇。在前两篇文章

*《MIPS架构深入理解8-向MIPS架构移植软件之大小端问题》

*《MIPS架构深入理解9-向MIPS移植软件之Cache管理》

中,我们分别讨论了大小端模式和Cache对于移植代码的影响。那么本文,我们再从内存序理解一下对于移植代码的影响,尤指底层代码或操作系统代码。

1 内存访问的排序和重新排序

程序员往往认为他们的代码是顺序执行的:CPU执行指令,更新系统的状态,然后继续下一条指令。但是,如果允许CPU乱序执行,而不是这种串行方式执行,效率可能更高。这对于执行load和store这种存储指令尤其重要。

从CPU的角度来看,执行store操作就是发送一个write请求:给出内存地址和数据,其余的交给内存控制器完成。实际的内存和I/O设备相对较慢,等write操作完成,CPU可能已经完成了几十条甚至几百条指令。

read操作又有不同:它需要发送一个read请求,然后等待对请求的响应。当CPU需要知道内存或者设备寄存器中的内容时,没有得到请求响应前,可能啥也做不了。

如果想要追求更高的性能,就意味着我们需要让read尽可能地快,甚至不惜让write操作变得更慢。进一步考虑,我们可以让write操作排队等待,把随后的任何read操作请求提前到write请求队列之前执行。从CPU的角度来看,这是一个大优势:尽可能快地启动read操作,就越早得到read操作的响应。然后,在某个时刻把执行write操作,而且write请求队列的大小是固定的。但是,这个write操作可能需要写Cache一段时间。如果这个队列满了,可能需要停下来等待一段时间,等待所有的write完成操作。但是,这肯定要比顺序执行,效率更高。这就是现代CPU一般都具有一个write buffer的原因。

看到这儿,你可能会有一个疑问:某些程序可能会写入一个地址,然后再将其读回来,这时候会怎么样呢?如果read提前到write之前执行,我们可能从内存中读取的是旧值,从而导致程序发生故障。通常,CPU会提供额外的硬件,比较read操作的地址和write队列中的地址,如果有相同的项,就不允许这样的read操作提前到write操作之前执行。

上面的讨论没有考虑真正的并发系统,比如多核系统。并发执行的任务间共享变量,对其执行read和write操作会非常危险。比如使用共享变量进行同步和通信的时候,内存访问次序就会非常重要。这种情况下,软件一般会采用精心的设计,比如锁和信号量,进行同步操作。

但是,使用共享内存,还有一些技巧,往往效果更好,开销也更小。因为不需要使用信号量或者锁。但是,可能会被乱序执行打断。假设,我们有2个任务,如下图所示:一个读取数据结构,一个写数据结构。它们可以交替使用这个数据结构。

为了能够正确执行,我们需要知道,对于reader任务来说,当什么时候reader任务看见关键域中的值发生了更新时,能够保证其它所有的更新对reader任务可见。

当然,硬件可以实现所有的内存访问顺序问题,从而将它们对程序员不可见,但是也就放弃了解耦read和write操作带来的性能优势。MIPS架构提供了sync指令实现这个目的,它可以确保sync指令之前的访问先于之后的执行。但是,这种保障指令有其局限性:只与内存的访问顺序有关,只能被非Cache或具有Cache一致性的内存访问的参与者看见。

对于上面的示例,为了让其在合适的系统上可靠地运行,writer任务应该在写关键域的值之前,调用一条sync指令;reader任务应该在读关键域的值之后插入一条sync指令。对于sync指令的详细使用方法,可以参考《MIPS指令集参考大全》一文。

不同的体系架构对执行顺序作出了不同的规定。一类极端情况就是,要求所有的CPU和系统设计人员努力保证一个CPU的全部读和写操作,从另一个CPU的角度看上去顺序完全相同,这叫做强序。也有一类情况就是弱序,比如只要求所有的写操作保持顺序不变。而MIPS架构更为激进,完全就是无序访问内存。这就要求我们系统开发人员必须手动保证内存的访问顺序是正确的。

2 访存顺序和写缓存

前面讨论了这么多理论,接下来让我们讨论点实际的内容吧。把write操作缓存到一个队列中(也就是硬件中常常讨论的write buffer)的思想在实践中证明非常有效。因为,store指令往往是多条指令扎堆出现。比如,一个运行MIPS代码的CPU,实际上运行的store指令大约占所有指令的10%左右;但是,往往是突发式访问,比如函数的调用过程中,首先需要压栈操作一组寄存器的值。

但是,一般情况下,写缓存(英文称为write buffer)都是硬件保证的,对于软件来说不用管理。但是,也有一些特殊的情况,程序员需要知道怎样处理:

  1. I/O寄存器访问的时序 这个问题,对于所有架构CPU都存在。比如,CPU发出一个store指令,更新I/O设备寄存器的值,write请求可能会在写缓存中延迟一段时间。这时候,可能会发生其它事件,比如中断。但是此时写入的值还未更新到对应的I/O设备寄存器中。这可能导致一些奇怪的行为:比如,你想禁止产生中断,但是CPU发出write操作之后,CPU还有可能会收到中断。
  2. read操作抢先于write操作执行 上面已经讨论过,MIPS32/64架构允许这种操作。如果想要软件更加健壮和具有可移植性,就不应该假定read和write操作顺序会被保持。如果想要保证前后两个指令周期是按照特定顺序执行,就需要插入sync指令。
  3. 字节汇集 有些写缓存会汇集不足WORD大小的write操作,凑成一个WORD大小的write操作,然后再执行(有些写缓存甚至会攒一个Cache行,然后再写入)。所以,为了避免对于非Cache的内存区也做相同的操作,最好的办法就是把I/O寄存器(比如,一个8位的寄存器)映射到一个单独的WORD大小的地址上。

3 写缓存的flush

通过对非Cache内存区的任意位置执行write操作,然后再read,可以清空写缓存(大部分都是这样实现的)。当然,写缓存不允许read操作发生在write之前,这样导致返回旧值。所以,必须在write和read操作之间,插入sync指令。对于兼容MIPS32/64规范的任何系统,这应该都是有效的。

但是,有效不等于高效。通过提高内存的读写速度也可以降低整体的负荷。有些特定的系统可能会提供更快的内存或者写缓存。

任何具有回写功能的处理器或者内存接口,都引入了写缓存。只是,有的在CPU内部实现,有的在CPU外部实现。不管是在CPU内部,还是在CPU外部,麻烦是相同的。在编程的时候,一定要仔细确认你的系统中,写缓存的位置,善加利用。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-11-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 嵌入式ARM和Linux 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1 内存访问的排序和重新排序
  • 2 访存顺序和写缓存
  • 3 写缓存的flush
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档