From 00713eeccb943132d5e0f6fc67787830cd517869 Mon Sep 17 00:00:00 2001 From: Christopher Wirt Date: Mon, 5 Feb 2024 21:14:28 +0000 Subject: [PATCH] Reduce the number of object allocations on the decode/encode hotpath When profiling I noticed a large number of strings being generated from AbstractBase64UrlEncoder.decode(). I think this is mostly attributable to the string concatenation inside the various loops. To fix this I've switched to using a StringBuilder. This avoids new strings being allocated on each iteration. I've also removed the use of regular expressions to validate inputs these are also relatively expensive to be happening at such a low level of the code. For bitString validation I've used a simple char inspecting method. For base64 I just used the absence of it appearing in the REVERSE_DICT to indicate this is an invalid base64 string. Finally to reduce autoboxing I switched to using a Char2IntOpenHashMap for holding the REVERSE_DICT. To do this I've had to import a 3rd party fast utils lib. --- iabgpp-encoder/pom.xml | 6 ++- .../encoder/AbstractBase64UrlEncoder.java | 48 +++++++++++-------- .../datatype/encoder/FixedIntegerEncoder.java | 16 +++---- 3 files changed, 40 insertions(+), 30 deletions(-) diff --git a/iabgpp-encoder/pom.xml b/iabgpp-encoder/pom.xml index fe7ebc3..e840906 100644 --- a/iabgpp-encoder/pom.xml +++ b/iabgpp-encoder/pom.xml @@ -20,6 +20,11 @@ 5.9.2 test + + it.unimi.dsi + fastutil + 8.5.12 + @@ -53,4 +58,3 @@ - diff --git a/iabgpp-encoder/src/main/java/com/iab/gpp/encoder/datatype/encoder/AbstractBase64UrlEncoder.java b/iabgpp-encoder/src/main/java/com/iab/gpp/encoder/datatype/encoder/AbstractBase64UrlEncoder.java index 698a099..caac9cc 100644 --- a/iabgpp-encoder/src/main/java/com/iab/gpp/encoder/datatype/encoder/AbstractBase64UrlEncoder.java +++ b/iabgpp-encoder/src/main/java/com/iab/gpp/encoder/datatype/encoder/AbstractBase64UrlEncoder.java @@ -6,6 +6,8 @@ import java.util.stream.Stream; import com.iab.gpp.encoder.error.DecodingException; import com.iab.gpp.encoder.error.EncodingException; +import it.unimi.dsi.fastutil.chars.Char2IntMap; +import it.unimi.dsi.fastutil.chars.Char2IntOpenHashMap; public abstract class AbstractBase64UrlEncoder { @@ -15,9 +17,9 @@ public abstract class AbstractBase64UrlEncoder { * Base 64 URL character set. Different from standard Base64 char set in that '+' and '/' are * replaced with '-' and '_'. */ - private static String DICT = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; + private static final String DICT = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; // prettier-ignore - private static Map REVERSE_DICT = Stream + private static final Char2IntMap REVERSE_DICT = new Char2IntOpenHashMap(Stream .of(new Object[][] {{'A', 0}, {'B', 1}, {'C', 2}, {'D', 3}, {'E', 4}, {'F', 5}, {'G', 6}, {'H', 7}, {'I', 8}, {'J', 9}, {'K', 10}, {'L', 11}, {'M', 12}, {'N', 13}, {'O', 14}, {'P', 15}, {'Q', 16}, {'R', 17}, {'S', 18}, {'T', 19}, {'U', 20}, {'V', 21}, {'W', 22}, {'X', 23}, {'Y', 24}, {'Z', 25}, {'a', 26}, {'b', 27}, {'c', 28}, @@ -25,21 +27,17 @@ public abstract class AbstractBase64UrlEncoder { {'n', 39}, {'o', 40}, {'p', 41}, {'q', 42}, {'r', 43}, {'s', 44}, {'t', 45}, {'u', 46}, {'v', 47}, {'w', 48}, {'x', 49}, {'y', 50}, {'z', 51}, {'0', 52}, {'1', 53}, {'2', 54}, {'3', 55}, {'4', 56}, {'5', 57}, {'6', 58}, {'7', 59}, {'8', 60}, {'9', 61}, {'-', 62}, {'_', 63}}) - .collect(Collectors.toMap(data -> (Character) data[0], data -> (Integer) data[1])); - - private static Pattern BITSTRING_VERIFICATION_PATTERN = Pattern.compile("^[0-1]*$", Pattern.CASE_INSENSITIVE); - private static Pattern BASE64URL_VERIFICATION_PATTERN = - Pattern.compile("^[A-Za-z0-9\\-_]*$", Pattern.CASE_INSENSITIVE); + .collect(Collectors.toMap(data -> (Character) data[0], data -> (Integer) data[1]))); public String encode(String bitString) throws EncodingException { // should only be 0 or 1 - if (!BITSTRING_VERIFICATION_PATTERN.matcher(bitString).matches()) { + if (isInvalidBitString(bitString)) { throw new EncodingException("Unencodable Base64Url '" + bitString + "'"); } bitString = pad(bitString); - String str = ""; + StringBuilder sb = new StringBuilder(); int index = 0; while (index <= bitString.length() - 6) { @@ -47,32 +45,40 @@ public String encode(String bitString) throws EncodingException { try { int n = FixedIntegerEncoder.decode(s); - Character c = AbstractBase64UrlEncoder.DICT.charAt(n); - str += c; + char c = AbstractBase64UrlEncoder.DICT.charAt(n); + sb.append(c); index += 6; } catch (DecodingException e) { throw new EncodingException("Unencodable Base64Url '" + bitString + "'"); } } - return str; + return sb.toString(); } public String decode(String str) throws DecodingException { - // should contain only characters from the base64url set - if (!BASE64URL_VERIFICATION_PATTERN.matcher(str).matches()) { - throw new DecodingException("Undecodable Base64URL string"); - } - - String bitString = ""; + StringBuilder bitString = new StringBuilder(); for (int i = 0; i < str.length(); i++) { char c = str.charAt(i); - Integer n = AbstractBase64UrlEncoder.REVERSE_DICT.get(c); + int n = AbstractBase64UrlEncoder.REVERSE_DICT.getOrDefault(c, -1); + if (n == -1) { + throw new DecodingException("Undecodable Base64URL string"); + } String s = FixedIntegerEncoder.encode(n, 6); - bitString += s; + bitString.append(s); } - return bitString; + return bitString.toString(); + } + + public static boolean isInvalidBitString(String bitString) { + for(int i = 0; i < bitString.length(); i++) { + char testChar = bitString.charAt(i); + if (testChar != '0' && testChar != '1') { + return true; + } + } + return false; } } diff --git a/iabgpp-encoder/src/main/java/com/iab/gpp/encoder/datatype/encoder/FixedIntegerEncoder.java b/iabgpp-encoder/src/main/java/com/iab/gpp/encoder/datatype/encoder/FixedIntegerEncoder.java index 2870af6..7fb0a7c 100644 --- a/iabgpp-encoder/src/main/java/com/iab/gpp/encoder/datatype/encoder/FixedIntegerEncoder.java +++ b/iabgpp-encoder/src/main/java/com/iab/gpp/encoder/datatype/encoder/FixedIntegerEncoder.java @@ -1,34 +1,34 @@ package com.iab.gpp.encoder.datatype.encoder; +import static com.iab.gpp.encoder.datatype.encoder.AbstractBase64UrlEncoder.isInvalidBitString; + import java.util.regex.Pattern; import com.iab.gpp.encoder.error.DecodingException; public class FixedIntegerEncoder { - private static Pattern BITSTRING_VERIFICATION_PATTERN = Pattern.compile("^[0-1]*$", Pattern.CASE_INSENSITIVE); - public static String encode(int value, int bitStringLength) { // let bitString = value.toString(2); - String bitString = ""; + StringBuilder bitString = new StringBuilder(); while (value > 0) { if ((value & 1) == 1) { - bitString = "1" + bitString; + bitString.insert(0, "1"); } else { - bitString = "0" + bitString; + bitString.insert(0, "0"); } value = value >> 1; } while (bitString.length() < bitStringLength) { - bitString = "0" + bitString; + bitString.insert(0, "0"); } - return bitString; + return bitString.toString(); } public static int decode(String bitString) throws DecodingException { - if (!BITSTRING_VERIFICATION_PATTERN.matcher(bitString).matches()) { + if (isInvalidBitString(bitString)) { throw new DecodingException("Undecodable FixedInteger '" + bitString + "'"); }