Running on Java 24-ea+25-3155 (Preview)
Home of The JavaSpecialists' Newsletter

281Puzzle 2: Is it garbage?

Author: Dr Heinz M. KabutzDate: 2020-06-24Java Version: 8+Category: Performance
 

Abstract: In our next puzzle, we up the ante a bit. We prevent GC during the test() method by storing a strong reference to all our Vectors. Cock and bull story, or are we struggling to identity our biases?

 

Welcome to the 281st edition of The Java(tm) Specialists' Newsletter. Yes, I'm still in Crete, where the sun is shining and the cicadas are finally starting to emerge from the ground. In a few days time we will have their deafening song make all human communication impossible. But it's the sound of summer, and that's a good time of year :-)

javaspecialists.teachable.com: Please visit our new self-study course catalog to see how you can upskill your Java knowledge.

Puzzle 2: Is it garbage?

A special thank you to all my wonderful Java Specialist readers for sending me a lot of great explanations to yesterday's puzzle. Most answers were correct, although there was one subtlety that so far everyone has missed. Here is a follow-up puzzle, but be careful as our identity should not be in our biases, yet it is. Or is it the other way round? Have fun :-)

In our VectorBench2, instead of only storing the Vector inside the ThreadLocal, we also store it inside a shared set. Since Vector does not implement Comparable, we cannot use TreeSet nor ConcurrentSkipListSet. Also, the hashCode() method of Vector is calculated on all the elements and similarly equals() compares the contents. Since a Vector may change, it is not a good idea to use that as a key in any hash set. In our particular case, all the Vectors contain the same elements, thus only one of them would remain in the Set. Neither HashSet nor ConcurrentHashMap.newKeySet() are appropriate sets for our use case. The only set that would work is the IdentityHashMap converted with Collections.newSetFromMap() to a Set. The IdentityHashMap is not thread-safe, but we can solve that problem by wrapping it with Collections.synchronizedMap(). Since we now hold a reference to the Vector until we leave the method, we can be sure that it cannot be somehow garbage collected early. (My dad would have called all this a cock and bull story, but I assure you that it does affect the outcome.)

import java.util.*;
import java.util.stream.*;

public class VectorBench2 {
  public static void main(String... args) {
    for (int i = 0; i < 10; i++) {
      test(false);
      test(true);
    }
  }

  private static void test(boolean parallel) {
    Set<List<Integer>> vectors = Collections.newSetFromMap(
        Collections.synchronizedMap(
            // should not rely on a mutating hashCode()
            new IdentityHashMap<>()
        )
    );
    IntStream range = IntStream.range(1, 100_000_000);
    if (parallel) range = range.parallel();
    long time = System.nanoTime();
    try {
      ThreadLocal<List<Integer>> lists =
          ThreadLocal.withInitial(() -> {
            List<Integer> result = new Vector<>();
            vectors.add(result); // avoid GC during run
            for (int i = 0; i < 1024; i++) result.add(i);
            return result;
          });
      range.map(i -> lists.get().get(i & 1023)).sum();
    } finally {
      time = System.nanoTime() - time;
      System.out.printf("%s %dms%n",
          parallel ? "parallel" : "sequential",
          (time / 1_000_000));
    }
  }
}

The results are now all quite similar:

openjdk version "1.8.0_252"
OpenJDK Runtime Environment (build 1.8.0_252-b14)
OpenJDK 64-Bit Server VM (build 25.252-b14, mixed mode)
sequential 1624ms
parallel 220ms
sequential 1607ms
parallel 214ms
sequential 1641ms
parallel 218ms
sequential 1629ms
parallel 222ms
sequential 1591ms
parallel 202ms
sequential 1612ms
parallel 219ms
sequential 1590ms
parallel 203ms
sequential 1783ms
parallel 233ms
sequential 1880ms
parallel 239ms
sequential 1791ms
parallel 290ms

openjdk version "15-ea" 2020-09-15
OpenJDK Runtime Environment (build 15-ea+23-1098)
OpenJDK 64-Bit Server VM (build 15-ea+23-1098, mixed mode, sharing)
sequential 2125ms
parallel 323ms
sequential 1710ms
parallel 262ms
sequential 2111ms
parallel 233ms
sequential 1578ms
parallel 252ms
sequential 1591ms
parallel 249ms
sequential 1606ms
parallel 235ms
sequential 1567ms
parallel 224ms
sequential 1633ms
parallel 224ms
sequential 1579ms
parallel 229ms
sequential 1584ms
parallel 225ms

openjdk version "15-ea" 2020-09-15
OpenJDK Runtime Environment (build 15-ea+28-1400)
OpenJDK 64-Bit Server VM (build 15-ea+28-1400, mixed mode, sharing)
sequential 1572ms
parallel 244ms
sequential 1522ms
parallel 222ms
sequential 1538ms
parallel 218ms
sequential 1532ms
parallel 216ms
sequential 1499ms
parallel 222ms
sequential 1501ms
parallel 227ms
sequential 1520ms
parallel 217ms
sequential 1471ms
parallel 215ms
sequential 1545ms
parallel 233ms
sequential 1503ms
parallel 220ms

What is going on? Please let me know your guesses by return email. Best explanation gets a mention in the next newsletter :-)

Kind regards from Crete

Heinz

 

Comments

We are always happy to receive comments from our readers. Feel free to send me a comment via email or discuss the newsletter in our JavaSpecialists Slack Channel (Get an invite here)

When you load these comments, you'll be connected to Disqus. Privacy Statement.

Related Articles

Browse the Newsletter Archive

About the Author

Heinz Kabutz Java Conference Speaker

Java Champion, author of the Javaspecialists Newsletter, conference speaking regular... About Heinz

Superpack '23

Superpack '24 Our entire Java Specialists Training in one huge bundle more...

Free Java Book

Dynamic Proxies in Java Book
Java Training

We deliver relevant courses, by top Java developers to produce more resourceful and efficient programmers within their organisations.

Java Consulting

We can help make your Java application run faster and trouble-shoot concurrency and performance bugs...