JVM Anatomy Park
shipilev.net
shipilev.net
This reminds me when the Java Pub House did a few podcasts on Garbage Collections and performance (http://www.javapubhouse.com/2012/04/episode-22-garbage-man-i...)
Would also love details on the author's workflow and automation behind this.
" for (...) { synchronized (obj) { // something } }
…could it optimize into this?
synchronized (this) { for (...) { <----XXX---- // something } } "
The answer would be no in general, I think, since it is unsafe.
Moving the entire loop into the synchronized block would make the statement marked XXX above, i.e. "for (...)" execute inside the synchronized block, which has the potential to change the semantics (for example, the statement may include an rpc, and we don't want to make that rpc under the lock).
The earlier optimization of
synchronized (this) { statement1; }
synchronized (this) { statement2; }
to
synchronized (this) { statement1; statement2; }
is safe.
That's not change in semantics. The JVM doesn't provide any guarantees about parallelism or scheduling semantics in the first place so how can you possibly argue about the semantics of holding a lock too long?
And it's unsafe? For what definition of safety?
But in practice - yes you obviously wouldn't do this if the code in the loop control code had any side effects.
Let me try to answer with an example. Let us say, we have original code like this:
synchronized(this) {
a();
b();
}
c();
synchronized(this) {
d();
e();
}
It would be unsafe (in general) to transform the above code to synchronized(this) {
a();
b();
c();
d();
e();
}
Simply because Compiler does not know (again, in general) what may happen during the execution of c(). However, the following transformation is safe (the timing behavior changes, but as you point out, that is not a guarantee programmers should expect). synchronized(this) {
p();
q();
}
synchronized(this) {
r();
s();
}
to synchronized(this) {
p();
q();
r();
s();
}
since there is nothing happening between the two synchronized sections.For the for loop, an example of code where pulling the synchronized statement out of the loop is problematic:
for(a = AcquireLock(), c = 0; c < 100; c++) {
synchronized(this) {
f();
}
}
ReleaseLock(a);
I don't think it is safe to transform to a = AcquireLock();
synchronized(this) {
for(c = 0; c < 100; c++) {
f();
}
}
ReleaseLock(a);
Perhaps you (and the original blog post) assume we are only talking about movement of code after ensuring that such movement is safe, but it was not clear from the document. for(a = AcquireLock(), c = 0; c < 100; c++) {
synchronized(this) {
f();
}
}
ReleaseLock(a);
Per definition of the for-loop, that's the same as: a = AcquireLock();
c = 0;
while (c < 100) {
synchronized(this) { f(); }
c++;
}
ReleaseLock(a);
From there, you can deduce: 'c < 100' and 'c++' don't depend on a and have no side effects besides c. Furthermore, there is no guarantee about two consecutive synchronized blocks actually releasing the lock. Thus, you can safely lift the synchronized block from the loop. However, as AcquireLock and ReleaseLock are function calls with unknown behavior, you can't coarsen the lock further in your example.Interestingly enough, if you'd talk about 'f(c = 0, g(); c < 100; c++)', the optimizations above might be impossible because you don't know if the call to g modifies c. Unless you can inline g, so you can re-arrange instructions again.
Let me change the example a bit. Say we have two locks aL and bL, that we must always acquire in the order aL first and then bL.
Following the rule, say we write code like this:
import java.util.concurrent.locks.ReentrantLock;
class X {
private static ReentrantLock aL = new ReentrantLock();
private static ReentrantLock bL = new ReentrantLock();
static int x = 0;
static int c = 0;
static public void main(String[] args) {
for(aL.lock(); c < 100; c++) {
synchronized(bL) {
x = x + 0x42;
}
}
aL.unlock();
}
}
If I understood it right, the blog post was asking a question whether JVM can transform this to: import java.util.concurrent.locks.ReentrantLock;
class X {
private static ReentrantLock aL = new ReentrantLock();
private static ReentrantLock bL = new ReentrantLock();
static int x = 0;
static int c = 0;
static public void main(String[] args) {
synchronized(bL) {
for(aL.lock(); c < 100; c++) {
x = x + 0x42;
}
aL.unlock();
} // end synnchronized
}
}
Since the locks are now acquired in a different order, does that not qualify as observable behavior? synchronized(this) {
a = AcquireLock();
for(c = 0; c < 100; c++) {
f();
}
}
ReleaseLock(a);
which is inline with what the blog post was proposing.To repeat the blog is a question:
for (...) {
synchronized (obj) {
// something
}
}
…could it optimize into this? synchronized (this) {
for (...) {
// something
}
}
My answer to that is in general, no.Yes, but I think it's an assumption so obvious as to be not worth stating that the author means as long as ... does not do that.
Because '...' can include arbitrary side-effect inducing statements that can't be moved around without affecting the behavior. As the poster discovered.
I do agree that it's not a "safe" optimization in the extreme general case (so don't go rewriting your code assuming it's equivalent!), but in the case where the loop is a candidate for unrolling it works just fine. Imagine you had a more CPU- or memory-bound workload, and these benchmarks are a whole lot more interesting.
Put another way: if there's an RPC call in the for loop, the time spent in the RPC will dwarf the work involved in executing loop itself so ... odds are good it's not going to be a candidate for unrolling anyways. :)
Consider the unoptimized version. It's possible that after you release the lock, another thread will take it. But I'm pretty confident there is no guarantee of this, and your own thread might immediately take the lock back. (Probably often, especially if there's just one CPU.) If taking the lock back immediately causes a deadlock, then your code is already broken as written.
So you already must account for the possibility that nothing changes between the time you release the lock and take it again. All the optimization does is take away the _other_ possibility, which was something you couldn't rely on anyway.
The set of possible executions of a correctly optimized program should be a subset of---and specifically, need not be necessarily equal to---the possible executions of the original unoptimized version.
I don't even know what sense of the word is intended. Is it "park" like a city or amusement park that you visit, and the idea is that you enjoy a variety of different activities while you're there? Is it "park" like a ballpark, and the analogy is that sporting events take place there and each one of these topics is an event? Is it somehow "park" like a parking lot? Maybe "park" is a too-literal translation from another language where the corresponding word has a meaning that the English one doesn't?
I'm trying to guess based on context, but none of these really seems to fit.