从JVM并发看CPU内存指令重排序

简介: 对主存的一次访问一般花费硬件的数百次时钟周期。处理器通过缓存(caching)能够从数量级上降低内存延迟的成本这些缓存为了性能重新排列待定内存操作的顺序。也就是说,程序的读写操作不一定会按照它要求处理器的顺序执行。

这两天,我拜读了 Dennis Byrne 写的一片博文Memory Barriers and JVM Concurrency (中译文内存屏障与JVM并发)。


文中提到:

对主存的一次访问一般花费硬件的数百次时钟周期。处理器通过缓存(caching)能够从数量级上降低内存延迟的成本这些缓存为了性能重新排列待定内存操作的顺序。也就是说,程序的读写操作不一定会按照它要求处理器的顺序执行。


这段话是作者对内存屏障重要性的定义。通过cache降低内存延迟,这句话很好理解。但后面那句“为了性能重排序内存操作顺序”,让没学好微机原理的我倍感疑惑。

CPU为何要重排序内存访问指令?在哪种场景下会触发重排序?作者在文中并未提及。

为了解答疑问,我在网上查阅了一些资料,在这里跟大家分享一下。

 

重排序的背景

我们知道现代CPU的主频越来越高,与cache的交互次数也越来越多。当CPU的计算速度远远超过访问cache时,会产生cache wait,过多的cache wait就会造成性能瓶颈。

针对这种情况,多数架构(包括X86)采用了一种将cache分片的解决方案,即将一块cache划分成互不关联地多个 slots (逻辑存储单元,又名 Memory Bank 或 Cache Bank),CPU可以自行选择在多个 idle bank 中进行存取。这种 SMP 的设计,显著提高了CPU的并行处理能力,也回避了cache访问瓶颈。

Memory Bank的划分
一般 Memory bank 是按cache address来划分的。比如 偶数adress 0×12345000分到 bank 0, 奇数address 0×12345100分到 bank1。

重排序的种类
编译期重排。编译源代码时,编译器依据对上下文的分析,对指令进行重排序,以之更适合于CPU的并行执行。

运行期重排,CPU在执行过程中,动态分析依赖部件的效能,对指令做重排序优化。


实例讲解指令重排序原理

为了方便理解,我们先来看一张CPU内部结构图。

image.png

从图中可以看到,这是一台配备双CPU的计算机,cache 按地址被分成了两块 cache banks,分别是cache bank0cache bank1

理想的内存访问指令顺序:

1,CPU0往cache address 0×12345000 写入一个数字 1。因为address 0×12345000是偶数,所以值被写入 bank0.

2,CPU1读取 bank0 address 0×12345000 的值,即数字1。

3,CPU0往 cache 地址 0×12345100 写入一个数字 2。因为address 0×12345100是奇数,所以值被写入 bank1.

4,CPU1读取 bank1 address 0×12345100 的值,即数字2。

重排序后的内存访问指令顺序:

1,CPU0 准备往 bank0 address 0×12345000 写入数字 1。

2,CPU0检查 bank0 的可用性。发现 bank0 处于 busy 状态。

3, CPU0 为了防止 cache等待,发挥最大效能,将内存访问指令重排序。即先执行后面的 bank1 address 0×12345100 数字2的写入请求。

4,CPU0检查 bank1 可用性,发现bank1处于 idle 状态。

5,CPU0 将数字2写入 bank 1 address 0×12345100。

6,CPU1来读取 0×12345000,未读到 数字1,出错。

7, CPU0 继续检查 bank0 的可用性,发现这次bank0 可用了,然后将数字1写入 0×12345000。

8, CPU1 读取 0×12345100,读到数字2,正确。

从上述触发步骤中,可以看到第 3 步发生了指令重排序,并导致第 6步读到错误的数据。

通过对指令重排,CPU可以获得更快地响应速度,但也给编写并发程序的程序员带来了诸多挑战。

内存屏障是用来防止CPU出现指令重排序的利器之一。

通过这个实例,不知道你对指令重排理解了没有?


不同架构下的指令重排优化

X86仅在 Stores after loadsIncoherent instruction cache pipeline 中会触发重排。

Stores after loads的含义是在对同一个地址进行读写操作时,写入在读取后面,允许重排序。即满足弱一致性(Weak Consistency),这是最可被接受的类型,不会造成太大的影响。

