简短总结
好消息
您的测量结果确实显示出了真正的效果。
坏消息
这主要是因为您的基准测试存在许多技术缺陷,而且它所显示的效果可能不是您想要的那个。
new Character()
方法更快,但仅当HotSpot的逃逸分析成功地证明生成的实例可以安全地分配在堆栈上时才会如此。因此,其效果并不像您的问题中暗示的那样普遍。
效果的解释
new Character()
更快的原因是“引用局部性”:您的实例位于堆栈上,所有对它的访问都通过CPU高速缓存命中进行。当您重用缓存的实例时,您必须:
- 访问远程的
static
字段;
- 将其反引用到远程数组;
- 将数组条目反引用到远程的
Character
实例;
- 访问该实例中包含的
char
。
每次反引用都有可能导致CPU高速缓存未命中。此外,它会强制将高速缓存的一部分重定向到这些远程位置,从而导致输入字符串和/或堆栈位置更多的高速缓存未命中。
详细信息
我已经使用 jmh
运行了此代码:
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@BenchmarkMode(Mode.AverageTime)
public class Chars {
static String string = "12345678901234567890"; static {
for (int i = 0; i < 10; i++) string += string;
}
@GenerateMicroBenchmark
public void newChar() {
int len = string.length();
for (int i = 0; i < len; i++) new Character(string.charAt(i));
}
@GenerateMicroBenchmark
public void justChar() {
int len = string.length();
for (int i = 0; i < len; i++) Character.valueOf(string.charAt(i));
}
}
这样可以保留代码的精华,但消除一些系统性错误,例如预热和编译时间。以下是结果:
Benchmark Mode Thr Cnt Sec Mean Mean error Units
o.s.Chars.justChar avgt 1 3 5 39.062 6.587 usec/op
o.s.Chars.newChar avgt 1 3 5 19.114 0.653 usec/op
这可能是我对发生的情况的最佳猜测:
- 在 `newChar` 中,您正在创建 `Character` 的一个新实例。 HotSpot 的逃逸分析可以证明该实例永远不会逃逸,因此它允许堆栈分配,或者在 `Character` 的特殊情况下,可以完全消除分配,因为可以明显地证明从中提取的数据永远不会被使用;
- 在 `justChar` 中,您涉及查找 `Character` 缓存数组,这会产生一些成本。
更新
回应Aleks的批评,我添加了更多的基准测试方法。主要效果保持稳定,但我们获得了更细粒度的有关较小优化效果的详细信息。
@GenerateMicroBenchmark
public int newCharUsed() {
int len = string.length(), sum = 0;
for (int i = 0; i < len; i++) sum += new Character(string.charAt(i));
return sum;
}
@GenerateMicroBenchmark
public int justCharUsed() {
int len = string.length(), sum = 0;
for (int i = 0; i < len; i++) sum += Character.valueOf(string.charAt(i));
return sum;
}
@GenerateMicroBenchmark
public void newChar() {
int len = string.length();
for (int i = 0; i < len; i++) new Character(string.charAt(i));
}
@GenerateMicroBenchmark
public void justChar() {
int len = string.length();
for (int i = 0; i < len; i++) Character.valueOf(string.charAt(i));
}
@GenerateMicroBenchmark
public void newCharValue() {
int len = string.length();
for (int i = 0; i < len; i++) new Character(string.charAt(i)).charValue();
}
@GenerateMicroBenchmark
public void justCharValue() {
int len = string.length();
for (int i = 0; i < len; i++) Character.valueOf(string.charAt(i)).charValue();
}
描述:
- 基础版本是
justChar
和newChar
;
...Value
方法在基础版本上增加charValue
调用;
...Used
方法隐式添加charValue
调用,并使用该值以防止死代码消除。
结果:
Benchmark Mode Thr Cnt Sec Mean Mean error Units
o.s.Chars.justChar avgt 1 3 1 246.847 5.969 usec/op
o.s.Chars.justCharUsed avgt 1 3 1 370.031 26.057 usec/op
o.s.Chars.justCharValue avgt 1 3 1 296.342 60.705 usec/op
o.s.Chars.newChar avgt 1 3 1 123.302 10.596 usec/op
o.s.Chars.newCharUsed avgt 1 3 1 172.721 9.055 usec/op
o.s.Chars.newCharValue avgt 1 3 1 123.040 5.095 usec/op
- 在
justChar
和newChar
变量中,有一些死代码消除(DCE)的证据,但只是部分的;
- 使用
newChar
变量时,添加charValue
没有任何效果,因此显然已经被DCE了;
- 对于
justChar
,charValue
确实有影响,所以似乎没有被消除;
- DCE对整体影响较小,如
newCharUsed
和justCharUsed
之间稳定的差异所示。
jmh
中的结果:对于我的字符串,justChar
花费了38微秒,而newChar
花费了19微秒。 - Marko Topolnik