Optimize (Linked-)HashMap lookup when backing array is null

Claes Redestad claes.redestad at oracle.com
Tue May 19 13:31:42 UTC 2020


Hi,

On 2020-05-19 15:15, Christoph Dreis wrote:
> Hi Claes,
> 
>> unlike CHM, HashMap and LinkedHashMap have constant-time size/isEmpty
>>   accessors - could this be used to simplify your patch?
> 
> I was wondering about that during implementation, but simply took the path I already chose for the CHM.

I think we should try to keep any changes here as simple as possible.

> 
>> So I'd like to see some analysis on microbenchmark that uses a mix of such maps in the same
> 
> I see the following results when I introduce a counter and have it look like that:
> 
>      @Benchmark
>      public String test(ThreadState threadState) {
>          if (threadState.counter++ % 2 == 0) {
>              return threadState.emptyMap.get(threadState.key);
>          }
>          return threadState.map.get(threadState.key);
>      }
> 
> Old
> Benchmark                             Mode  Cnt   Score    Error   Units
> MyBenchmark.test                      avgt   10   4,916 ±  0,106   ns/op
> MyBenchmark.test:·gc.alloc.rate       avgt   10  ≈ 10⁻⁴           MB/sec
> MyBenchmark.test:·gc.alloc.rate.norm  avgt   10  ≈ 10⁻⁶             B/op
> MyBenchmark.test:·gc.count            avgt   10     ≈ 0           counts
> 
> New patched
> MyBenchmark.test                      avgt   10   4,493 ±  0,169   ns/op
> MyBenchmark.test:·gc.alloc.rate       avgt   10  ≈ 10⁻⁴           MB/sec
> MyBenchmark.test:·gc.alloc.rate.norm  avgt   10  ≈ 10⁻⁶             B/op
> MyBenchmark.test:·gc.count            avgt   10     ≈ 0           counts
> 
> Was that what you had in mind?

Something like that, yes.

Thanks!

/Claes

> 
> Cheers,
> Christoph
> 
> Am 19.05.20, 14:47 schrieb "Claes Redestad" <claes.redestad at oracle.com>:
> 
>      Hi Christoph,
> 
>      unlike CHM, HashMap and LinkedHashMap have constant-time size/isEmpty
>      accessors - could this be used to simplify your patch?
> 
>      It's easy to get heavily biased results in microbenchmarks when all you
>      do is repeatedly calling down one path. That is, JIT might speculatively
>      assume all HashMaps are empty or non-empty if all you do is call a
>      method on only empty or non-empty maps respectively. So I'd like to see
>      some analysis on microbenchmark that uses a mix of such maps in the same
>      @Benchmark
> 
>      /Claes
> 
>      On 2020-05-19 14:22, Christoph Dreis wrote:
>      > Hi,
>      >
>      > similar to JDK-8244960[1] that I reported last week, I noticed that HashMap & LinkedHashMap could benefit from a similar improvement.
>      >
>      > I used the following benchmark again to validate the results:
>      >
>      >
>      > @BenchmarkMode(Mode.AverageTime)
>      > @OutputTimeUnit(TimeUnit.NANOSECONDS)
>      > public class MyBenchmark {
>      >
>      >      @State(Scope.Benchmark)
>      >      public static class ThreadState {
>      >
>      >          private Map<TestKey, String> map = new HashMap<>();
>      >          private TestKey key = new TestKey(Collections.singleton("test"));
>      >
>      >          /*
>      >          public ThreadState() {
>      >              this.map.put(key, "test");
>      >          }
>      >          */
>      >      }
>      >
>      >      @Benchmark
>      >      public String test(ThreadState threadState) {
>      >          return threadState.map.get(threadState.key);
>      >      }
>      >
>      > }
>      >
>      > Where TestKey is the following:
>      >
>      > public class TestKey {
>      >
>      > 	private final Set<String> params;
>      >
>      > 	public TestKey(Set<String> params) {
>      > 		this.params = params;
>      > 	}
>      >
>      > 	@Override
>      > 	public int hashCode() {
>      > 		return this.params.hashCode();
>      > 	}
>      >
>      > }
>      >
>      > Applying the (hopefully) attached patch I see the following results:
>      >
>      > Patched
>      > Benchmark                             Mode  Cnt   Score    Error   Units
>      > MyBenchmark.test                      avgt   10   2,717 ±  0,247   ns/op
>      > MyBenchmark.test:·gc.alloc.rate       avgt   10  ≈ 10⁻⁴           MB/sec
>      > MyBenchmark.test:·gc.alloc.rate.norm  avgt   10  ≈ 10⁻⁶             B/op
>      > MyBenchmark.test:·gc.count            avgt   10     ≈ 0           counts
>      >
>      > Old
>      > Benchmark                             Mode  Cnt   Score    Error   Units
>      > MyBenchmark.test                      avgt   10   3,713 ±  0,091   ns/op
>      > MyBenchmark.test:·gc.alloc.rate       avgt   10  ≈ 10⁻⁴           MB/sec
>      > MyBenchmark.test:·gc.alloc.rate.norm  avgt   10  ≈ 10⁻⁶             B/op
>      > MyBenchmark.test:·gc.count            avgt   10     ≈ 0           counts
>      >
>      > The case when the map is already filled didn't seem to show any regression.
>      >
>      > Unfortunately, there is the caveat of potentially executing the hash() method twice in computeIfPresent if the remapping function returns null and the node is removed. I don't know if this case is really common (or more common than an empty map), but I should mention it for completeness reasons.
>      >
>      > One common case for the above scenario is the following: I noticed that in a typical Spring-Boot app Manifest.getTrustedAttributes or Manifest.getEntries() is actually empty. Since this is used during class loading it is executed relatively frequent. I could imagine other use-cases where this might be benefitial for startup scenarios.
>      >
>      > In case you think this is worthwhile, I would highly appreciate a sponsoring of the attached patch.
>      >
>      > Cheers,
>      > Christoph
>      >
>      >
>      > [1] https://bugs.openjdk.java.net/browse/JDK-8244960
>      >
>      >
> 
> 


More information about the core-libs-dev mailing list