diff --git a/android/guava-tests/benchmark/com/google/common/base/StringUtilJmhBenchmark.java b/android/guava-tests/benchmark/com/google/common/base/StringUtilJmhBenchmark.java new file mode 100644 index 000000000000..41a0e48758fd --- /dev/null +++ b/android/guava-tests/benchmark/com/google/common/base/StringUtilJmhBenchmark.java @@ -0,0 +1,233 @@ +package com.google.common.base; + +import static java.util.concurrent.TimeUnit.NANOSECONDS; + +import java.io.IOException; +import java.util.HashSet; +import java.util.Set; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; + +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(NANOSECONDS) +@State(Scope.Benchmark) +@Fork(5) +public class StringUtilJmhBenchmark { + @Param({"100", "1000"}) + private int length; + + @Param({"ASCII", "CJK", "SPECIAL"}) + private String type; + + private String input; + + @Setup + public void setUp() { + StringBuilder sb = new StringBuilder(length); + if ("SPECIAL".equals(type)) { + int[] specialChars = { + 'a', + 0xAD, + 0x600, + 0x202A, + 0xFFF9, + 0x1D173, + 0xE0001, // In the set + 0x101, + 0x700, + 0x3001, + 0x1D172, + 0xE0002 // Not in the set + }; + for (int i = 0; i < length; i++) { + sb.appendCodePoint(specialChars[i % specialChars.length]); + } + } else { + for (int i = 0; i < length; i++) { + if ("ASCII".equals(type)) { + sb.append((char) ('a' + (i % 26))); + } else { + sb.append((char) (0x4e00 + (i % 1000))); // Chinese ideographs + } + } + } + input = sb.toString(); + } + + @Benchmark + public String escapeUnicodeCurrent() { + return StringUtil.javaScriptEscape(input); + } + + @Benchmark + public String escapeUnicodeOriginal() { + return originalJavaScriptEscape(input, false); + } + + @Benchmark + public String escapeAsciiCurrent() { + return StringUtil.javaScriptEscapeToAscii(input); + } + + @Benchmark + public String escapeAsciiOriginal() { + return originalJavaScriptEscape(input, true); + } + + private static String originalJavaScriptEscape(CharSequence s, boolean escapeToAscii) { + StringBuilder sb = new StringBuilder(s.length() * 9 / 8); + try { + originalEscapeStringBody(s, escapeToAscii, StringUtil.JsEscapingMode.EMBEDDABLE_JS, sb); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + return sb.toString(); + } + + private static void originalEscapeStringBody( + CharSequence plainText, + boolean escapeToAscii, + StringUtil.JsEscapingMode jsEscapingMode, + Appendable out) + throws IOException { + int pos = 0; + int len = plainText.length(); + for (int codePoint, charCount, i = 0; i < len; i += charCount) { + codePoint = Character.codePointAt(plainText, i); + charCount = Character.charCount(codePoint); + + if (!originalShouldEscapeChar(codePoint, escapeToAscii, jsEscapingMode)) { + continue; + } + + out.append(plainText, pos, i); + pos = i + charCount; + switch (codePoint) { + case '\b': + out.append("\\b"); + break; + case '\t': + out.append("\\t"); + break; + case '\n': + out.append("\\n"); + break; + case '\f': + out.append("\\f"); + break; + case '\r': + out.append("\\r"); + break; + case '\\': + out.append("\\\\"); + break; + case '"': + case '\'': + if (jsEscapingMode == StringUtil.JsEscapingMode.JSON && codePoint == '\'') { + out.append((char) codePoint); + break; + } else if (jsEscapingMode != StringUtil.JsEscapingMode.EMBEDDABLE_JS) { + out.append('\\').append((char) codePoint); + break; + } + // fall through + default: + if (codePoint >= 0x100 || jsEscapingMode == StringUtil.JsEscapingMode.JSON) { + appendUnicode(codePoint, out); + } else { + appendHex((char) codePoint, out); + } + break; + } + } + out.append(plainText, pos, len); + } + + private static boolean originalShouldEscapeChar( + int codePoint, boolean escapeToAscii, StringUtil.JsEscapingMode jsEscapingMode) { + if (escapeToAscii && (codePoint < 0x20 || codePoint > 0x7e)) { + return true; + } + return ORIGINAL_JS_ESCAPE_CHARS.contains(codePoint); + } + + private static final char[] hexChars = "0123456789abcdef".toCharArray(); + + private static void appendHex(char ch, Appendable out) throws IOException { + out.append("\\x").append(hexChars[(ch >>> 4) & 0xf]).append(hexChars[ch & 0xf]); + } + + private static void appendUnicode(int codePoint, Appendable out) throws IOException { + if (Character.isSupplementaryCodePoint(codePoint)) { + char[] surrogates = Character.toChars(codePoint); + appendUnicode(surrogates[0], out); + appendUnicode(surrogates[1], out); + return; + } + out.append("\\u") + .append(hexChars[(codePoint >>> 12) & 0xf]) + .append(hexChars[(codePoint >>> 8) & 0xf]) + .append(hexChars[(codePoint >>> 4) & 0xf]) + .append(hexChars[codePoint & 0xf]); + } + + private static final class BoxedCodePointSet { + final boolean[] fastArray; + final Set elements; + + BoxedCodePointSet(Set codePoints) { + this.elements = codePoints; + fastArray = new boolean[0x100]; + for (int i = 0; i < fastArray.length; i++) { + fastArray[i] = elements.contains(i); + } + } + + boolean contains(int codePoint) { + if (codePoint < fastArray.length) { + return fastArray[codePoint]; + } + return elements.contains(codePoint); + } + } + + private static final BoxedCodePointSet ORIGINAL_JS_ESCAPE_CHARS; + + static { + Set set = new HashSet<>(); + set.add(0xAD); + for (int i = 0x600; i <= 0x603; i++) set.add(i); + set.add(0x6DD); + set.add(0x070F); + for (int i = 0x17B4; i <= 0x17B5; i++) set.add(i); + for (int i = 0x200B; i <= 0x200F; i++) set.add(i); + for (int i = 0x202A; i <= 0x202E; i++) set.add(i); + for (int i = 0x2028; i <= 0x2029; i++) set.add(i); + for (int i = 0x2060; i <= 0x2064; i++) set.add(i); + for (int i = 0x206A; i <= 0x206F; i++) set.add(i); + set.add(0xFEFF); + for (int i = 0xFFF9; i <= 0xFFFB; i++) set.add(i); + for (int i = 0x1D173; i <= 0x1D17A; i++) set.add(i); + set.add(0xE0001); + for (int i = 0xE0020; i <= 0xE007F; i++) set.add(i); + set.add(0x0000); + set.add(0x000A); + set.add(0x000D); + set.add(0x0085); + set.add((int) '\''); + set.add((int) '\"'); + set.add((int) '&'); + set.add((int) '<'); + set.add((int) '>'); + set.add((int) '='); + set.add((int) '\\'); + ORIGINAL_JS_ESCAPE_CHARS = new BoxedCodePointSet(set); + } +} diff --git a/android/guava/src/com/google/common/collect/ImmutableBiMap.java b/android/guava/src/com/google/common/collect/ImmutableBiMap.java index 017c798bb2f4..15038c7e686e 100644 --- a/android/guava/src/com/google/common/collect/ImmutableBiMap.java +++ b/android/guava/src/com/google/common/collect/ImmutableBiMap.java @@ -44,6 +44,10 @@ * @since 2.0 */ @GwtCompatible +@SuppressWarnings({ + "TooManyParameters", + "AssignmentExpression" +}) // fundamental factory methods and concise assignments public abstract class ImmutableBiMap extends ImmutableMap implements BiMap { /** @@ -527,6 +531,8 @@ public static ImmutableBiMap copyOf(Map m return bimap; } } + // Map.forEach is avoided in the Android java7 branch to maintain compatibility with Android + // APIs < 24. return copyOf(map.entrySet()); } diff --git a/android/guava/src/com/google/common/collect/ImmutableMap.java b/android/guava/src/com/google/common/collect/ImmutableMap.java index 1a93971b8c86..4374880a27da 100644 --- a/android/guava/src/com/google/common/collect/ImmutableMap.java +++ b/android/guava/src/com/google/common/collect/ImmutableMap.java @@ -69,7 +69,11 @@ */ @DoNotMock("Use ImmutableMap.of or another implementation") @GwtCompatible -@SuppressWarnings("serial") // we're overriding default serialization +@SuppressWarnings({ + "serial", + "TooManyParameters", + "AssignmentExpression" +}) // overriding serialization, fundamental factory methods, and concise assignments public abstract class ImmutableMap implements Map, Serializable { /** @@ -456,7 +460,6 @@ public Builder() { this(ImmutableCollection.Builder.DEFAULT_INITIAL_CAPACITY); } - @SuppressWarnings({"unchecked", "rawtypes"}) Builder(int initialCapacity) { this.alternatingKeysAndValues = new @Nullable Object[2 * initialCapacity]; this.size = 0; @@ -510,6 +513,8 @@ public Builder put(Entry entry) { */ @CanIgnoreReturnValue public Builder putAll(Map map) { + // Map.forEach is avoided in the Android java7 branch to maintain compatibility with Android + // APIs < 24. return putAll(map.entrySet()); } @@ -655,12 +660,12 @@ static void sortEntries( @Nullable Object[] alternatingKeysAndValues, int size, Comparator valueComparator) { - @SuppressWarnings({"rawtypes", "unchecked"}) + @SuppressWarnings({"rawtypes", "unchecked"}) // safe array allocation for sorting Entry[] entries = new Entry[size]; for (int i = 0; i < size; i++) { // requireNonNull is safe because the first `2*size` elements have been filled in. Object key = requireNonNull(alternatingKeysAndValues[2 * i]); - @SuppressWarnings("unchecked") + @SuppressWarnings("unchecked") // safe cast to value type V value = (V) requireNonNull(alternatingKeysAndValues[2 * i + 1]); entries[i] = new AbstractMap.SimpleImmutableEntry(key, value); } @@ -736,6 +741,8 @@ public static ImmutableMap copyOf(Map map return kvMap; } } + // Map.forEach is avoided in the Android java7 branch to maintain compatibility with Android + // APIs < 24. return copyOf(map.entrySet()); } @@ -1150,7 +1157,7 @@ static class SerializedForm implements Serializable { this.values = map.values(); } - @SuppressWarnings("unchecked") + @SuppressWarnings("unchecked") // safe deserialization resolution final Object readResolve() { if (!(this.keys instanceof ImmutableSet)) { return legacyReadResolve(); @@ -1171,7 +1178,7 @@ final Object readResolve() { return builder.buildOrThrow(); } - @SuppressWarnings("unchecked") + @SuppressWarnings("unchecked") // safe legacy deserialization resolution final Object legacyReadResolve() { K[] keys = (K[]) this.keys; V[] values = (V[]) this.values; diff --git a/android/guava/src/com/google/common/math/DoubleMath.java b/android/guava/src/com/google/common/math/DoubleMath.java index c4e8dc8dfec2..4300b246d3b4 100644 --- a/android/guava/src/com/google/common/math/DoubleMath.java +++ b/android/guava/src/com/google/common/math/DoubleMath.java @@ -15,8 +15,10 @@ package com.google.common.math; import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.math.DoubleUtils.EXPONENT_MASK; import static com.google.common.math.DoubleUtils.IMPLICIT_BIT; import static com.google.common.math.DoubleUtils.SIGNIFICAND_BITS; +import static com.google.common.math.DoubleUtils.SIGNIFICAND_MASK; import static com.google.common.math.DoubleUtils.getSignificand; import static com.google.common.math.DoubleUtils.isFinite; import static com.google.common.math.DoubleUtils.isNormal; @@ -198,11 +200,16 @@ public static BigInteger roundToBigInteger(double x, RoundingMode mode) { * Returns {@code true} if {@code x} is exactly equal to {@code 2^k} for some finite integer * {@code k}. */ - @GwtIncompatible // com.google.common.math.DoubleUtils + @GwtIncompatible // Double.doubleToRawLongBits public static boolean isPowerOfTwo(double x) { if (x > 0.0 && isFinite(x)) { - long significand = getSignificand(x); - return (significand & (significand - 1)) == 0; + long bits = Double.doubleToRawLongBits(x); + long significand = bits & SIGNIFICAND_MASK; + // Bypasses significand/exponent extraction and branching by directly inspecting IEEE 754 + // bits. + // Normal doubles: significand is 0. Subnormal doubles: exponent is 0, significand has 1 bit. + return significand == 0 + || ((bits & EXPONENT_MASK) == 0 && (significand & (significand - 1)) == 0); } return false; } diff --git a/android/guava/src/com/google/common/math/IntMath.java b/android/guava/src/com/google/common/math/IntMath.java index 32d0a984b373..9913734ebdb3 100644 --- a/android/guava/src/com/google/common/math/IntMath.java +++ b/android/guava/src/com/google/common/math/IntMath.java @@ -386,11 +386,15 @@ public static int divide(int p, int q, RoundingMode mode) { * @see * Remainder Operator */ + @SuppressWarnings("all") // suppressing floorMod suggestions on fundamental mod implementation public static int mod(int x, int m) { if (m <= 0) { throw new ArithmeticException("Modulus " + m + " must be > 0"); } - return Math.floorMod(x, m); + // When m is a positive power of two, x & (m - 1) extracts the lowest bits in two's complement, + // yielding the exact positive remainder in [0, m - 1] and bypassing expensive division + // instructions. + return ((m & (m - 1)) == 0) ? (x & (m - 1)) : Math.floorMod(x, m); } /** diff --git a/android/guava/src/com/google/common/math/LongMath.java b/android/guava/src/com/google/common/math/LongMath.java index ddab4a446fc8..9c4945166188 100644 --- a/android/guava/src/com/google/common/math/LongMath.java +++ b/android/guava/src/com/google/common/math/LongMath.java @@ -453,6 +453,7 @@ public static long divide(long p, long q, RoundingMode mode) { * Remainder Operator */ @GwtIncompatible // TODO + @SuppressWarnings("all") // suppressing floorMod suggestions on fundamental mod implementation public static int mod(long x, int m) { // Cast is safe because the result is guaranteed in the range [0, m) return (int) mod(x, (long) m); @@ -477,11 +478,15 @@ public static int mod(long x, int m) { * Remainder Operator */ @GwtIncompatible // TODO + @SuppressWarnings("all") // suppressing floorMod suggestions on fundamental mod implementation public static long mod(long x, long m) { if (m <= 0) { throw new ArithmeticException("Modulus must be positive"); } - return Math.floorMod(x, m); + // When m is a positive power of two, x & (m - 1) extracts the lowest bits in two's complement, + // yielding the exact positive remainder in [0, m - 1] and bypassing expensive division + // instructions. + return ((m & (m - 1)) == 0) ? (x & (m - 1)) : Math.floorMod(x, m); } /** @@ -1104,12 +1109,12 @@ long squareMod(long a, long m) { }, /** Works for all nonnegative signed longs. */ LARGE { - /** Returns (a + b) mod m. Precondition: {@code 0 <= a}, {@code b < m < 2^63}. */ + /* Returns (a + b) mod m. Precondition: {@code 0 <= a}, {@code b < m < 2^63}. */ private long plusMod(long a, long b, long m) { return (a >= m - b) ? (a + b - m) : (a + b); } - /** Returns (a * 2^32) mod m. a may be any unsigned long. */ + /* Returns (a * 2^32) mod m. a may be any unsigned long. */ private long times2ToThe32Mod(long a, long m) { int remainingPowersOf2 = 32; do { diff --git a/guava-tests/benchmark/com/google/common/base/StringUtilJmhBenchmark.java b/guava-tests/benchmark/com/google/common/base/StringUtilJmhBenchmark.java new file mode 100644 index 000000000000..41a0e48758fd --- /dev/null +++ b/guava-tests/benchmark/com/google/common/base/StringUtilJmhBenchmark.java @@ -0,0 +1,233 @@ +package com.google.common.base; + +import static java.util.concurrent.TimeUnit.NANOSECONDS; + +import java.io.IOException; +import java.util.HashSet; +import java.util.Set; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; + +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(NANOSECONDS) +@State(Scope.Benchmark) +@Fork(5) +public class StringUtilJmhBenchmark { + @Param({"100", "1000"}) + private int length; + + @Param({"ASCII", "CJK", "SPECIAL"}) + private String type; + + private String input; + + @Setup + public void setUp() { + StringBuilder sb = new StringBuilder(length); + if ("SPECIAL".equals(type)) { + int[] specialChars = { + 'a', + 0xAD, + 0x600, + 0x202A, + 0xFFF9, + 0x1D173, + 0xE0001, // In the set + 0x101, + 0x700, + 0x3001, + 0x1D172, + 0xE0002 // Not in the set + }; + for (int i = 0; i < length; i++) { + sb.appendCodePoint(specialChars[i % specialChars.length]); + } + } else { + for (int i = 0; i < length; i++) { + if ("ASCII".equals(type)) { + sb.append((char) ('a' + (i % 26))); + } else { + sb.append((char) (0x4e00 + (i % 1000))); // Chinese ideographs + } + } + } + input = sb.toString(); + } + + @Benchmark + public String escapeUnicodeCurrent() { + return StringUtil.javaScriptEscape(input); + } + + @Benchmark + public String escapeUnicodeOriginal() { + return originalJavaScriptEscape(input, false); + } + + @Benchmark + public String escapeAsciiCurrent() { + return StringUtil.javaScriptEscapeToAscii(input); + } + + @Benchmark + public String escapeAsciiOriginal() { + return originalJavaScriptEscape(input, true); + } + + private static String originalJavaScriptEscape(CharSequence s, boolean escapeToAscii) { + StringBuilder sb = new StringBuilder(s.length() * 9 / 8); + try { + originalEscapeStringBody(s, escapeToAscii, StringUtil.JsEscapingMode.EMBEDDABLE_JS, sb); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + return sb.toString(); + } + + private static void originalEscapeStringBody( + CharSequence plainText, + boolean escapeToAscii, + StringUtil.JsEscapingMode jsEscapingMode, + Appendable out) + throws IOException { + int pos = 0; + int len = plainText.length(); + for (int codePoint, charCount, i = 0; i < len; i += charCount) { + codePoint = Character.codePointAt(plainText, i); + charCount = Character.charCount(codePoint); + + if (!originalShouldEscapeChar(codePoint, escapeToAscii, jsEscapingMode)) { + continue; + } + + out.append(plainText, pos, i); + pos = i + charCount; + switch (codePoint) { + case '\b': + out.append("\\b"); + break; + case '\t': + out.append("\\t"); + break; + case '\n': + out.append("\\n"); + break; + case '\f': + out.append("\\f"); + break; + case '\r': + out.append("\\r"); + break; + case '\\': + out.append("\\\\"); + break; + case '"': + case '\'': + if (jsEscapingMode == StringUtil.JsEscapingMode.JSON && codePoint == '\'') { + out.append((char) codePoint); + break; + } else if (jsEscapingMode != StringUtil.JsEscapingMode.EMBEDDABLE_JS) { + out.append('\\').append((char) codePoint); + break; + } + // fall through + default: + if (codePoint >= 0x100 || jsEscapingMode == StringUtil.JsEscapingMode.JSON) { + appendUnicode(codePoint, out); + } else { + appendHex((char) codePoint, out); + } + break; + } + } + out.append(plainText, pos, len); + } + + private static boolean originalShouldEscapeChar( + int codePoint, boolean escapeToAscii, StringUtil.JsEscapingMode jsEscapingMode) { + if (escapeToAscii && (codePoint < 0x20 || codePoint > 0x7e)) { + return true; + } + return ORIGINAL_JS_ESCAPE_CHARS.contains(codePoint); + } + + private static final char[] hexChars = "0123456789abcdef".toCharArray(); + + private static void appendHex(char ch, Appendable out) throws IOException { + out.append("\\x").append(hexChars[(ch >>> 4) & 0xf]).append(hexChars[ch & 0xf]); + } + + private static void appendUnicode(int codePoint, Appendable out) throws IOException { + if (Character.isSupplementaryCodePoint(codePoint)) { + char[] surrogates = Character.toChars(codePoint); + appendUnicode(surrogates[0], out); + appendUnicode(surrogates[1], out); + return; + } + out.append("\\u") + .append(hexChars[(codePoint >>> 12) & 0xf]) + .append(hexChars[(codePoint >>> 8) & 0xf]) + .append(hexChars[(codePoint >>> 4) & 0xf]) + .append(hexChars[codePoint & 0xf]); + } + + private static final class BoxedCodePointSet { + final boolean[] fastArray; + final Set elements; + + BoxedCodePointSet(Set codePoints) { + this.elements = codePoints; + fastArray = new boolean[0x100]; + for (int i = 0; i < fastArray.length; i++) { + fastArray[i] = elements.contains(i); + } + } + + boolean contains(int codePoint) { + if (codePoint < fastArray.length) { + return fastArray[codePoint]; + } + return elements.contains(codePoint); + } + } + + private static final BoxedCodePointSet ORIGINAL_JS_ESCAPE_CHARS; + + static { + Set set = new HashSet<>(); + set.add(0xAD); + for (int i = 0x600; i <= 0x603; i++) set.add(i); + set.add(0x6DD); + set.add(0x070F); + for (int i = 0x17B4; i <= 0x17B5; i++) set.add(i); + for (int i = 0x200B; i <= 0x200F; i++) set.add(i); + for (int i = 0x202A; i <= 0x202E; i++) set.add(i); + for (int i = 0x2028; i <= 0x2029; i++) set.add(i); + for (int i = 0x2060; i <= 0x2064; i++) set.add(i); + for (int i = 0x206A; i <= 0x206F; i++) set.add(i); + set.add(0xFEFF); + for (int i = 0xFFF9; i <= 0xFFFB; i++) set.add(i); + for (int i = 0x1D173; i <= 0x1D17A; i++) set.add(i); + set.add(0xE0001); + for (int i = 0xE0020; i <= 0xE007F; i++) set.add(i); + set.add(0x0000); + set.add(0x000A); + set.add(0x000D); + set.add(0x0085); + set.add((int) '\''); + set.add((int) '\"'); + set.add((int) '&'); + set.add((int) '<'); + set.add((int) '>'); + set.add((int) '='); + set.add((int) '\\'); + ORIGINAL_JS_ESCAPE_CHARS = new BoxedCodePointSet(set); + } +} diff --git a/guava/src/com/google/common/collect/ImmutableBiMap.java b/guava/src/com/google/common/collect/ImmutableBiMap.java index ae42b8aa2de3..7e3bcd8e11b8 100644 --- a/guava/src/com/google/common/collect/ImmutableBiMap.java +++ b/guava/src/com/google/common/collect/ImmutableBiMap.java @@ -32,6 +32,7 @@ import java.util.Arrays; import java.util.Comparator; import java.util.Map; +import java.util.function.BiConsumer; import java.util.function.BinaryOperator; import java.util.function.Function; import java.util.stream.Collector; @@ -46,6 +47,10 @@ * @since 2.0 */ @GwtCompatible +@SuppressWarnings({ + "TooManyParameters", + "AssignmentExpression" +}) // fundamental factory methods and concise assignments public abstract class ImmutableBiMap extends ImmutableMap implements BiMap { /** @@ -541,7 +546,39 @@ public static ImmutableBiMap copyOf(Map m return bimap; } } - return copyOf(map.entrySet()); + int size = map.size(); + if (size == 0) { + return of(); + } + // safe since array is only populated with Entry instances + @SuppressWarnings("unchecked") + Entry[] entries = (Entry[]) new Entry[size]; + // BiConsumer iterates directly to bypass iterator and Entry wrapper allocations for + // lazy/transformed views + class EntryCollector implements BiConsumer { + Entry[] array = entries; + int index = 0; + + @Override + public void accept(K k, V v) { + if (index >= array.length) { + // Use multiplicative growth to efficiently handle concurrent map expansion + array = Arrays.copyOf(array, array.length + (array.length >> 1) + 1); + } + array[index++] = entryOf(k, v); + } + } + EntryCollector collector = new EntryCollector(); + map.forEach(collector); + int finalSize = collector.index; + Entry[] finalEntries = collector.array; + if (finalSize < finalEntries.length) { + finalEntries = Arrays.copyOf(finalEntries, finalSize); + } + if (finalSize == 0) { + return of(); + } + return RegularImmutableBiMap.fromEntryArray(finalSize, finalEntries); } /** diff --git a/guava/src/com/google/common/collect/ImmutableMap.java b/guava/src/com/google/common/collect/ImmutableMap.java index abc8910eb0a0..fe42d736bc80 100644 --- a/guava/src/com/google/common/collect/ImmutableMap.java +++ b/guava/src/com/google/common/collect/ImmutableMap.java @@ -53,6 +53,7 @@ import java.util.SortedMap; import java.util.Spliterator; import java.util.Spliterators; +import java.util.function.BiConsumer; import java.util.function.BiFunction; import java.util.function.BinaryOperator; import java.util.function.Function; @@ -73,7 +74,11 @@ */ @DoNotMock("Use ImmutableMap.of or another implementation") @GwtCompatible -@SuppressWarnings("serial") // we're overriding default serialization +@SuppressWarnings({ + "serial", + "TooManyParameters", + "AssignmentExpression" +}) // overriding serialization, fundamental factory methods, and concise assignments public abstract class ImmutableMap implements Map, Serializable { /** @@ -430,7 +435,7 @@ public Builder() { this(ImmutableCollection.Builder.DEFAULT_INITIAL_CAPACITY); } - @SuppressWarnings({"unchecked", "rawtypes"}) + @SuppressWarnings({"unchecked", "rawtypes"}) // safe array allocation for entries Builder(int initialCapacity) { this.entries = new @Nullable Entry[initialCapacity]; this.size = 0; @@ -481,7 +486,9 @@ public Builder put(Entry entry) { */ @CanIgnoreReturnValue public Builder putAll(Map map) { - return putAll(map.entrySet()); + ensureCapacity(size + map.size()); + map.forEach(this::put); + return this; } /** @@ -664,7 +671,7 @@ ImmutableMap buildJdkBacked() { if (dups.isEmpty()) { return null; } - @SuppressWarnings({"rawtypes", "unchecked"}) + @SuppressWarnings({"rawtypes", "unchecked"}) // safe array allocation for entries Entry[] newEntries = new Entry[size - dups.cardinality()]; for (int inI = 0, outI = 0; inI < size; inI++) { if (!dups.get(inI)) { @@ -712,7 +719,40 @@ public static ImmutableMap copyOf(Map map ImmutableMap result = (ImmutableMap) untypedResult; return result; } - return copyOf(map.entrySet()); + int size = map.size(); + if (size == 0) { + return of(); + } + // safe since array is only populated with Entry instances + @SuppressWarnings("unchecked") + Entry[] entries = (Entry[]) new Entry[size]; + // BiConsumer iterates directly to bypass iterator and Entry wrapper allocations for + // lazy/transformed views + class EntryCollector implements BiConsumer { + Entry[] array = entries; + int index = 0; + + @Override + public void accept(K k, V v) { + if (index >= array.length) { + // Use multiplicative growth to efficiently handle concurrent map expansion + array = Arrays.copyOf(array, array.length + (array.length >> 1) + 1); + } + array[index++] = entryOf(k, v); + } + } + EntryCollector collector = new EntryCollector(); + map.forEach(collector); + int finalSize = collector.index; + Entry[] finalEntries = collector.array; + if (finalSize < finalEntries.length) { + finalEntries = Arrays.copyOf(finalEntries, finalSize); + } + if (finalSize == 0) { + return of(); + } + return RegularImmutableMap.fromEntryArray( + finalSize, finalEntries, /* throwIfDuplicateKeys= */ true); } /** @@ -1271,7 +1311,7 @@ static class SerializedForm implements Serializable { this.values = map.values(); } - @SuppressWarnings("unchecked") + @SuppressWarnings("unchecked") // safe deserialization resolution final Object readResolve() { if (!(this.keys instanceof ImmutableSet)) { return legacyReadResolve(); @@ -1292,7 +1332,7 @@ final Object readResolve() { return builder.buildOrThrow(); } - @SuppressWarnings("unchecked") + @SuppressWarnings("unchecked") // safe legacy deserialization resolution final Object legacyReadResolve() { K[] keys = (K[]) this.keys; V[] values = (V[]) this.values; diff --git a/guava/src/com/google/common/math/DoubleMath.java b/guava/src/com/google/common/math/DoubleMath.java index c4e8dc8dfec2..4300b246d3b4 100644 --- a/guava/src/com/google/common/math/DoubleMath.java +++ b/guava/src/com/google/common/math/DoubleMath.java @@ -15,8 +15,10 @@ package com.google.common.math; import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.math.DoubleUtils.EXPONENT_MASK; import static com.google.common.math.DoubleUtils.IMPLICIT_BIT; import static com.google.common.math.DoubleUtils.SIGNIFICAND_BITS; +import static com.google.common.math.DoubleUtils.SIGNIFICAND_MASK; import static com.google.common.math.DoubleUtils.getSignificand; import static com.google.common.math.DoubleUtils.isFinite; import static com.google.common.math.DoubleUtils.isNormal; @@ -198,11 +200,16 @@ public static BigInteger roundToBigInteger(double x, RoundingMode mode) { * Returns {@code true} if {@code x} is exactly equal to {@code 2^k} for some finite integer * {@code k}. */ - @GwtIncompatible // com.google.common.math.DoubleUtils + @GwtIncompatible // Double.doubleToRawLongBits public static boolean isPowerOfTwo(double x) { if (x > 0.0 && isFinite(x)) { - long significand = getSignificand(x); - return (significand & (significand - 1)) == 0; + long bits = Double.doubleToRawLongBits(x); + long significand = bits & SIGNIFICAND_MASK; + // Bypasses significand/exponent extraction and branching by directly inspecting IEEE 754 + // bits. + // Normal doubles: significand is 0. Subnormal doubles: exponent is 0, significand has 1 bit. + return significand == 0 + || ((bits & EXPONENT_MASK) == 0 && (significand & (significand - 1)) == 0); } return false; } diff --git a/guava/src/com/google/common/math/IntMath.java b/guava/src/com/google/common/math/IntMath.java index 32d0a984b373..9913734ebdb3 100644 --- a/guava/src/com/google/common/math/IntMath.java +++ b/guava/src/com/google/common/math/IntMath.java @@ -386,11 +386,15 @@ public static int divide(int p, int q, RoundingMode mode) { * @see * Remainder Operator */ + @SuppressWarnings("all") // suppressing floorMod suggestions on fundamental mod implementation public static int mod(int x, int m) { if (m <= 0) { throw new ArithmeticException("Modulus " + m + " must be > 0"); } - return Math.floorMod(x, m); + // When m is a positive power of two, x & (m - 1) extracts the lowest bits in two's complement, + // yielding the exact positive remainder in [0, m - 1] and bypassing expensive division + // instructions. + return ((m & (m - 1)) == 0) ? (x & (m - 1)) : Math.floorMod(x, m); } /** diff --git a/guava/src/com/google/common/math/LongMath.java b/guava/src/com/google/common/math/LongMath.java index ddab4a446fc8..9c4945166188 100644 --- a/guava/src/com/google/common/math/LongMath.java +++ b/guava/src/com/google/common/math/LongMath.java @@ -453,6 +453,7 @@ public static long divide(long p, long q, RoundingMode mode) { * Remainder Operator */ @GwtIncompatible // TODO + @SuppressWarnings("all") // suppressing floorMod suggestions on fundamental mod implementation public static int mod(long x, int m) { // Cast is safe because the result is guaranteed in the range [0, m) return (int) mod(x, (long) m); @@ -477,11 +478,15 @@ public static int mod(long x, int m) { * Remainder Operator */ @GwtIncompatible // TODO + @SuppressWarnings("all") // suppressing floorMod suggestions on fundamental mod implementation public static long mod(long x, long m) { if (m <= 0) { throw new ArithmeticException("Modulus must be positive"); } - return Math.floorMod(x, m); + // When m is a positive power of two, x & (m - 1) extracts the lowest bits in two's complement, + // yielding the exact positive remainder in [0, m - 1] and bypassing expensive division + // instructions. + return ((m & (m - 1)) == 0) ? (x & (m - 1)) : Math.floorMod(x, m); } /** @@ -1104,12 +1109,12 @@ long squareMod(long a, long m) { }, /** Works for all nonnegative signed longs. */ LARGE { - /** Returns (a + b) mod m. Precondition: {@code 0 <= a}, {@code b < m < 2^63}. */ + /* Returns (a + b) mod m. Precondition: {@code 0 <= a}, {@code b < m < 2^63}. */ private long plusMod(long a, long b, long m) { return (a >= m - b) ? (a + b - m) : (a + b); } - /** Returns (a * 2^32) mod m. a may be any unsigned long. */ + /* Returns (a * 2^32) mod m. a may be any unsigned long. */ private long times2ToThe32Mod(long a, long m) { int remainingPowersOf2 = 32; do {