Skip to content

Commit 176846f

Browse files
committed
Optimizing putAll into a new empty map
Optimizing the common case of doing putAll into a new empty map / clone Moved existing putAll code into putAllMerge Added putAllIntoEmptyMap which is special case of putAll for the common empty destination case (used by clone) putAll now picks between putAllIntoEmptyMap and putAllMerge based on the map size
1 parent 266b36d commit 176846f

File tree

1 file changed

+97
-62
lines changed
  • internal-api/src/main/java/datadog/trace/api

1 file changed

+97
-62
lines changed

internal-api/src/main/java/datadog/trace/api/TagMap.java

Lines changed: 97 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -30,16 +30,10 @@
3030
* primitive. By using immutable entries, the entry objects can be shared between builders & maps
3131
* freely.
3232
*
33-
* <p>This map lacks some features of a regular java.util.Map...
34-
*
35-
* <ul>
36-
* <li>Entry object mutation
37-
* <li>size tracking - falls back to computeSize
38-
* <li>manipulating Map through the entrySet() or values()
39-
* </ul>
40-
*
41-
* <p>Also lacks features designed for handling large maps...
33+
* <p>This map lacks the ability to mutate an entry via @link {@link Entry#setValue(Object)}.
34+
* Entries must be replaced by re-setting / re-putting the key which will create a new Entry object.
4235
*
36+
* <p>This map also lacks features designed for handling large long lived mutable maps...
4337
* <ul>
4438
* <li>bucket array expansion
4539
* <li>adaptive collision
@@ -52,7 +46,7 @@
5246
* When there is only a single Entry in a particular bucket, the Entry is stored into the bucket directly.
5347
* <p>
5448
* Because the Entry objects can be shared between multiple TagMaps, the Entry objects cannot contain
55-
* form a link list to handle collisions.
49+
* directly form a link list to handle collisions.
5650
* <p>
5751
* Instead when multiple entries collide in the same bucket, a BucketGroup is formed to hold multiple entries.
5852
* But a BucketGroup is only formed when a collision occurs to keep allocation low in the common case of no collisions.
@@ -61,7 +55,7 @@
6155
* to hold the additional Entry-s. And the BucketGroup-s are connected via a linked list instead of the Entry-s.
6256
* <p>
6357
* This does introduce some inefficiencies when Entry-s are removed.
64-
* In the current system, given that removals are rare, BucketGroups are never consolidated.
58+
* The assumption is that removals are rare, so BucketGroups are never consolidated.
6559
* However as a precaution if a BucketGroup becomes completely empty, then that BucketGroup will be
6660
* removed from the collision chain.
6761
*/
@@ -125,44 +119,10 @@ public final int size() {
125119
return this.size;
126120
}
127121

128-
final int computeSize() {
129-
Object[] thisBuckets = this.buckets;
130-
131-
int size = 0;
132-
for (int i = 0; i < thisBuckets.length; ++i) {
133-
Object curBucket = thisBuckets[i];
134-
135-
if (curBucket instanceof Entry) {
136-
size += 1;
137-
} else if (curBucket instanceof BucketGroup) {
138-
BucketGroup curGroup = (BucketGroup) curBucket;
139-
size += curGroup.sizeInChain();
140-
}
141-
}
142-
return size;
143-
}
144-
145122
@Override
146123
public final boolean isEmpty() {
147124
return (this.size == 0);
148125
}
149-
150-
final boolean checkIfEmpty() {
151-
Object[] thisBuckets = this.buckets;
152-
153-
for (int i = 0; i < thisBuckets.length; ++i) {
154-
Object curBucket = thisBuckets[i];
155-
156-
if (curBucket instanceof Entry) {
157-
return false;
158-
} else if (curBucket instanceof BucketGroup) {
159-
BucketGroup curGroup = (BucketGroup) curBucket;
160-
if (!curGroup.isEmptyChain()) return false;
161-
}
162-
}
163-
164-
return true;
165-
}
166126

167127
@Override
168128
public boolean containsKey(Object key) {
@@ -399,7 +359,8 @@ public final void putAll(Map<? extends String, ? extends Object> map) {
399359
this.putAll((TagMap) map);
400360
} else {
401361
for (Map.Entry<? extends String, ?> entry : map.entrySet()) {
402-
this.put(entry.getKey(), entry.getValue());
362+
// use set which returns a prior Entry rather put which may box a prior primitive value
363+
this.set(entry.getKey(), entry.getValue());
403364
}
404365
}
405366
}
@@ -412,21 +373,29 @@ public final void putAll(Map<? extends String, ? extends Object> map) {
412373
* source TagMap
413374
*/
414375
public final void putAll(TagMap that) {
415-
this.checkWriteAccess();
416-
376+
this.checkWriteAccess();
377+
378+
if ( this.size == 0 ) {
379+
this.putAllIntoEmptyMap(that);
380+
} else {
381+
this.putAllMerge(that);
382+
}
383+
}
384+
385+
private final void putAllMerge(TagMap that) {
417386
Object[] thisBuckets = this.buckets;
418387
Object[] thatBuckets = that.buckets;
419388

420389
// Since TagMap-s don't support expansion, buckets are perfectly aligned
390+
// Check against both thisBuckets.length && thatBuckets.length is to help the JIT do bound check elimination
421391
for (int i = 0; i < thisBuckets.length && i < thatBuckets.length; ++i) {
422-
Object thatBucket = thatBuckets[i];
423-
424-
// nothing in the other hash, just skip this bucket
425-
if (thatBucket == null) continue;
392+
Object thatBucket = thatBuckets[i];
393+
394+
// if nothing incoming, nothing to do
395+
if ( thatBucket == null ) continue;
426396

427397
Object thisBucket = thisBuckets[i];
428-
429-
if (thisBucket == null) {
398+
if (thisBucket == null) {
430399
// This bucket is null, easy case
431400
// Either copy over the sole entry or clone the BucketGroup chain
432401

@@ -475,19 +444,21 @@ public final void putAll(TagMap that) {
475444
// there's already an entry w/ the same tag from the incoming TagMap
476445
// incoming entry clobbers the existing try, so we're done
477446
thisBuckets[i] = thisNewGroup;
478-
this.size +=
479-
thisNewGroupSize
480-
- 1; // overlapping group - subtract one for clobbered existing entry
447+
448+
// overlapping group - subtract one for clobbered existing entry
449+
this.size += thisNewGroupSize - 1;
481450
} else if (thisNewGroup.insertInChain(thisHash, thisEntry)) {
482451
// able to add thisEntry into the existing groups
483452
thisBuckets[i] = thisNewGroup;
484-
this.size +=
485-
thisNewGroupSize; // non overlapping group - existing entry already accounted for
453+
454+
// non overlapping group - existing entry already accounted for in this.size
455+
this.size += thisNewGroupSize;
486456
} else {
487457
// unable to add into the existing groups
488458
thisBuckets[i] = new BucketGroup(thisHash, thisEntry, thisNewGroup);
489-
this.size +=
490-
thisNewGroupSize; // non overlapping group - existing entry already accounted for
459+
460+
// non overlapping group - existing entry already accounted for in this.size
461+
this.size += thisNewGroupSize;
491462
}
492463
}
493464
} else if (thisBucket instanceof BucketGroup) {
@@ -523,6 +494,32 @@ public final void putAll(TagMap that) {
523494
}
524495
}
525496
}
497+
498+
/*
499+
* Optimized special case method for the common case of putAll into an empty TagMap used by copy, etc
500+
*/
501+
private final void putAllIntoEmptyMap(TagMap that) {
502+
Object[] thisBuckets = this.buckets;
503+
Object[] thatBuckets = that.buckets;
504+
505+
// Check against both thisBuckets.length && thatBuckets.length is to help the JIT do bound check elimination
506+
for (int i = 0; i < thisBuckets.length && i < thatBuckets.length; ++i) {
507+
Object thatBucket = thatBuckets[i];
508+
509+
// faster to explicitly null check first, then do instanceof
510+
if ( thatBucket == null ) {
511+
// do nothing
512+
} else if ( thatBucket instanceof BucketGroup ) {
513+
// if it is a BucketGroup, then need to clone
514+
BucketGroup thatGroup = (BucketGroup)thatBucket;
515+
516+
thisBuckets[i] = thatGroup.cloneChain();
517+
} else { // if ( thatBucket instanceof Entry )
518+
thisBuckets[i] = thatBucket;
519+
}
520+
}
521+
this.size = that.size;
522+
}
526523

527524
public final void fillMap(Map<? super String, Object> map) {
528525
Object[] thisBuckets = this.buckets;
@@ -615,7 +612,7 @@ public final Entry removeEntry(String tag) {
615612
/** Returns a mutable copy of this TagMap */
616613
public final TagMap copy() {
617614
TagMap copy = new TagMap();
618-
copy.putAll(this);
615+
copy.putAllIntoEmptyMap(this);
619616
return copy;
620617
}
621618

@@ -745,6 +742,10 @@ public final void checkWriteAccess() {
745742
}
746743

747744
final void checkIntegrity() {
745+
// Decided to use if ( cond ) throw new IllegalStateException rather than assert
746+
// That was done to avoid the extra static initialization needed for an assertion
747+
// While that's probably an unnecessary optimization, this method is only called in tests
748+
748749
Object[] thisBuckets = this.buckets;
749750

750751
for ( int i = 0; i < thisBuckets.length; ++i ) {
@@ -789,6 +790,40 @@ final void checkIntegrity() {
789790
}
790791
}
791792

793+
final int computeSize() {
794+
Object[] thisBuckets = this.buckets;
795+
796+
int size = 0;
797+
for (int i = 0; i < thisBuckets.length; ++i) {
798+
Object curBucket = thisBuckets[i];
799+
800+
if (curBucket instanceof Entry) {
801+
size += 1;
802+
} else if (curBucket instanceof BucketGroup) {
803+
BucketGroup curGroup = (BucketGroup) curBucket;
804+
size += curGroup.sizeInChain();
805+
}
806+
}
807+
return size;
808+
}
809+
810+
final boolean checkIfEmpty() {
811+
Object[] thisBuckets = this.buckets;
812+
813+
for (int i = 0; i < thisBuckets.length; ++i) {
814+
Object curBucket = thisBuckets[i];
815+
816+
if (curBucket instanceof Entry) {
817+
return false;
818+
} else if (curBucket instanceof BucketGroup) {
819+
BucketGroup curGroup = (BucketGroup) curBucket;
820+
if (!curGroup.isEmptyChain()) return false;
821+
}
822+
}
823+
824+
return true;
825+
}
826+
792827
@Override
793828
public final String toString() {
794829
return toPrettyString();

0 commit comments

Comments
 (0)