Incoherent instruction cache pipeline是跟JIT相关的类型,作用是在执行self-modifying code 时预防JIT没有flush指令缓存。我不知道该类型跟指令排序有什么关系,既然不在本文涉及范围内,就不做深入探讨了。

参考资料

相关文章
|
16天前
|
Linux
Linux rsyslog占用内存CPU过高解决办法
该文档描述了`rsyslog`占用内存过高的问题及其解决方案。
40 4
|
21天前
|
机器学习/深度学习 缓存 监控
linux查看CPU、内存、网络、磁盘IO命令
`Linux`系统中,使用`top`命令查看CPU状态,要查看CPU详细信息,可利用`cat /proc/cpuinfo`相关命令。`free`命令用于查看内存使用情况。网络相关命令包括`ifconfig`(查看网卡状态)、`ifdown/ifup`(禁用/启用网卡)、`netstat`(列出网络连接,如`-tuln`组合)以及`nslookup`、`ping`、`telnet`、`traceroute`等。磁盘IO方面,`iostat`(如`-k -p ALL`)显示磁盘IO统计,`iotop`(如`-o -d 1`)则用于查看磁盘IO瓶颈。
|
11天前
|
存储 弹性计算 固态存储
阿里云服务器CPU内存配置详细指南,如何选择合适云服务器配置?
阿里云服务器配置选择涉及CPU、内存、公网带宽和磁盘。个人开发者或中小企业推荐使用轻量应用服务器或ECS经济型e实例,如2核2G3M配置,适合低流量网站。企业用户则应选择企业级独享型ECS,如通用算力型u1、计算型c7或通用型g7,至少2核4G配置,公网带宽建议5M,系统盘可选SSD或ESSD云盘。选择时考虑实际应用需求和性能稳定性。
117 6
|
2天前
LabVIEW中CPU和内存使用情况在NI分布式系统管理器中不可见
LabVIEW中CPU和内存使用情况在NI分布式系统管理器中不可见
10 3
|
2天前
|
监控 Linux API
LabVIEW监控实时嵌入式目标上的CPU和内存使用情况
LabVIEW监控实时嵌入式目标上的CPU和内存使用情况
14 4
|
3天前
|
存储 Java 索引
深入浅出JVM(十)之字节码指令(下篇)
深入浅出JVM(十)之字节码指令(下篇)
|
3天前
|
存储 Java 索引
深入浅出JVM(九)之字节码指令(上篇)
深入浅出JVM(九)之字节码指令(上篇)
|
13天前
|
Ubuntu Shell 网络安全
【专栏】在Ubuntu 22.04上安装KubeSphere的指南:确保系统至少有4GB内存和2核CPU,安装Docker和docker-compose
【4月更文挑战第28天】在Ubuntu 22.04上安装KubeSphere的指南:确保系统至少有4GB内存和2核CPU,安装Docker和docker-compose。下载安装脚本`curl -sSL https://kubesphere.io/install.sh | bash`,根据提示选择安装选项,等待完成。安装后,通过访问控制台验证安装效果。解决可能出现的错误、网络问题和性能问题,利用KubeSphere提升容器管理效率。本文为顺利安装和使用提供参考。
|
18天前
|
监控 Shell
Shell脚本监控CPU、内存和硬盘利用率
Shell脚本监控CPU、内存和硬盘利用率
|
25天前
|
存储
CPU的内存分页
CPU的内存分页是一种内存管理机制,旨在优化内存的使用效率和程序的运行效率。在现代计算机系统中,整个虚拟和物理内存空间被切割成固定大小的块,称为页(Page)和帧(Frame)。页用于虚拟地址空间,而帧用于物理内存空间。这些页和帧的大小通常是固定的,比如常见的4KB。 CPU通过内存管理单元(MMU)来实现虚拟地址到物理地址的转换。这个转换过程是通过页表来完成的,页表存储在内存中,并保存了页号与页帧号的映射关系。当CPU需要访问某个虚拟地址时,它会查阅页表,找到对应的物理地址,然后完成内存访问。 内存分页的主要好处有以下几点: 1. **减小换入换出的粒度**:内存分页允许操作系统以更小
18 0