Hacker News new | past | comments | ask | show | jobs | submit login
JVM Anatomy Park (shipilev.net)
356 points by r4um on Jan 7, 2018 | hide | past | web | favorite | 32 comments



I really enjoy the completeness of this publication, additionally the ability for me to read it later from an epub is great.

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...)


Agreed. I would love to see this become the standard for longer blog posts.

Would also love details on the author's workflow and automation behind this.


Regarding the question in the 1st topic: (https://shipilev.net/jvm-anatomy-park/1-lock-coarsening-for-...)

" 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.


> 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).

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.


>And it's unsafe? For what definition of safety?

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.


Look at it in smaller transformations. Compiler optimizations are kinda like algebra - many small structured steps result in hard, large changes.

  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.


Just looking at your final example, I can't understand how you can say that is unsafe. How would anyone be able to tell the difference between the two? I think anything you tell me I'm just going to be able to answer 'but Java never guaranteed you that in the first place'. If nobody can tell the difference then how can it be unsafe?


>Just looking at your final example, I can't understand how you can say that is unsafe

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?


But that's just a different example to the one you gave before. In your previous example acquiring the explicit lock always came before the start of synchronised block, both before and after the rewrite. You've changed it here so it's a different question.


Sorry, I meant to write:

  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.


You mean because ... could be code that can detect whether or not the monitor is held?

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.


> My answer to that is in general, no.

Because '...' can include arbitrary side-effect inducing statements that can't be moved around without affecting the behavior. As the poster discovered.


> The answer would be no in general, I think, since it is unsafe. > ... > for example, the statement may include an rpc, and we don't want to make that rpc under the lock

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. :)


Neither of those optimizations looks safe to me. Couldn't they potentially introduce deadlocks?


I had the same reaction, but now I'm starting to suspect it actually is safe, for a reason that's not immediately obvious.

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.


Yes, you're right. The definition of an optimization being ``safe'' is tricky in the presence of non-determinism.

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.


This is of no real consequence except curiosity, but I'm at a loss to understand what "park" means in the title.

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.


Maybe it is a reference to the Rick and Morty episode "Anatomy Park". http://rickandmorty.wikia.com/wiki/Anatomy_Park_(episode)


Oh, that has to be it! So it's just a reference I didn't understand.


Could be as in wildlife park.


Besides being interesting in itself, this is the best collection of examples I've seen for JMH. When I looked last the official documentation was a bit thin, and most articles on it just included an unmotivated hello world example.


Why PDF is so large?


7k views, 155 upvotes, 1 comment...


I peek at articles all the time without upvoting, and upvote articles without commenting all the time.


I was skeptical at first as well, but your own stats show a different picture: the site gets a lot of views, which correlates with the upvotes. No comments is probably due to the fact that people use upvoting as a mechanism to save links to material (which might be of use later)


To clarify, I don't think this is improper use of the upvote mechanism, I was just hoping for more engagement with the comment feature of Hacker News, given the 7k click thrus.


That's just what you've achieved :-) Probably not the way you intended it, though


Ironically, more comments about your meta-comment.


Half the upvotes are probably for the Rick and Morty reference.


My take: not everyone has time to read this right now. I use upvote to "bookmark", and I expect not everyone here use Java. I am not, but I intend to give it a round at some point.


How do you know it has 7K views?





Applications are open for YC Winter 2020

Guidelines | FAQ | Support | API | Security | Lists | Bookmarklet | Legal | Apply to YC | Contact

Search: