Avoid calling String.substring when possible.
Due to e.g. method inlining, the same strings occur over and over again.
Java's Map interface requires us to call String.substring to even do a
dedup, which creates garbage.
This change adds a small direct-mapped cache to avoid creating objects.
When fully parsing youtube.android_17.19, I found that
ProguardMapReader.substring is called about 5M times, and the new cache
has a hit rate of ~80%, avoiding creating ~3.9M Strings.
Bug: b/240037206
Change-Id: Iff054f814ce33dfe65f6afe5b03181630721239a
diff --git a/src/main/java/com/android/tools/r8/naming/ProguardMapReader.java b/src/main/java/com/android/tools/r8/naming/ProguardMapReader.java
index 10a5fee..96609c9 100644
--- a/src/main/java/com/android/tools/r8/naming/ProguardMapReader.java
+++ b/src/main/java/com/android/tools/r8/naming/ProguardMapReader.java
@@ -461,6 +461,16 @@
}
}
+ // Small direct-mapped cache for computing String.substring.
+ //
+ // Due to inlining, the same method names and parameter types will occur repeatedly on multiple
+ // lines. String.substring ends up allocating a lot of garbage, so we use this cache to find
+ // String objects without having to allocate memory.
+ //
+ // "Direct-mapped" is inspired from computer architecture, where having a lookup policy in
+ // which entries can only ever map to one cache line is often faster than a fancy LRU cache.
+ private static final int SUBSTRING_CACHE_SIZE = 64;
+ private final String[] substringCache = new String[SUBSTRING_CACHE_SIZE];
// Cache for canonicalizing strings.
// This saves 10% of heap space for large programs.
private final HashMap<String, String> identifierCache = new HashMap<>();
@@ -472,8 +482,21 @@
private final HashMap<Signature, Signature> signatureCache = new HashMap<>();
private String substring(int start) {
+ int cacheIdx;
+ {
+ // Check if there was a recent String accessed which matches the substring.
+ int len = lineOffset - start;
+ cacheIdx = len % SUBSTRING_CACHE_SIZE;
+ String candidate = substringCache[cacheIdx];
+ if (candidate != null
+ && candidate.length() == len
+ && line.regionMatches(start, candidate, 0, len)) {
+ return candidate;
+ }
+ }
+
String result = line.substring(start, lineOffset);
- return identifierCache.computeIfAbsent(result, Function.identity());
+ return substringCache[cacheIdx] = identifierCache.computeIfAbsent(result, Function.identity());
}
private String parseMethodName() {