Experimenting with GC-less (heap-less) Java
maximullaris.com
maximullaris.com
| Benchmark | Count | Preallocate | Time (ns/op) | Allocation (B/op) | Allocation (op) |
|-----------+-------+-------------+--------------+-------------------+---------------------|
| OnHeap | 10^6 | true | 6231838 | 16000020 | ~ 1 |
| OffHeap | 10^6 | true | 6819142 | 54 | Should also be ~ 1 |
| OnHeap | 10^6 | false | 9719988 | 33554598 | ~ 17 |
| OffHeap | 10^6 | false | 19450448 | 773 | Should also be ~ 17 |
1. The difference between on/off-heap Python-like implementation is less
significant (~ 10%) provided that we preallocate things correctly.2. The off-heap (MemorySegment) implementation slows down as the number of allocation increasing.
EDIT: Profiling with JMH (using -prof async) shows that the off-heap implementation spent quite some time on jdk.internal.misc.Unsafe.getIntUnaligned in its resizeIfNeeded method, which should be the culprit here - Java array are guaranteed to be aligned (at least in HotSpot JVM), and aligned operations are much more efficient than unaligned ones, which should be able to explain the performance hit.
--- 55482970432 ns (18.53%), 5548 samples
[ 0] jdk.internal.misc.Unsafe.getIntUnaligned
[ 1] jdk.internal.misc.ScopedMemoryAccess.getIntUnalignedInternal
[ 2] jdk.internal.misc.ScopedMemoryAccess.getIntUnaligned
[ 3] java.lang.invoke.VarHandleSegmentAsInts.get
[ 4] java.lang.invoke.VarHandleGuards.guard_LJ_I
[ 5] jdk.internal.foreign.AbstractMemorySegmentImpl.get
[ 6] gc_less.python_like.IntHashtableOffHeap.resizeIfNeeded
[ 7] gc_less.python_like.IntHashtableOffHeap.put
[ 8] gc_less.benchmarks.OffHeapBenchmark.testPutOffHeap
[1] https://github.com/openjdk/jmh[2] https://github.com/gudzpoz/gc_less/commit/17dcc3eb57b89f2642...
To avoid allocation churn, one can use a big array of objects and for each index a flag whether the array element contains an object or not. Today I would say I used an arena. Combined with the fly-weight pattern in some use cases you don't need to reach for unsafe at all. For me it was about a visual simulation of chemical reactions. In each frame iteration an array was held and even this long time ago it was fast enough to allow smooth animation with tens of thousands of possibly colliding particles.
I respect research out of curiousity. This way we learn what works and what doesn't work. So keep up this work but I just wanted to tell about the boring alternative.
Typically, you never need to iterate through all used objects. Also, all unused objects are interchangeable from the program’s view. Each just is a chunk of memory that you can use to write a Foo to.
That leads to the standard way to do this for objects that are at least as large as a pointer: make the array an array of union{Foo, Foo*}, and, at allocation, create a linked list through all the unused entries. Getting memory for a new Foo then is just a matter of removing the head of the list, freeing a Foo a matter of appending it to the head of the list.
That means you don’t need n flags (one for each entry, but only a single Foo* pointing to the head of the free list. That’s a big gain if n is large.
(Doing that in Java is left as an exercise. It may not be possible)
[0]: https://github.com/moditect/jfrunit [1]: https://www.morling.dev/blog/towards-continuous-performance-...