perf(java): replace stream+flatMap with pre-sized for-loop in ArrayTransformUtils#5631
perf(java): replace stream+flatMap with pre-sized for-loop in ArrayTransformUtils#5631ajzach wants to merge 3 commits intovalkey-io:mainfrom
Conversation
…ansformUtils Seven methods in ArrayTransformUtils used stream().flatMap(entry -> Stream.of(key, value)).toArray() to build alternating key-value arrays. This pattern allocates one Stream object per map entry (via flatMap) plus internal SpinedBuffer infrastructure since the final size is unknown. Replace with a single pre-sized array and a plain for-loop over entrySet(). The output array is allocated once at exact capacity (map.size() * 2), and no intermediate objects are created. flattenAllKeysFollowedByAllValues additionally dropped two ArrayList instances, two intermediate toArray() calls, and a concatenateArrays() stream — replaced with a single-pass index-offset write into one array. JMH benchmark results (AverageTime, gc profiler, 2 forks x 10 iterations): Method (size=50) BEFORE AFTER alloc BEFORE alloc AFTER convertMapToKeyValueGlideStringArray 1029 ns/op 170 ns/op 6976 B/op 416 B/op (-94%) convertMapToKeyValueStringArray 1207 ns/op 176 ns/op 6976 B/op 416 B/op (-94%) flattenMapToGlideStringArray 1245 ns/op 181 ns/op 6976 B/op 416 B/op (-94%) flattenMapToGlideStringArrayValueFirst 992 ns/op 186 ns/op 6976 B/op 416 B/op (-94%) flattenAllKeysFollowedByAllValues 743 ns/op 190 ns/op 3840 B/op 416 B/op (-89%) convertMapToValueKeyStringArray 2153 ns/op 968 ns/op 9376 B/op 2816 B/op (-70%) convertMapToValueKeyStringArrayBinary 3302 ns/op 1533 ns/op 12576 B/op 6016 B/op (-52%) The residual allocations after the change are exactly the output array (e.g. new GlideString[100] with compressed oops = 416 bytes for size=50). The remaining allocations in the Value*Key methods are unavoidable Double.toString() / gs() object creation per entry. These methods are called on every mset, hset, zadd, xadd, geoadd, and other bulk commands, so the GC pressure reduction is proportional to command throughput. Adds unit tests for all modified methods and a JMH benchmark module (me.champeau.jmh plugin) under benchmarks/ for future regression tracking. Signed-off-by: Ariel Zach <[email protected]>
ec89654 to
96dfec7
Compare
|
Sorry. This is reviewing here. #5603 |
This PR applies the same optimization pattern as #5603 (
Results were measured with JMH + GC profiler (2 forks × 10 iterations) to have quantitative evidence before and after each change. Allocation reductions range from −52% to −94% depending on the method. If #5603 merges first, happy to rebase and drop the two overlapping methods. |
| keysList.add(GlideString.of(entry.getKey())); | ||
| valuesList.add(GlideString.of(entry.getValue())); | ||
| result[i] = GlideString.of(entry.getKey()); | ||
| result[i + size] = GlideString.of(entry.getValue()); |
There was a problem hiding this comment.
This seems alot better than concatenating afterwards.
jduo
left a comment
There was a problem hiding this comment.
Please address the cases where the key and value are null (technically HashMap allows a null key). Add tests for these scenarios.
java/benchmarks/src/jmh/java/glide/benchmarks/ArrayTransformBenchmark.java
Outdated
Show resolved
Hide resolved
- Null keys and values now propagate as null elements in the result array instead of throwing NullPointerException implicitly - Add 8 unit tests covering null key and null value cases for the four representative methods - Fix stale JavaDoc/comment in ArrayTransformBenchmark that referenced the old flatMap+Stream implementation Signed-off-by: Ariel Zach <[email protected]> Signed-off-by: Ariel Zach <[email protected]>
b1e597e to
301aabd
Compare
|
@jduo Addressed in the latest commit. For all 7 methods:
Added 8 unit tests covering null key and null value for the representative methods. |
Add nullKey/nullValue tests for convertMapToValueKeyStringArray and convertMapToValueKeyStringArrayBinary to match coverage of other methods. Signed-off-by: Ariel Zach <[email protected]>
618e686 to
a8b446e
Compare
Summary
Seven methods in
ArrayTransformUtilsusedstream().flatMap(entry -> Stream.of(key, value)).toArray()to interleave map entries into a flat array. This pattern allocates oneStreamobject per entry (viaflatMap) plusSpinedBufferinfrastructure since the final array size is unknown to the stream pipeline.Replaced with a single pre-sized array (
map.size() * 2) and a plainforloop overentrySet()— allocates exactly one array, zero intermediate objects.flattenAllKeysFollowedByAllValuesadditionally dropped twoArrayListinstances, two intermediatetoArray()calls, and aconcatenateArrays()stream call — replaced with a single-pass index-offset write into one array.These methods are called on every
mset,hset,zadd,xadd,geoadd, and other bulk commands, so the GC pressure reduction scales directly with command throughput.Benchmark results
JMH,
AverageTime, GC profiler, 2 forks × 10 iterations,mapSize= 50:convertMapToKeyValueGlideStringArrayconvertMapToKeyValueStringArrayflattenMapToGlideStringArrayflattenMapToGlideStringArrayValueFirstflattenAllKeysFollowedByAllValuesconvertMapToValueKeyStringArrayconvertMapToValueKeyStringArrayBinaryThe 416 B/op residual is exactly the output array (
new GlideString[100]with compressed oops = 416 bytes for 50 entries). The remaining allocations in theValue*Keymethods are unavoidableDouble.toString()/gs()object creation per entry.Test plan
ArrayTransformUtilsTestwith 15 unit tests covering all modified methods (basic, empty, single-entry, size invariant)ArrayTransformBenchmarkvia JMH plugin (me.champeau.jmh) underbenchmarks/for future regression tracking — run with./gradlew :benchmarks:jmh