Skip to content

Commit 6ad621e

Browse files
XenoAmessGoogle Java Core Libraries
authored and
Google Java Core Libraries
committedMar 22, 2022
Adjust the capacity computation in Maps.newHashMapWithExpectedSize.
[This JDK change](openjdk/jdk@3e39304) fixed the computation used when the first write to a `HashMap` is `putAll(otherMap)`. Previously that would occasionally lead to an unnecessarily large internal table. That same computation was copied into `Maps.newHashMapWithExpectedSize`, so it should also be adjusted. Thanks to @XenoAmess for both the JDK fix and the fix here. Closes #5965. RELNOTES=`Maps.newHashMapWithExpectedSize` sometimes allocated maps that were larger than they needed to be. PiperOrigin-RevId: 436464386
1 parent 7e04a00 commit 6ad621e

File tree

4 files changed

+66
-12
lines changed

4 files changed

+66
-12
lines changed
 

‎android/guava-tests/test/com/google/common/collect/MapsTest.java

+20-2
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,10 @@ public void testNewHashMapWithExpectedSize_wontGrow() throws Exception {
126126

127127
for (int size = 1; size < 200; size++) {
128128
assertWontGrow(
129-
size, Maps.newHashMapWithExpectedSize(size), Maps.newHashMapWithExpectedSize(size));
129+
size,
130+
new HashMap<>(),
131+
Maps.newHashMapWithExpectedSize(size),
132+
Maps.newHashMapWithExpectedSize(size));
130133
}
131134
}
132135

@@ -139,14 +142,19 @@ public void testNewLinkedHashMapWithExpectedSize_wontGrow() throws Exception {
139142
for (int size = 1; size < 200; size++) {
140143
assertWontGrow(
141144
size,
145+
new LinkedHashMap<>(),
142146
Maps.newLinkedHashMapWithExpectedSize(size),
143147
Maps.newLinkedHashMapWithExpectedSize(size));
144148
}
145149
}
146150

147151
@GwtIncompatible // reflection
148152
private static void assertWontGrow(
149-
int size, HashMap<Object, Object> map1, HashMap<Object, Object> map2) throws Exception {
153+
int size,
154+
HashMap<Object, Object> referenceMap,
155+
HashMap<Object, Object> map1,
156+
HashMap<Object, Object> map2)
157+
throws Exception {
150158
// Only start measuring table size after the first element inserted, to
151159
// deal with empty-map optimization.
152160
map1.put(0, null);
@@ -168,6 +176,16 @@ private static void assertWontGrow(
168176
assertWithMessage("table size after adding " + size + " elements")
169177
.that(bucketsOf(map1))
170178
.isEqualTo(initialBuckets);
179+
180+
// Ensure that referenceMap, which doesn't use WithExpectedSize, ends up with the same table
181+
// size as the other two maps. If it ended up with a smaller size that would imply that we
182+
// computed the wrong initial capacity.
183+
for (int i = 0; i < size; i++) {
184+
referenceMap.put(i, null);
185+
}
186+
assertWithMessage("table size after adding " + size + " elements")
187+
.that(initialBuckets)
188+
.isAtMost(bucketsOf(referenceMap));
171189
}
172190

173191
@GwtIncompatible // reflection

‎android/guava/src/com/google/common/collect/Maps.java

+13-4
Original file line numberDiff line numberDiff line change
@@ -237,10 +237,19 @@ static int capacity(int expectedSize) {
237237
return expectedSize + 1;
238238
}
239239
if (expectedSize < Ints.MAX_POWER_OF_TWO) {
240-
// This is the calculation used in JDK8 to resize when a putAll
241-
// happens; it seems to be the most conservative calculation we
242-
// can make. 0.75 is the default load factor.
243-
return (int) ((float) expectedSize / 0.75F + 1.0F);
240+
// This seems to be consistent across JDKs. The capacity argument to HashMap and LinkedHashMap
241+
// ends up being used to compute a "threshold" size, beyond which the internal table
242+
// will be resized. That threshold is ceilingPowerOfTwo(capacity*loadFactor), where
243+
// loadFactor is 0.75 by default. So with the calculation here we ensure that the
244+
// threshold is equal to ceilingPowerOfTwo(expectedSize). There is a separate code
245+
// path when the first operation on the new map is putAll(otherMap). There, prior to
246+
// https://github.com/openjdk/jdk/commit/3e393047e12147a81e2899784b943923fc34da8e, a bug
247+
// meant that sometimes a too-large threshold is calculated. However, this new threshold is
248+
// independent of the initial capacity, except that it won't be lower than the threshold
249+
// computed from that capacity. Because the internal table is only allocated on the first
250+
// write, we won't see copying because of the new threshold. So it is always OK to use the
251+
// calculation here.
252+
return (int) Math.ceil(expectedSize / 0.75);
244253
}
245254
return Integer.MAX_VALUE; // any large value
246255
}

