This PR is a redesign of subtype checking. 

The implementation of subtype checking in the HotSpot JVM is now twenty years 
old. There have been some performance-related bugs reported, and the only way 
to fix them is a redesign of the way it works.

So what's changed, so that the old design should be replaced?

Firstly, the computers of today aren't the computers of twenty years ago. It's 
not merely a matter of speed: the systems are much more parallel, both in the 
sense of having more cores and each core can run many instructions in parallel. 
Because of this, the speed ratio between memory accesses and the rate at which 
we can execute instructions has become wider and wider.

The most severe reported problem is to do with the "secondary supers cache". 
This is a 1-element per-class cache for interfaces (and arrays of interfaces). 
Unfortunately, if two threads repeatedly update this cache, the result is that 
a cache line ping-pongs between cores, causing a severe slowdown. 

Also, the linear search for an interface that is absent means that the entire 
list of interfaces has to be scanned. This plays badly with newer language 
features such as JEP 406, pattern matching for switch.

However, the computers of today can help us. The very high 
instruction-per-cycle rate of a Great Big Out-Of-Order (GBOOO) processor allows 
us to execute many of the instructions of a hash table lookup in parallel, as 
long as we avoid dependencies between instructions. 

The solution
------------

We use a hashed lookup of secondary supers. This is a 64-way hash table, with 
linear probing for collisions. The table is compressed, in that null entries 
are removed, and the resulting hash table fits into the same secondary supers 
array as today's unsorted array of secondary supers. This means that existing 
code in HotSpot that simply does a linear scan of the secondary supers array 
does not need to be altered.

We add a bitmap field to each Klass object. This bitmap contains an occupancy 
bit corresponding to each element of the hash table, with a 1 indicating 
element presence. As well as allowing the hash table to be decompressed, this 
bimap is used as a simple kind of Bloom Filter. To determine whether a 
superclass is present, we simply have to check a single bit in the bitmap. If 
the bit is clear, we know that the superclass is not present. If the bit is 
set, we have to do a little arithmetic and then consult the hash table.

It works like this:


       mov    sub_klass, [& sub_klass->_bitmap]
       mov    array_index, sub_klass
       shl    array_index, <hash code of class we're looking for>
  ╭    jns    not_found 
  │    popcnt array_index,array_index
  │    mov    array_base, [& sub_klass->_secondary_supers]
  │    cmp    super_klass, [array_base+array_index*8]
  │    je     found


The popcount instruction returns the cardinality of a bitset. By shifting out 
the bits higher in number than the hash code of the element we're looking for, 
we leave only the lower bits. `popcount`, then, gives us the index of the 
element we're looking for.

If we don't get a match at the first attempt, we test the next bit in the 
bitset and jump to a fallback stub:


  │    bt     sub_klass, <next hash code>
  │╭   jae    not_found
  ││   ror    sub_klass, <hash code>
  ││   call   Stub::klass_subtype_fallback
  ││   je     found
  ↘↘


Collisions are rare. Vladimir Ivanov did a survey of Java benchmark code, and 
it is very rare to see more than about 20 super-interfaces, and even that gives 
us a collision rate of only about 0.25.

The time taken for a positive lookup is somewhere between 3 - 6 cycles, or 
about 0.9 - 1.8 ns. This is a robust figure, confirmed across current AArch64 
and x86 designs, and this rate can be sustained indefinitely. Negative lookups 
are slightly faster because there's usually no need to consult the secondary 
super cache, at about 3 - 5 cycles.

The current secondary super cache lookup is usually slightly faster than a hash 
table for positive lookups, at about 3 - 4 cycles, but it performs badly with 
negative lookups, and unless the class you're looking for is in the secondary 
super cache it performs badly as well. For example, a negative lookup in a 
class with 4 interfaces takes 10 - 47 cycles.

Limitations and disadvantages
-----------------------------

This PR is a subset of what is possible. It is only implemented for C2, in the 
cases where the superclass is a constant, known at compile time. Given that 
this is almost all uses of subtype checking, it's not really a restriction.

I would have liked to save the hashed interfaces list in the CDS archive file, 
but there are test cases for the Serviceability Agent that assume the 
interfaces are in the order in which they are declared. I think the tests are 
probably just wrong about this. For the same reason, I have to make copies of 
the interfaces array and sort the copies, rather than just using the same array 
at runtime.

I haven't removed the secondary super cache. It's still used by the interpreter 
and C2. 

In the future I'd like to delete the secondary super cache, but it is used in 
many places across the VM. Today is not that day.

Performance testing
-------------------

Hashing the secondary supers arrays takes a little time. I've added a perf 
counter for this, so you can see. It's only really a few milliseconds added to 
startup.

I have run Renaissance and SPECjvm, and whatever speed differences there may be 
are immeasurably small, down in the noise. The benchmark in this class allows 
you to isolate single instanceof invocations with various sets of secondary 
superclasses.

Finally
-------

Vladimir Ivanov was very generous with his time and his advice. He explained 
the problem and went into detail about his experiments, and shared with me his 
experimental code. This saved me a great deal of time in this work.

-------------

Commit messages:
 - Copyright dates
 - Merge branch 'JDK-8180450' of https://github.com/theRealAph/jdk into 
JDK-8180450
 - JDK-8180450: secondary_super_cache does not scale well
 - JDK-8180450: secondary_super_cache does not scale well
 - JDK-8180450: secondary_super_cache does not scale well
 - Merge branch 'JDK-8180450' of https://github.com/theRealAph/jdk into 
JDK-8180450
 - JDK-8180450: secondary_super_cache does not scale well
 - Comment typo
 - JDK-8180450: secondary_super_cache does not scale well
 - JDK-8180450: secondary_super_cache does not scale well
 - ... and 38 more: https://git.openjdk.org/jdk/compare/2482a505...a0677b71

Changes: https://git.openjdk.org/jdk/pull/18309/files
  Webrev: https://webrevs.openjdk.org/?repo=jdk&pr=18309&range=00
  Issue: https://bugs.openjdk.org/browse/JDK-8180450
  Stats: 1314 lines in 29 files changed: 1288 ins; 0 del; 26 mod
  Patch: https://git.openjdk.org/jdk/pull/18309.diff
  Fetch: git fetch https://git.openjdk.org/jdk.git pull/18309/head:pull/18309

PR: https://git.openjdk.org/jdk/pull/18309

Reply via email to