From e350c347d5be1e0107d3c1b7617b7037b50e34f9 Mon Sep 17 00:00:00 2001 From: Google Java Core Libraries Date: Wed, 20 May 2026 23:40:36 -0700 Subject: [PATCH] Optimize CharMatcher.replaceFrom(CharSequence, CharSequence) to use a thread-local character buffer for target lengths <= 1024, eliminating temporary array allocations. RELNOTES=n/a PiperOrigin-RevId: 918861136 --- .../com/google/common/base/CharMatcher.java | 61 ++++++++++++++++--- .../src/com/google/common/base/Platform.java | 48 ++++++++------- .../com/google/common/base/Platform.java | 17 ++++++ .../com/google/common/base/CharMatcher.java | 61 ++++++++++++++++--- .../src/com/google/common/base/Platform.java | 26 ++++++++ 5 files changed, 171 insertions(+), 42 deletions(-) diff --git a/android/guava/src/com/google/common/base/CharMatcher.java b/android/guava/src/com/google/common/base/CharMatcher.java index 149ca7e43ae4..37548b371e30 100644 --- a/android/guava/src/com/google/common/base/CharMatcher.java +++ b/android/guava/src/com/google/common/base/CharMatcher.java @@ -727,18 +727,59 @@ public String replaceFrom(CharSequence sequence, CharSequence replacement) { } int len = string.length(); - StringBuilder buf = new StringBuilder((len * 3 / 2) + 16); - int oldpos = 0; + // Count matches to pre-calculate the exact size of the destination array. + int count = 0; + int matchPos = pos; do { - buf.append(string, oldpos, pos); - buf.append(replacement); - oldpos = pos + 1; - pos = indexIn(string, oldpos); - } while (pos != -1); - - buf.append(string, oldpos, len); - return buf.toString(); + count++; + matchPos = indexIn(string, matchPos + 1); + } while (matchPos != -1); + + long newLen = (long) len + (long) count * (replacementLen - 1); + if (newLen > Integer.MAX_VALUE) { + throw new IllegalArgumentException("Required length exceeds implementation limit"); + } + int destSize = (int) newLen; + + char[] dest = Platform.acquireCharBuffer(); + boolean acquired = (dest != null); + if (dest == null) { + dest = new char[destSize]; + } else if (dest.length < destSize) { + dest = new char[destSize]; + } + + try { + String replStr = replacement.toString(); + int destIndex = 0; + int oldpos = 0; + matchPos = pos; + + do { + int charsToCopy = matchPos - oldpos; + if (charsToCopy > 0) { + string.getChars(oldpos, matchPos, dest, destIndex); + destIndex += charsToCopy; + } + replStr.getChars(0, replacementLen, dest, destIndex); + destIndex += replacementLen; + oldpos = matchPos + 1; + matchPos = indexIn(string, oldpos); + } while (matchPos != -1); + + int charsLeft = len - oldpos; + if (charsLeft > 0) { + string.getChars(oldpos, len, dest, destIndex); + destIndex += charsLeft; + } + + return new String(dest, 0, destIndex); + } finally { + if (acquired) { + Platform.releaseCharBuffer(dest); + } + } } /** diff --git a/android/guava/src/com/google/common/base/Platform.java b/android/guava/src/com/google/common/base/Platform.java index d4674d2ffcfd..0dc6307fd16c 100644 --- a/android/guava/src/com/google/common/base/Platform.java +++ b/android/guava/src/com/google/common/base/Platform.java @@ -37,16 +37,6 @@ static CharMatcher precomputeCharMatcher(CharMatcher matcher) { static > Optional getEnumIfPresent(Class enumClass, String value) { WeakReference> ref = Enums.getEnumConstants(enumClass).get(value); - /* - * We use `fromNullable` instead of `of` because `WeakReference.get()` has a nullable return - * type. - * - * In practice, we are very unlikely to see `null`: The `WeakReference` to the enum constant - * won't be cleared as long as the enum constant is referenced somewhere, and the enum constant - * is referenced somewhere for as long as the enum class is loaded. *Maybe in theory* the enum - * class could be unloaded after the above call to `getEnumConstants` but before we call - * `get()`, but that is vanishingly unlikely. - */ return ref == null ? Optional.absent() : Optional.fromNullable(enumClass.cast(ref.get())); } @@ -58,22 +48,10 @@ static boolean stringIsNullOrEmpty(@Nullable String string) { return string == null || string.isEmpty(); } - /** - * Returns the string if it is not null, or an empty string otherwise. - * - * @param string the string to test and possibly return - * @return {@code string} if it is not null; {@code ""} otherwise - */ static String nullToEmpty(@Nullable String string) { return (string == null) ? "" : string; } - /** - * Returns the string if it is not empty, or a null string otherwise. - * - * @param string the string to test and possibly return - * @return {@code string} if it is not empty; {@code null} otherwise - */ static @Nullable String emptyToNull(@Nullable String string) { return stringIsNullOrEmpty(string) ? null : string; } @@ -116,4 +94,30 @@ public boolean isPcreLike() { return true; } } + + private static @Nullable ThreadLocal destTl; + + /** Acquires a thread-local 1024-char buffer if available, or returns null if busy. */ + static char @Nullable [] acquireCharBuffer() { + ThreadLocal tl = destTl; + if (tl == null) { + destTl = tl = new ThreadLocal(); + } + char[] buffer = tl.get(); + if (buffer == null) { + return new char[1024]; + } + tl.set(null); + return buffer; + } + + /** Releases the acquired thread-local buffer. */ + static void releaseCharBuffer(char[] buffer) { + if (buffer.length == 1024) { + ThreadLocal tl = destTl; + if (tl != null) { + tl.set(buffer); + } + } + } } diff --git a/guava-gwt/src-super/com/google/common/base/super/com/google/common/base/Platform.java b/guava-gwt/src-super/com/google/common/base/super/com/google/common/base/Platform.java index c2dc4ed06c98..19d5ebfe8ebd 100644 --- a/guava-gwt/src-super/com/google/common/base/super/com/google/common/base/Platform.java +++ b/guava-gwt/src-super/com/google/common/base/super/com/google/common/base/Platform.java @@ -70,5 +70,22 @@ static String stringValueOf(@Nullable Object o) { return String.valueOf(o); } + private static final char[] CHAR_BUFFER = new char[1024]; + private static boolean inUse = false; + + /** Acquires the static buffer if available, or returns null if busy (re-entrant call). */ + static char @Nullable [] acquireCharBuffer() { + if (inUse) { + return null; + } + inUse = true; + return CHAR_BUFFER; + } + + /** Releases the acquired buffer. */ + static void releaseCharBuffer(char[] buffer) { + inUse = false; + } + private Platform() {} } diff --git a/guava/src/com/google/common/base/CharMatcher.java b/guava/src/com/google/common/base/CharMatcher.java index ee7c6de1c043..11c2ccaec864 100644 --- a/guava/src/com/google/common/base/CharMatcher.java +++ b/guava/src/com/google/common/base/CharMatcher.java @@ -727,18 +727,59 @@ public String replaceFrom(CharSequence sequence, CharSequence replacement) { } int len = string.length(); - StringBuilder buf = new StringBuilder((len * 3 / 2) + 16); - int oldpos = 0; + // Count matches to pre-calculate the exact size of the destination array. + int count = 0; + int matchPos = pos; do { - buf.append(string, oldpos, pos); - buf.append(replacement); - oldpos = pos + 1; - pos = indexIn(string, oldpos); - } while (pos != -1); - - buf.append(string, oldpos, len); - return buf.toString(); + count++; + matchPos = indexIn(string, matchPos + 1); + } while (matchPos != -1); + + long newLen = (long) len + (long) count * (replacementLen - 1); + if (newLen > Integer.MAX_VALUE) { + throw new IllegalArgumentException("Required length exceeds implementation limit"); + } + int destSize = (int) newLen; + + char[] dest = Platform.acquireCharBuffer(); + boolean acquired = (dest != null); + if (dest == null) { + dest = new char[destSize]; + } else if (dest.length < destSize) { + dest = new char[destSize]; + } + + try { + String replStr = replacement.toString(); + int destIndex = 0; + int oldpos = 0; + matchPos = pos; + + do { + int charsToCopy = matchPos - oldpos; + if (charsToCopy > 0) { + string.getChars(oldpos, matchPos, dest, destIndex); + destIndex += charsToCopy; + } + replStr.getChars(0, replacementLen, dest, destIndex); + destIndex += replacementLen; + oldpos = matchPos + 1; + matchPos = indexIn(string, oldpos); + } while (matchPos != -1); + + int charsLeft = len - oldpos; + if (charsLeft > 0) { + string.getChars(oldpos, len, dest, destIndex); + destIndex += charsLeft; + } + + return new String(dest, 0, destIndex); + } finally { + if (acquired) { + Platform.releaseCharBuffer(dest); + } + } } /** diff --git a/guava/src/com/google/common/base/Platform.java b/guava/src/com/google/common/base/Platform.java index aee2cdcb06aa..635075e10f7e 100644 --- a/guava/src/com/google/common/base/Platform.java +++ b/guava/src/com/google/common/base/Platform.java @@ -112,4 +112,30 @@ public boolean isPcreLike() { return true; } } + + private static @Nullable ThreadLocal destTl; + + /** Acquires a thread-local 1024-char buffer if available, or returns null if busy. */ + static char @Nullable [] acquireCharBuffer() { + ThreadLocal tl = destTl; + if (tl == null) { + destTl = tl = new ThreadLocal(); + } + char[] buffer = tl.get(); + if (buffer == null) { + return new char[1024]; + } + tl.set(null); + return buffer; + } + + /** Releases the acquired thread-local buffer. */ + static void releaseCharBuffer(char[] buffer) { + if (buffer.length == 1024) { + ThreadLocal tl = destTl; + if (tl != null) { + tl.set(buffer); + } + } + } }