Skip to content

Optimize MultiValueMap iteration operations #29972

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed

Conversation

yuzawa-san
Copy link
Contributor

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:

@Override
@Nullable
public V putIfAbsent(String key, @Nullable V value) {
String oldKey = this.caseInsensitiveKeys.putIfAbsent(convertKey(key), key);
if (oldKey != null) {
V oldKeyValue = this.targetMap.get(oldKey);
if (oldKeyValue != null) {
return oldKeyValue;
}
else {
key = oldKey;
}
}
return this.targetMap.putIfAbsent(key, value);
}

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):
image

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:

HttpHeaders src = new HttpHeaders();
src.set("foo", "bar");
src = HttpHeaders.readOnlyHttpHeaders(src);
src.entrySet().iterator().next().getValue().add("blah") // this should throw since ["bar"] is should be unmodifiable.
assertThat(src.get("foo").size()).isEqualTo(2); // should really be 1?

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.

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Feb 15, 2023
@rstoyanchev rstoyanchev added the in: web Issues in web modules (web, webmvc, webflux, websocket) label Feb 23, 2023
@yuzawa-san yuzawa-san force-pushed the fast-multivaluemap branch from 7bd64ff to e3b2c8f Compare March 2, 2023 23:43
@yuzawa-san
Copy link
Contributor Author

Here are the results of my stress test. This is a portion of BodyInserterRequest.writeTo(), but the savings can be realized all over the place: DefaultRequestBodyUriSpec.init*(), AbstractServerResponse.copy(), ReactorClientHttpRequest.apply*(), and everywhere else in user code where putAll or addAll is used on the headers.

before memory:
image

after memory:
image

before cpu:
image

after cpu:
image

@yuzawa-san yuzawa-san force-pushed the fast-multivaluemap branch from b661a04 to 5cff321 Compare April 4, 2023 00:13
@yuzawa-san
Copy link
Contributor Author

@rstoyanchev would it be possible to triage this? let me know if anything else is needed.

* use forEach and putIfAbsent to copy headers in DefaultClientRequestBuilder
* use forEach in ReactorClientHttpRequest and ReactorNetty2ClientHttpRequest
* circumvent ReadOnlyHttpHeaders.entrySet()
* ensure the fast path to LinkedCaseInsensitiveMap for forEach and putIfAbsent exists
@sbrannen sbrannen added in: core Issues in core modules (aop, beans, core, context, expression) type: enhancement A general enhancement labels Apr 22, 2023
@bclozel bclozel self-assigned this Apr 25, 2023
@bclozel bclozel removed the status: waiting-for-triage An issue we've not yet triaged or decided on label Apr 25, 2023
@bclozel bclozel added this to the 6.0.9 milestone Apr 25, 2023
@bclozel bclozel closed this in 5dacf50 Apr 25, 2023
@bclozel
Copy link
Member

bclozel commented Apr 25, 2023

Thanks for this PR @yuzawa-san and sorry about the delay.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: core Issues in core modules (aop, beans, core, context, expression) in: web Issues in web modules (web, webmvc, webflux, websocket) type: enhancement A general enhancement
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants