缓存带来的挑战?
多核系统下,CUP 一般有一层或者多层的缓存,这些的缓存通过加速数据访问(因为数据距离处理器更近)和降低共享内存在总线上的通讯(因为本地缓存能够满足许多内存操作)来提高CPU性能。缓存能够大大提升性能,但是它们也带来了许多挑战。例如,当两个CPU同时检查相同的内存地址时会发生什么?在什么样的条件下它们会看到相同的值?
内存屏蔽和内存栅栏?
内存屏障或内存栅栏——也就是让一个CPU处理单元中的内存状态对其它处理单元可见的一项技术。
为了达成CPU执行单元的速度要远超主存访问速度, CPU使用了很多优化技术。CPU避免内存访问延迟最常见的技术是将指令管道化,然后尽量重排这些管道的执行以最大利用缓存而把因为缓存未命中引起的延迟降到最小。
一个程序执行时指令是否被重排并不重要,只要最终的结果是一样的。代码顺序并不是真正的执行顺序,CPU和编译器可以各种优化,只要有空间提高性能。缓存和主存的读取会利用load, store和write-combining缓冲区来缓冲和重排。这些缓冲区是查找速度很快的关联队列,当一个后来发生的load需要读取上一个store的值,而该值还没有到达缓存,查找是必需的
。
例如,在一个循环里,如果循环体内没用到这个计数器,循环的计数器什么时候更新(在循环开始,中间还是最后)并不重要。编译器和CPU可以自由的重排指令以最佳的利用CPU,只要下一次循环前更新该计数器即可。并且在循环执行中,这个变量可能一直存在寄存器上,并没有被推到缓存或主存,这样这个变量对其他CPU来说一直都是不可见的。
【内存屏蔽】:
一旦内存数据被推送到缓存,就会有消息协议来确保所有的缓存会对所有的共享数据同步并保持一致。这个使内存数据对CPU核可见的技术被称为内存屏障或内存栅栏。
内存屏障提供了两个功能。首先,它们通过确保从另一个CPU来看屏障的两边的所有指令都是正确的程序顺序,而保持程序顺序的外部可见性;其次它们可以实现内存数据可见性,确保内存数据会同步到CPU缓存子系统。
- Store屏障(Store Barrier),是x86的”sfence“指令,强制所有在store屏障指令之前的store指令,都在该store屏障指令执行之前被执行,并把store缓冲区的数据都刷到CPU缓存。这会使得程序状态对其它CPU可见,这样其它CPU可以根据需要介入。一个实际的好例子是Disruptor中的BatchEventProcessor。当序列Sequence被一个消费者更新时,其它消费者(Consumers)和生产者(Producers)知道该消费者的进度,因此可以采取合适的动作。所以屏障之前发生的内存更新都可见了。
- Load屏障(Load Barrier),是x86上的”ifence“指令,强制所有在load屏障指令之后的load指令,都在该load屏障指令执行之后被执行,并且一直等到load缓冲区被该CPU读完才能执行之后的load指令。这使得从其它CPU暴露出来的程序状态对该CPU可见,这之后CPU可以进行后续处理。一个好例子是上面的BatchEventProcessor的sequence对象是放在屏障后被生产者或消费者使用。
- Full屏障(Full Barrier),是x86上的”mfence“指令,复合了load和save屏障的功能。
内存屏障的性能影响
内存屏障阻碍了CPU采用优化技术来降低内存操作延迟,必须考虑因此带来的性能损失。为了达到最佳性能,最好是把要解决的问题模块化,这样处理器可以按单元执行任务,然后在任务单元的边界放上所有需要的内存屏障。采用这个方法可以让处理器不受限的执行一个任务单元。合理的内存屏障组合还有一个好处是:缓冲区在第一次被刷后开销会减少,因为再填充改缓冲区不需要额外工作了。