Optimize MultiValueMap iteration operations #29972
Closed
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
I was doing some flame graph analysis and found that ReadOnlyHttpHeaders.entrySet() was a small hotspot in my codebase. I looked into its implementation and found that some care was taken in the past to fully copy over the entrySet to into a new and properly ordered set. The main hotspot in my specific codebase was in BodyInserterRequest.writeTo() which was called every time I used the reactive WebClient. I found that the entrySet() could be avoided and replaced with a forEach with putIfAbsent. The putIfAbsent is particularly useful since in the primary implementation I found it has to hash/normalize keys only once instead of twice like it was doing currently:
spring-framework/spring-core/src/main/java/org/springframework/util/LinkedCaseInsensitiveMap.java
Lines 209 to 223 in b233163
In order to make sure the fast path is taken, I had to make sure the whole call chain avoided the Map class' default implementations (which use the slow entrySet() or a naive putIfAbsent). this allowed us to get to LinkedCaseInsensitiveMap's implementations. The forEach is optimized in LinkedHashMap as well. This foregoes the need to create entrySets and iterators (all with immutability) which saves memory allocations. Here is a stack trace from a breakpoint in LinkedCaseInsensitiveMap.putIfAbsent() (note how the fast path is taken):
Some other minor optimizations were included: presizing list in MultiValueMapAdapter.addAll, use forEach in implementation of MultiValueMapAdapter.addAll (this avoids that slow entrySet path)
ASIDE: I think I found a bug in the ReadOnlyHttpHeaders.entrySet(). It appears that it leaks a mutable value list. See the following code:
That code currently works, but should it? I have a fix, but I'll save that for a separate PR if you want.
I can also look into adding forEach implementations in things like NettyHeadersAdapter later.