‎guava-tests/test/com/google/common/collect/MapsTest.java

+20-2
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,10 @@ public void testNewHashMapWithExpectedSize_wontGrow() throws Exception {
126126

127127
for (int size = 1; size < 200; size++) {
128128
assertWontGrow(
129-
size, Maps.newHashMapWithExpectedSize(size), Maps.newHashMapWithExpectedSize(size));
129+
size,
130+
new HashMap<>(),
131+
Maps.newHashMapWithExpectedSize(size),
132+
Maps.newHashMapWithExpectedSize(size));
130133
}
131134
}
132135

@@ -139,14 +142,19 @@ public void testNewLinkedHashMapWithExpectedSize_wontGrow() throws Exception {
139142
for (int size = 1; size < 200; size++) {
140143
assertWontGrow(
141144
size,
145+
new LinkedHashMap<>(),
142146
Maps.newLinkedHashMapWithExpectedSize(size),
143147
Maps.newLinkedHashMapWithExpectedSize(size));
144148
}
145149
}
146150

147151
@GwtIncompatible // reflection
148152
private static void assertWontGrow(
149-
int size, HashMap<Object, Object> map1, HashMap<Object, Object> map2) throws Exception {
153+
int size,
154+
HashMap<Object, Object> referenceMap,
155+
HashMap<Object, Object> map1,
156+
HashMap<Object, Object> map2)
157+
throws Exception {
150158
// Only start measuring table size after the first element inserted, to
151159
// deal with empty-map optimization.
152160
map1.put(0, null);
@@ -168,6 +176,16 @@ private static void assertWontGrow(
168176
assertWithMessage("table size after adding " + size + " elements")
169177
.that(bucketsOf(map1))
170178
.isEqualTo(initialBuckets);
179+
180+
// Ensure that referenceMap, which doesn't use WithExpectedSize, ends up with the same table
181+
// size as the other two maps. If it ended up with a smaller size that would imply that we
182+
// computed the wrong initial capacity.
183+
for (int i = 0; i < size; i++) {
184+
referenceMap.put(i, null);
185+
}
186+
assertWithMessage("table size after adding " + size + " elements")
187+
.that(initialBuckets)
188+
.isAtMost(bucketsOf(referenceMap));
171189
}
172190

173191
@GwtIncompatible // reflection

‎guava/src/com/google/common/collect/Maps.java

+13-4
Original file line numberDiff line numberDiff line change
@@ -284,10 +284,19 @@ static int capacity(int expectedSize) {
284284
return expectedSize + 1;
285285
}
286286
if (expectedSize < Ints.MAX_POWER_OF_TWO) {
287-
// This is the calculation used in JDK8 to resize when a putAll
288-
// happens; it seems to be the most conservative calculation we
289-
// can make. 0.75 is the default load factor.
290-
return (int) ((float) expectedSize / 0.75F + 1.0F);
287+
// This seems to be consistent across JDKs. The capacity argument to HashMap and LinkedHashMap
288+
// ends up being used to compute a "threshold" size, beyond which the internal table
289+
// will be resized. That threshold is ceilingPowerOfTwo(capacity*loadFactor), where
290+
// loadFactor is 0.75 by default. So with the calculation here we ensure that the
291+
// threshold is equal to ceilingPowerOfTwo(expectedSize). There is a separate code
292+
// path when the first operation on the new map is putAll(otherMap). There, prior to
293+
// https://github.com/openjdk/jdk/commit/3e393047e12147a81e2899784b943923fc34da8e, a bug
294+
// meant that sometimes a too-large threshold is calculated. However, this new threshold is
295+
// independent of the initial capacity, except that it won't be lower than the threshold
296+
// computed from that capacity. Because the internal table is only allocated on the first
297+
// write, we won't see copying because of the new threshold. So it is always OK to use the
298+
// calculation here.
299+
return (int) Math.ceil(expectedSize / 0.75);
291300
}
292301
return Integer.MAX_VALUE; // any large value
293302
}

0 commit comments

Comments
 (0)
Please sign in to comment.