在研究UTF8解码性能时,我注意到protobuf的UnsafeProcessor::decodeUtf8
对于以下非ascii字符串的性能优于String(byte[] bytes, int offset, int length, Charset charset)
: "Quizdeltagerne spiste jordbær med flØde, mens cirkusklovnen"
。
我尝试找出原因,所以我复制了String
中相关的代码,并用不安全的数组访问替换了数组访问,就像UnsafeProcessor::decodeUtf8
一样。
这是JMH基准测试结果:
Benchmark Mode Cnt Score Error Units
StringBenchmark.safeDecoding avgt 10 127.107 ± 3.642 ns/op
StringBenchmark.unsafeDecoding avgt 10 100.915 ± 4.090 ns/op
我认为差异是由于缺少边界检查消除,而我预计它会启动,特别是因为在 String(byte[] bytes, int offset, int length, Charset charset)
开始处有一个显式的边界检查调用 checkBoundsOffCount(offset, length, bytes.length)
。
问题真的是缺少边界检查消除吗?
这是我使用 OpenJDK 17 和 JMH 对其进行基准测试的代码。请注意,这仅是 String(byte[] bytes, int offset, int length, Charset charset)
构造函数代码的一部分,并且仅对此特定的德语字符串有效。
静态方法从 String
中复制。
查找指示我用不安全的方式替换了安全访问的 //不安全版本:
注释。
private static byte[] safeDecode(byte[] bytes, int offset, int length) {
checkBoundsOffCount(offset, length, bytes.length);
int sl = offset + length;
int dp = 0;
byte[] dst = new byte[length];
while (offset < sl) {
int b1 = bytes[offset];
// the unsafe version:
// int b1 = UnsafeUtil.getByte(bytes, offset);
if (b1 >= 0) {
dst[dp++] = (byte)b1;
offset++;
continue;
}
if ((b1 == (byte)0xc2 || b1 == (byte)0xc3) &&
offset + 1 < sl) {
// the unsafe version:
// int b2 = UnsafeUtil.getByte(bytes, offset + 1);
int b2 = bytes[offset + 1];
if (!isNotContinuation(b2)) {
dst[dp++] = (byte)decode2(b1, b2);
offset += 2;
continue;
}
}
// anything not a latin1, including the repl
// we have to go with the utf16
break;
}
if (offset == sl) {
if (dp != dst.length) {
dst = Arrays.copyOf(dst, dp);
}
return dst;
}
return dst;
}
跟进
显然,如果我将 while 循环的条件从 offset < sl
改为 0 <= offset && offset < sl
,那么我会在两个版本中获得类似的性能:
Benchmark Mode Cnt Score Error Units
StringBenchmark.safeDecoding avgt 10 100.802 ± 13.147 ns/op
StringBenchmark.unsafeDecoding avgt 10 102.774 ± 3.893 ns/op
结论
这个问题被HotSpot开发者们选中了,详情请见https://bugs.openjdk.java.net/browse/JDK-8278518。
优化这段代码最终成功提升了2.5倍以上的解码上述Latin1字符串的速度。
C2优化技术缩小了下面基准测试中commonBranchFirst
和commonBranchSecond
超过7倍的巨大差距,将会在Java 19中发布。
Benchmark Mode Cnt Score Error Units
LoopBenchmark.commonBranchFirst avgt 25 1737.111 ± 56.526 ns/op
LoopBenchmark.commonBranchSecond avgt 25 232.798 ± 12.676 ns/op
@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class LoopBenchmark {
private final boolean[] mostlyTrue = new boolean[1000];
@Setup
public void setup() {
for (int i = 0; i < mostlyTrue.length; i++) {
mostlyTrue[i] = i % 100 > 0;
}
}
@Benchmark
public int commonBranchFirst() {
int i = 0;
while (i < mostlyTrue.length) {
if (mostlyTrue[i]) {
i++;
} else {
i += 2;
}
}
return i;
}
@Benchmark
public int commonBranchSecond() {
int i = 0;
while (i < mostlyTrue.length) {
if (!mostlyTrue[i]) {
i += 2;
} else {
i++;
}
}
return i;
}
}
bytes
和offset
没有被修改的情况下。我认为你应该使用LinuxPerfAsmProfiler来查看执行的汇编代码。 - Sergey Tsypanov0 <= offset && offset < sl
,并将结果告诉您。 - Sergey Tsypanov