-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Small Tags performance improvements #6185
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -71,11 +71,20 @@ private static boolean isSortedSet(Tag[] tags, int length) { | |
| if (length > tags.length) { | ||
| return false; | ||
| } | ||
|
|
||
| // This style is intentionally chosen to have only one array | ||
| // access per iteration (tags[i].compareTo(tags[i + 1]) would | ||
| // have two). | ||
| Tag current = tags[0]; | ||
| Tag next; | ||
|
|
||
| for (int i = 0; i < length - 1; i++) { | ||
| int cmp = tags[i].compareTo(tags[i + 1]); | ||
| next = tags[i + 1]; | ||
| int cmp = current.compareTo(next); | ||
| if (cmp >= 0) { | ||
| return false; | ||
| } | ||
| current = next; | ||
| } | ||
| return true; | ||
| } | ||
|
|
@@ -96,7 +105,8 @@ private static Tags toTags(Tag[] tags) { | |
| } | ||
|
|
||
| /** | ||
| * Removes duplicate tags from an ordered array of tags. | ||
| * Removes duplicate tags from an ordered array of tags. In the case of several | ||
| * consecutive tags with the same key, only the last one is preserved. | ||
| * @param tags an ordered array of {@code Tag} objects. | ||
| * @return the number of unique tags in the {@code tags} array after removing | ||
| * duplicates. | ||
|
|
@@ -111,9 +121,24 @@ private static int dedup(Tag[] tags) { | |
| // index of next unique element | ||
| int j = 0; | ||
|
|
||
| for (int i = 0; i < n - 1; i++) | ||
| if (!tags[i].getKey().equals(tags[i + 1].getKey())) | ||
| // The following is intentionally written in this style to facilitate performance. | ||
| // Normally one would just do tags[i].getKey().equals(tags[i + 1].getKey()), | ||
| // but the tags[i + 1].getKey() value is exactly the same as tags[i].getKey() | ||
| // for the next iteration, thus caching it in a variable halves the number | ||
| // of lookups. | ||
| // The compiler is very unlikely to do this for us because of the absence of | ||
| // "this data is immutable" signs, even if MM doesn't enforce any HB | ||
| // relations with external modifications in this case. You can run the | ||
| // associated benchmarks to check the behavior for your specific JVM. | ||
| String current = tags[0].getKey(); | ||
| String next; | ||
|
|
||
| for (int i = 0; i < n - 1; i++) { | ||
| next = tags[i + 1].getKey(); | ||
| if (!current.equals(next)) | ||
| tags[j++] = tags[i]; | ||
| current = next; | ||
| } | ||
|
|
||
| tags[j++] = tags[n - 1]; | ||
| return j; | ||
|
|
@@ -126,12 +151,14 @@ private static int dedup(Tag[] tags) { | |
| * @return a {@code Tags} instance with the merged sets of tags. | ||
| */ | ||
| private Tags merge(Tags other) { | ||
| if (other.length == 0) { | ||
| return this; | ||
| if (length == 0) { | ||
| return other; | ||
| } | ||
| if (Objects.equals(this, other)) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Object.equals is basically a null-safe version of calling equals directly, and the first thing this code was doing is an access to |
||
|
|
||
| if (other.length == 0 || tagsEqual(other)) { | ||
| return this; | ||
| } | ||
|
|
||
| Tag[] sortedSet = new Tag[this.length + other.length]; | ||
| int sortedIndex = 0; | ||
| int thisIndex = 0; | ||
|
|
@@ -215,13 +242,12 @@ public Tags and(@Nullable Tag... tags) { | |
| * @return a new {@code Tags} instance | ||
| */ | ||
| public Tags and(@Nullable Iterable<? extends Tag> tags) { | ||
| if (tags == null || tags == EMPTY || !tags.iterator().hasNext()) { | ||
| return this; | ||
| } | ||
|
|
||
| if (this.length == 0) { | ||
| return Tags.of(tags); | ||
| } | ||
|
|
||
| // Tags.of() will take care of nulls, empty iterables and so on | ||
| // merge() then will check if the argument is empty and reduce to no-op | ||
| return merge(Tags.of(tags)); | ||
| } | ||
|
|
||
|
|
@@ -276,7 +302,7 @@ public int hashCode() { | |
|
|
||
| @Override | ||
| public boolean equals(@Nullable Object obj) { | ||
| return this == obj || obj != null && getClass() == obj.getClass() && tagsEqual((Tags) obj); | ||
| return this == obj || (obj != null && getClass() == obj.getClass() && tagsEqual((Tags) obj)); | ||
| } | ||
|
|
||
| private boolean tagsEqual(Tags obj) { | ||
|
|
@@ -323,16 +349,19 @@ public static Tags concat(@Nullable Iterable<? extends Tag> tags, @Nullable Stri | |
| * @return a new {@code Tags} instance | ||
| */ | ||
| public static Tags of(@Nullable Iterable<? extends Tag> tags) { | ||
| if (tags == null || tags == EMPTY || !tags.iterator().hasNext()) { | ||
| return Tags.empty(); | ||
| } | ||
| else if (tags instanceof Tags) { | ||
| if (tags instanceof Tags) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since EMPTY is instanceof Tags, it will fall in this branch as well. Instanceof requires one extra memory lookup though, so idk. Null is also recognized only at the emptyIterable stage, but this shouldn't bring additional cost, i don't expect JIT to try to check for null thrice |
||
| return (Tags) tags; | ||
| } | ||
| else if (tags instanceof Collection) { | ||
| Collection<? extends Tag> tagsCollection = (Collection<? extends Tag>) tags; | ||
| if (tagsCollection.isEmpty()) { | ||
| return Tags.empty(); | ||
| } | ||
| return toTags(tagsCollection.toArray(EMPTY_TAG_ARRAY)); | ||
| } | ||
| else if (emptyIterable(tags)) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. emptyIterable was used more widely in a version that hasn't reached publication, maybe we want to get rid of it completely. There are some repeated checks, compiler should be able to pick up invariants during inlining, but i've never explicitly checked. |
||
| return Tags.empty(); | ||
| } | ||
| else { | ||
| return toTags(StreamSupport.stream(tags.spliterator(), false).toArray(Tag[]::new)); | ||
| } | ||
|
|
@@ -346,7 +375,7 @@ else if (tags instanceof Collection) { | |
| * @return a new {@code Tags} instance | ||
| */ | ||
| public static Tags of(String key, String value) { | ||
| return new Tags(new Tag[] { Tag.of(key, value) }, 1); | ||
| return of(Tag.of(key, value)); | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -373,6 +402,26 @@ private static boolean blankVarargs(@Nullable Object[] args) { | |
| return args == null || args.length == 0 || (args.length == 1 && args[0] == null); | ||
| } | ||
|
|
||
| private static boolean emptyIterable(@Nullable Iterable<? extends Tag> iterable) { | ||
| // Doing the checks in the ascending cost order | ||
| if (iterable == null || iterable == EMPTY) { | ||
| return true; | ||
| } | ||
|
|
||
| if (iterable instanceof Tags) { | ||
| return ((Tags) iterable).length == 0; | ||
| } | ||
|
|
||
| if (iterable instanceof Collection) { | ||
| return ((Collection<?>) iterable).isEmpty(); | ||
| } | ||
|
|
||
| // While the compiler can theoretically avoid Iterator allocation here | ||
| // (via scalarization), it is not guaranteed, thus leaving this check as the last | ||
| // one | ||
| return !iterable.iterator().hasNext(); | ||
| } | ||
|
|
||
| /** | ||
| * Return a new {@code Tags} instance containing tags constructed from the specified | ||
| * tags. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've extracted dedup to a separate method to see that in isolation i can reduce ~1000 ns to ~750 ns on workstation by this (not sure how many elements i was feeding in, either 64 or 128). I haven't tested explicitly the same above, but both can be dropped if you feel this is too much.