在一些讲Java并发编程的书中,经常会出现JMM内存模型、volatile
关键字、重排序、乱序执行等字眼,导致了有些刚开始学习Java并发编程的小伙伴一脸懵逼:这都是啥啊?
文章目录
今天我们就来说一下这些书中提到的一个概念,编译器重排序。
这里先说明一点,下提到的编译器指的是将高级语言编译成汇编语言(机器码)的编译器(比如说GCC、clang、甚至JIT),而不是Javac这种编译成中间语言(JVM字节码)的编译器,当然不同的Java编译器生成的JVM字节码的方式可能也各有不同,但这并不在本文的讨论范围之内。
注:本文实验环境基于Windows10中wsl2下的debian
版本
1. 什么是编译器重排序
1.1 定义
编译器重排序:编译器会对高级语言(本文里特指C/C++)的代码进行分析,当编译器认为你的代码可以优化的话,编译器会选择对代码进行优化然后生成汇编代码,当然编译器的优化满足特定的条件,这里要说一下大名鼎鼎的as-if规则:
Allows any and all code transformations that do not change the observable behavior of the program.
也就是说在不影响这段代码结果的前提下,编译器可以使用任意一种方式对代码进行编译,这也就给了编译器充分的空间对代码进行优化,从而提高代码的运行效率。
1.2 举个例子
下面是一段简单的C语言代码:
int a, b;
void foo(void)
{
a = b + 11;
b = 0;
}
这段代码逻辑很简单,但是对于编译器来说,问题就没有这么简单了。我们来看一下使用aarch64-linux-gnu-gcc
使用-O2
参数,让编译器在O2级别优化的情况下编译上述代码(得到RAM汇编),使用objdump
工具查看foo()
方法反汇编结果:
0000000000000750 <foo>:
750: 90000080 adrp x0, 10000 <__FRAME_END__+0xf6b8>
754: 90000081 adrp x1, 10000 <__FRAME_END__+0xf6b8>
758: f947dc00 ldr x0, [x0, #4024] // 取b内存地址
75c: f947e821 ldr x1, [x1, #4048] // 取a内存地址
760: b9400002 ldr w2, [x0] // 寄存器w2 = b(内存地址)
764: b900001f str wzr, [x0] // b(内存地址) = 0
768: 11002c40 add w0, w2, #0xb // 寄存器w0 = b + 11
76c: b9000020 str w0, [x1] // w0寄存器的值存入a(内存)
770: d65f03c0 ret
774: d503201f nop
我们发现,编译得到的汇编代码和我们原本的C语言代码不顺序并不一致,而是相当于如下C语言代码:
int a, b