this question is slightly related to one I've asked on SO before.
I've modified the code from java.util.regex.Matcher.quoteReplacement(String) by dropping char-by-char iteration via String.charAt() in favour of String.toCharArray():
//before
public static String quoteReplacement(String s) {
if ((s.indexOf('\\') == -1) && (s.indexOf('$') == -1))
return s;
StringBuilder sb = new StringBuilder();
for (int i=0; i<s.length(); i++) {
char c = s.charAt(i);
if (c == '\\' || c == '$') {
sb.append('\\');
}
sb.append(c);
}
return sb.toString();
}
//after
public static String quoteReplacement(String s) {
if ((s.indexOf('\\') == -1) && (s.indexOf('$') == -1)) {
return s;
}
StringBuilder sb = new StringBuilder();
for (char c : s.toCharArray()) {
if (c == '\\' || c == '$') {
sb.append('\\');
}
sb.append(c);
}
return sb.toString();
}
This change brought significant improvement as per average execution time along with higher memory consumption, measured with this benchmark (full output can be found here):
latin length before after Units
quoteReplacement true 8 37.708 ± 0.335 32.737 ± 0.040 ns/op
quoteReplacement true 64 212.155 ± 1.359 146.926 ± 0.239 ns/op
quoteReplacement true 128 382.048 ± 6.600 266.333 ± 0.872 ns/op
quoteReplacement true 256 759.663 ± 14.673 508.559 ± 0.987 ns/op
quoteReplacement false 8 61.607 ± 0.278 57.685 ± 0.210 ns/op
quoteReplacement false 64 309.292 ± 1.089 236.214 ± 1.822 ns/op
quoteReplacement false 128 591.459 ± 5.455 437.748 ± 3.134 ns/op
quoteReplacement false 256 1135.112 ± 5.119 837.601 ± 10.067 ns/op
quoteReplacement:·gc.alloc.rate.norm true 8 88.008 ± 0.001 120.010 ± 0.001 B/op
quoteReplacement:·gc.alloc.rate.norm true 64 288.036 ± 0.001 432.038 ± 0.001 B/op
quoteReplacement:·gc.alloc.rate.norm true 128 512.065 ± 0.001 784.068 ± 0.001 B/op
quoteReplacement:·gc.alloc.rate.norm true 256 944.121 ± 0.001 1472.128 ± 0.001 B/op
quoteReplacement:·gc.alloc.rate.norm false 8 176.015 ± 0.001 208.018 ± 0.001 B/op
quoteReplacement:·gc.alloc.rate.norm false 64 592.062 ± 0.001 736.064 ± 0.001 B/op
quoteReplacement:·gc.alloc.rate.norm false 128 1088.115 ± 0.001 1360.118 ± 0.001 B/op
quoteReplacement:·gc.alloc.rate.norm false 256 2064.217 ± 0.002 2592.225 ± 0.001 B/op
Later on I've applied the same approach to another JDK class - java.io.DataOutputStream:
//before
public final void writeBytes(String s) throws IOException {
int len = s.length();
for (int i = 0 ; i < len ; i++) {
out.write((byte)s.charAt(i));
}
incCount(len);
}
//after
public final void writeBytes(String s) throws IOException {
int len = s.length();
for (char c : s.toCharArray()) {
out.write((byte)c);
}
incCount(len);
}
To measure the impact I've used this benchmark:
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Fork(jvmArgsAppend = {"-Xms2g", "-Xmx2g"})
public class DataOutputStreamBenchmark {
@Benchmark
public Object writeBytes(Data data) throws IOException {
var string = data.string;
var baos = new ByteArrayOutputStream();
var dataOutputStream = new DataOutputStream(baos);
dataOutputStream.writeBytes(string);
return baos;
}
@State(Scope.Thread)
public static class Data {
private String string;
@Param({"8", "64", "256"})
private int length;
@Setup
public void setup() throws IOException {
String alphabet = "abcdefghijklmnopqrstuvwxyz";
RandomStringGenerator generator = new RandomStringGenerator();
string = generator.randomString(alphabet, length);
}
}
}
And the results are quite confusing:
length before after Units
writeBytes 8 150.922 ± 0.135 96.896 ± 0.229 ns/op
writeBytes 64 1158.181 ± 1.179 1124.765 ± 3.810 ns/op
writeBytes 256 4597.537 ± 9.825 4835.081 ± 4.610 ns/op
writeBytes:·gc.alloc.rate.norm 8 112.018 ± 0.001 144.018 ± 0.001 B/op
writeBytes:·gc.alloc.rate.norm 64 192.036 ± 0.005 376.065 ± 0.004 B/op
writeBytes:·gc.alloc.rate.norm 256 608.115 ± 0.020 1176.203 ± 0.011 B/op
While for the short string the change was indeed fruitful, for the longer ones it brought degradation. The fact is confusing to me, because the both benchmarks do mostly the same: the put a byte (in case of latin alphabet) into a byte[] in either StringBuilder or ByteArrayOutputStream and check bounds of that array.
Could one explain this behaviour or give an advice where I've got to look into to get some better understanding of what's going on under the hood? I suspect there's a kind of magic related to StrinBuilder.append(char) done by C2 in case of Matcher.quoteReplacement().