为什么“ new”关键字比赋值效率高得多?
问题内容:
我有两种方法可以读取字符串并创建Character对象:
static void newChar(String string) {
int len = string.length();
System.out.println("Reading " + len + " characters");
for (int i = 0; i < len; i++) {
Character cur = new Character(string.charAt(i));
}
}
和
static void justChar(String string) {
int len = string.length();
for (int i = 0; i < len; i++) {
Character cur = string.charAt(i);
}
}
当我使用18554760字符串运行方法时,我得到的运行时间截然不同。我得到的输出是:
newChar took: 20 ms
justChar took: 41 ms
使用较小的输入(4,638,690个字符)时,时间没有变化。
newChar took: 12 ms
justChar took: 13 ms
在这种情况下,为什么新的效率更高?
编辑:
我的基准代码很hacky。
start = System.currentTimeMillis();
newChar(largeString);
end = System.currentTimeMillis();
diff = end-start;
System.out.println("New char took: " + diff + " ms");
start = System.currentTimeMillis();
justChar(largeString);
end = System.currentTimeMillis();
diff = end-start;
System.out.println("just char took: " + diff+ " ms");
问题答案:
TL; DR部分
好消息
您的测量确实显示出真实的效果。
坏消息
它之所以这样做是偶然的,因为您的基准测试存在许多技术缺陷,而它所暴露的效果可能并非您所想到的。
当且仅当 HotSpot的转义分析成功证明可以将生成的实例安全地分配到堆栈而不是堆上时,该new Character()
方法才会更快。因此,效果不如您的问题所暗示的那么普遍。 __
效果说明
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
- 有证据表明,在和变体中都消除了 某些 死代码(DCE),但这只是部分的;
justChar``newChar
- 对于
newChar
变体,添加charValue
没有任何作用,因此显然是DCE的; - 与
justChar
,charValue
确实有作用,因此似乎没有被消除; newCharUsed
和之间的稳定差异可以证明DCE的总体影响较小justCharUsed
。