Skip to content

Avoid String concatenation for lookup in StaticMessageSource#22451

Closed
stsypanov wants to merge 1 commit into
spring-projects:masterfrom
stsypanov:use-composite-key
Closed

Avoid String concatenation for lookup in StaticMessageSource#22451
stsypanov wants to merge 1 commit into
spring-projects:masterfrom
stsypanov:use-composite-key

Conversation

@stsypanov
Copy link
Copy Markdown
Contributor

Currently StaticMessageSource employs String composed of key and stringified Locale as key in map:

public class StaticMessageSource extends AbstractMessageSource {

	/** Map from 'code + locale' keys to message Strings. */
	private final Map<String, String> messages = new HashMap<>();

	private final Map<String, MessageFormat> cachedMessageFormats = new HashMap<>();

        //...
}

As a result at each call to Map::get/Map::put we have to concat Strings resulting in allocations of StringBuilder and copying char[]. Instead I propose to use separately defined immutable Key.

I've tested behaviour with benchmark

@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class LocaleToStringBenchmark {

  @Benchmark
  public Object concatenation(Data data) {
    return data.code + '_' + data.locale;
  }

  @Benchmark
  public Object compositeKey(Data data) {
    return new Key(data.code, data.locale);
  }

  @Benchmark
  public int hashCodeCompositeString(Data data) {
    return data.compositeString.hashCode();
  }

  @Benchmark
  public int hashCodeCompositeKey(Data data) {
    return data.compositeKey.hashCode();
  }

  @Benchmark
  public boolean equalsCompositeString(Data data) {
    return data.compositeString.equals(data.anotherCompositeString);
  }

  @Benchmark
  public boolean equalsCompositeKey(Data data) {
    return data.compositeKey.equals(data.anotherCompositeKey);
  }

  @State(Scope.Thread)
  public static class Data {
    private final String code = "code1";
    private final Locale locale = Locale.getDefault();

    private final Key compositeKey = new Key(code, locale);
    private final String compositeString = code + '_' + locale;

    private final Key anotherCompositeKey = new Key(code, locale);
    private final String anotherCompositeString = code + '_' + locale;
  }

  private static final class Key {
    private final String code;
    private final Locale locale;

    private Key(String code, Locale locale) {
      this.code = code;
      this.locale = locale;
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) return true;
      if (o == null || getClass() != o.getClass()) return false;

      Key key = (Key) o;

      if (!code.equals(key.code)) return false;
      return locale.equals(key.locale);
    }

    @Override
    public int hashCode() {
      return 31 * code.hashCode() + locale.hashCode();
    }
  }
}

Patched consumes much less memory on key allocation and does it faster, performance of equals/hashCode is mostly the same (tested at work on i7-7700):

JDK 8
                                             Mode  Cnt     Score     Error   Units

compositeKey                                 avgt   25     5,134 ±   0,274   ns/op
concatenation                                avgt   25    67,999 ±   5,872   ns/op

compositeKey:·gc.alloc.rate.norm             avgt   25    24,000 ±   0,001    B/op
concatenation:·gc.alloc.rate.norm            avgt   25   224,000 ±   0,001    B/op

equalsCompositeKey                           avgt   25     3,229 ±   0,161   ns/op
equalsCompositeString                        avgt   25     6,447 ±   0,448   ns/op

hashCodeCompositeKey                         avgt   25     2,584 ±   0,054   ns/op
hashCodeCompositeString                      avgt   25     2,069 ±   0,028   ns/op

JDK 11
                                             Mode  Cnt     Score    Error   Units

compositeKey                                 avgt   25     5,851 ±  0,271   ns/op
concatenation                                avgt   25    61,900 ±  4,406   ns/op

compositeKey:·gc.alloc.rate.norm             avgt   25    24,000 ±  0,001    B/op
concatenation:·gc.alloc.rate.norm            avgt   25   168,000 ±  0,001    B/op

equalsCompositeKey                           avgt   25     3,177 ±  0,254   ns/op
equalsCompositeString                        avgt   25     5,787 ±  0,305   ns/op

hashCodeCompositeKey                         avgt   25     2,947 ±  0,105   ns/op
hashCodeCompositeString                      avgt   25     2,187 ±  0,083   ns/op

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Feb 21, 2019
@jhoeller jhoeller self-assigned this Feb 21, 2019
@jhoeller jhoeller added type: enhancement A general enhancement and removed status: waiting-for-triage An issue we've not yet triaged or decided on labels Feb 21, 2019
@jhoeller jhoeller modified the milestones: 5.2 M1, 5.2 M2 Feb 21, 2019
@jhoeller jhoeller added the in: core Issues in core modules (aop, beans, core, context, expression) label Mar 27, 2019
@jhoeller jhoeller modified the milestones: 5.2 M2, 5.x Backlog Mar 27, 2019
@stsypanov stsypanov changed the title Use separately defined Key as composite key in StaticMessageSource Use Arrays.asList as composite key in StaticMessageSource Aug 21, 2019
@stsypanov
Copy link
Copy Markdown
Contributor Author

@jhoeller I've slightly simplified the patch to use Array.asList

@stsypanov
Copy link
Copy Markdown
Contributor Author

@jhoeller hello, any updates on this?

@jhoeller jhoeller modified the milestones: 5.x Backlog, 5.2.2 Nov 13, 2019
@jhoeller
Copy link
Copy Markdown
Contributor

On review, I'll rather go with a nested Map<String, Map<Locale, ...>> structure here, similar to ReloadableResourceBundleMessageSource. We can even have a single structure with a message holder that contains both the raw message and a lazily resolved MessageFormat instance.

As a side note, part of the original design of StaticMessageSource was to have a human-readable map representation in its toString() output. Rendering a flat map with its concatenated String keys was a nice benefit there; fortunately, even the nested map structure looks alright there now.

I'll repurpose this issue accordingly. Thanks for the PR, in any case!

@jhoeller jhoeller changed the title Use Arrays.asList as composite key in StaticMessageSource Avoid String concatenation for lookup in StaticMessageSource Nov 13, 2019
@jhoeller jhoeller closed this in 3dc5e7b Nov 13, 2019
@stsypanov stsypanov deleted the use-composite-key branch November 14, 2019 08:20
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) type: enhancement A general enhancement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants