查看 UTF8 解码性能,我注意到 protobuf 的性能UnsafeProcessor::decodeUtf8
优于String(byte[] bytes, int offset, int length, Charset charset)
以下非 ascii 字符串"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
我认为差异是由于缺少我希望启动的边界检查消除,特别是因为checkBoundsOffCount(offset, length, bytes.length)
在String(byte[] bytes, int offset, int length, Charset charset)
.
问题真的是缺少边界检查消除吗?
这是我使用 OpenJDK 17 和 JMH 进行基准测试的代码。请注意,这只是String(byte[] bytes, int offset, int length, Charset charset)
构造函数代码的一部分,并且仅适用于这个特定的德语字符串。静态方法是从String
. 查找// the unsafe version:
表明我将安全访问替换为不安全的位置的注释。
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。
优化此代码最终使解码上述 Latin1 字符串的速度提高了 2.5 倍。
这种 C2 优化缩小了以下基准之间令人难以置信的超过7 倍的差距,commonBranchFirst
并将commonBranchSecond
在 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;
}
}