我可以通过字节码编译日志检测指令重新排序吗?
Can i detect instructions reordering by bytecode compilation log?
我有下一个代码示例:
class Shared {
int x;
int y;
void increment() {
x++;
y++;
}
void check() {
if (y > x) {
System.out.println("Ooops! y > x");
}
}
}
看清楚了吗?但是当我尝试递增并签入两个线程时,主要问题发生在这里:
Shared shared = new Shared();
Thread writer = new Thread(() -> {
for (int i = 0; i < N; i++) {
shared.increment();
}
});
Thread reader = new Thread(() -> {
for (int i = 0; i < N; i++) {
shared.check();
}
});
writer.start();
reader.start();
您会注意到数据竞争(在某些情况下指令重新排序?):
1. x++;
2. y++;
现在,我知道了特殊的 VM 标志,它可以帮助我打印 JIT 编译器日志 (-XX:+PrintCompilation
)。
...
120 181 3 Shared::increment (21 bytes)
120 182 3 Shared::check (20 bytes)
120 183 4 Shared::increment (21 bytes)
120 184 4 Shared::check (20 bytes)
121 181 3 Shared::increment (21 bytes) made not entrant
121 182 3 Shared::check (20 bytes) made not entrant
121 185 n 0 java.lang.invoke.MethodHandle::linkToStatic(L)L (native) (static)
121 184 4 Shared::check (20 bytes) made not entrant
122 186 3 Shared::check (20 bytes)
122 187 n 0 java.lang.Object::clone (native)
122 188 4 Shared::check (20 bytes)
122 189 % 3 Main::lambda$main[=13=] @ 2 (19 bytes)
122 190 3 Main::lambda$main[=13=] (19 bytes)
123 186 3 Shared::check (20 bytes) made not entrant
...
好的,现在我可以看到增量方法的编译是如何处理的了:
120 181 3 Shared::increment (21 bytes)
120 183 4 Shared::increment (21 bytes)
121 181 3 Shared::increment (21 bytes) made not entrant
我理解正确吗,这里重新排序是由于 tiered compilation?因为 increment()
- 热方法,JIT 编译器分析此信息并使用 C2
服务器编译器。而且,正如我认为的那样,以这种方式重新排序一些指令,但在某些情况下会发生优化(made not entrant
)。或者说错了?
此外,还有一些编译日志:
138 182 2 Shared::increment (21 bytes)
138 184 4 Shared::increment (21 bytes)
138 182 2 Shared::increment (21 bytes) made not entrant
这与分层编译无关。没有它也会发生问题。让 JVM 只编译一个方法 check
看看它在 C2 编译代码中的样子:
java -XX:-TieredCompilation \
-XX:CompileCommand=compileonly,Shared::check \
-XX:CompileCommand=print,Shared::check \
Shared
输出为
0x00000000031a4160: mov dword ptr [rsp+0ffffffffffffa000h],eax
0x00000000031a4167: push rbp
0x00000000031a4168: sub rsp,20h ;*synchronization entry
; - Shared::check@-1 (line 11)
(1) 0x00000000031a416c: mov r10d,dword ptr [rdx+0ch]
;*getfield x
; - Shared::check@5 (line 11)
(2) 0x00000000031a4170: mov r8d,dword ptr [rdx+10h] ;*getfield y
; - Shared::check@1 (line 11)
0x00000000031a4174: cmp r8d,r10d
0x00000000031a4177: jnle 31a4185h ;*if_icmple
; - Shared::check@8 (line 11)
0x00000000031a4179: add rsp,20h
0x00000000031a417d: pop rbp
0x00000000031a417e: test dword ptr [1020000h],eax
; {poll_return}
0x00000000031a4184: ret
如您所见,x
先加载(第 1 行),y
在后加载(第 2 行)。在这些行之间,另一个线程可能会增加 y
任意次数,从而使 y
看起来大于 x
.
在这种特殊情况下,您已经猜到相对于原始程序顺序(字节码 getfield y
在 getfield x
之前)对加载进行重新排序。但是,正如@Andreas 所提到的,这并不是程序可能中断的唯一原因。即使 JIT 编译器在 load(x)
之前发出 load(y)
,取决于 CPU 架构,也可能会发生第一次加载获得较新值,而第二次加载获得较旧值的情况,这从 JMM 的角度来看绝对正确。
我有下一个代码示例:
class Shared {
int x;
int y;
void increment() {
x++;
y++;
}
void check() {
if (y > x) {
System.out.println("Ooops! y > x");
}
}
}
看清楚了吗?但是当我尝试递增并签入两个线程时,主要问题发生在这里:
Shared shared = new Shared();
Thread writer = new Thread(() -> {
for (int i = 0; i < N; i++) {
shared.increment();
}
});
Thread reader = new Thread(() -> {
for (int i = 0; i < N; i++) {
shared.check();
}
});
writer.start();
reader.start();
您会注意到数据竞争(在某些情况下指令重新排序?):
1. x++;
2. y++;
现在,我知道了特殊的 VM 标志,它可以帮助我打印 JIT 编译器日志 (-XX:+PrintCompilation
)。
...
120 181 3 Shared::increment (21 bytes)
120 182 3 Shared::check (20 bytes)
120 183 4 Shared::increment (21 bytes)
120 184 4 Shared::check (20 bytes)
121 181 3 Shared::increment (21 bytes) made not entrant
121 182 3 Shared::check (20 bytes) made not entrant
121 185 n 0 java.lang.invoke.MethodHandle::linkToStatic(L)L (native) (static)
121 184 4 Shared::check (20 bytes) made not entrant
122 186 3 Shared::check (20 bytes)
122 187 n 0 java.lang.Object::clone (native)
122 188 4 Shared::check (20 bytes)
122 189 % 3 Main::lambda$main[=13=] @ 2 (19 bytes)
122 190 3 Main::lambda$main[=13=] (19 bytes)
123 186 3 Shared::check (20 bytes) made not entrant
...
好的,现在我可以看到增量方法的编译是如何处理的了:
120 181 3 Shared::increment (21 bytes)
120 183 4 Shared::increment (21 bytes)
121 181 3 Shared::increment (21 bytes) made not entrant
我理解正确吗,这里重新排序是由于 tiered compilation?因为 increment()
- 热方法,JIT 编译器分析此信息并使用 C2
服务器编译器。而且,正如我认为的那样,以这种方式重新排序一些指令,但在某些情况下会发生优化(made not entrant
)。或者说错了?
此外,还有一些编译日志:
138 182 2 Shared::increment (21 bytes)
138 184 4 Shared::increment (21 bytes)
138 182 2 Shared::increment (21 bytes) made not entrant
这与分层编译无关。没有它也会发生问题。让 JVM 只编译一个方法 check
看看它在 C2 编译代码中的样子:
java -XX:-TieredCompilation \
-XX:CompileCommand=compileonly,Shared::check \
-XX:CompileCommand=print,Shared::check \
Shared
输出为
0x00000000031a4160: mov dword ptr [rsp+0ffffffffffffa000h],eax
0x00000000031a4167: push rbp
0x00000000031a4168: sub rsp,20h ;*synchronization entry
; - Shared::check@-1 (line 11)
(1) 0x00000000031a416c: mov r10d,dword ptr [rdx+0ch]
;*getfield x
; - Shared::check@5 (line 11)
(2) 0x00000000031a4170: mov r8d,dword ptr [rdx+10h] ;*getfield y
; - Shared::check@1 (line 11)
0x00000000031a4174: cmp r8d,r10d
0x00000000031a4177: jnle 31a4185h ;*if_icmple
; - Shared::check@8 (line 11)
0x00000000031a4179: add rsp,20h
0x00000000031a417d: pop rbp
0x00000000031a417e: test dword ptr [1020000h],eax
; {poll_return}
0x00000000031a4184: ret
如您所见,x
先加载(第 1 行),y
在后加载(第 2 行)。在这些行之间,另一个线程可能会增加 y
任意次数,从而使 y
看起来大于 x
.
在这种特殊情况下,您已经猜到相对于原始程序顺序(字节码 getfield y
在 getfield x
之前)对加载进行重新排序。但是,正如@Andreas 所提到的,这并不是程序可能中断的唯一原因。即使 JIT 编译器在 load(x)
之前发出 load(y)
,取决于 CPU 架构,也可能会发生第一次加载获得较新值,而第二次加载获得较旧值的情况,这从 JMM 的角度来看绝对正确。