최근에 문자열 연결에 관한 문제가 발생했습니다. 이 벤치 마크는 다음을 요약합니다.
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class BrokenConcatenationBenchmark {
@Benchmark
public String slow(Data data) {
final Class<? extends Data> clazz = data.clazz;
return "class " + clazz.getName();
}
@Benchmark
public String fast(Data data) {
final Class<? extends Data> clazz = data.clazz;
final String clazzName = clazz.getName();
return "class " + clazzName;
}
@State(Scope.Thread)
public static class Data {
final Class<? extends Data> clazz = getClass();
@Setup
public void setup() {
//explicitly load name via native method Class.getName0()
clazz.getName();
}
}
}
JDK 1.8.0_222 (OpenJDK 64 비트 서버 VM, 25.222-b10)에서 다음과 같은 결과가 나타납니다.
Benchmark Mode Cnt Score Error Units
BrokenConcatenationBenchmark.fast avgt 25 22,253 ± 0,962 ns/op
BrokenConcatenationBenchmark.fast:·gc.alloc.rate avgt 25 9824,603 ± 400,088 MB/sec
BrokenConcatenationBenchmark.fast:·gc.alloc.rate.norm avgt 25 240,000 ± 0,001 B/op
BrokenConcatenationBenchmark.fast:·gc.churn.PS_Eden_Space avgt 25 9824,162 ± 397,745 MB/sec
BrokenConcatenationBenchmark.fast:·gc.churn.PS_Eden_Space.norm avgt 25 239,994 ± 0,522 B/op
BrokenConcatenationBenchmark.fast:·gc.churn.PS_Survivor_Space avgt 25 0,040 ± 0,011 MB/sec
BrokenConcatenationBenchmark.fast:·gc.churn.PS_Survivor_Space.norm avgt 25 0,001 ± 0,001 B/op
BrokenConcatenationBenchmark.fast:·gc.count avgt 25 3798,000 counts
BrokenConcatenationBenchmark.fast:·gc.time avgt 25 2241,000 ms
BrokenConcatenationBenchmark.slow avgt 25 54,316 ± 1,340 ns/op
BrokenConcatenationBenchmark.slow:·gc.alloc.rate avgt 25 8435,703 ± 198,587 MB/sec
BrokenConcatenationBenchmark.slow:·gc.alloc.rate.norm avgt 25 504,000 ± 0,001 B/op
BrokenConcatenationBenchmark.slow:·gc.churn.PS_Eden_Space avgt 25 8434,983 ± 198,966 MB/sec
BrokenConcatenationBenchmark.slow:·gc.churn.PS_Eden_Space.norm avgt 25 503,958 ± 1,000 B/op
BrokenConcatenationBenchmark.slow:·gc.churn.PS_Survivor_Space avgt 25 0,127 ± 0,011 MB/sec
BrokenConcatenationBenchmark.slow:·gc.churn.PS_Survivor_Space.norm avgt 25 0,008 ± 0,001 B/op
BrokenConcatenationBenchmark.slow:·gc.count avgt 25 3789,000 counts
BrokenConcatenationBenchmark.slow:·gc.time avgt 25 2245,000 ms
이는 부작용이있는 표현식이 새로운 체인의 최적화를 방해 하는 JDK-8043677 과 유사한 문제처럼 보입니다 StringBuilder.append().append().toString()
. 그러나 코드 Class.getName()
자체에는 부작용이없는 것 같습니다.
private transient String name;
public String getName() {
String name = this.name;
if (name == null) {
this.name = name = this.getName0();
}
return name;
}
private native String getName0();
여기서 가장 의심스러운 것은 실제로 한 번만 발생하는 기본 메소드 호출이며 결과는 클래스의 필드에 캐시됩니다. 내 벤치 마크에서 설정 방법으로 명시 적으로 캐시했습니다.
브랜치 예측자는 각 벤치 마크 호출에서 this.name의 실제 값이 null이 아니며 전체 표현식을 최적화한다는 것을 알 것으로 예상했습니다.
그러나 동안 BrokenConcatenationBenchmark.fast()
나는 이것을 가지고있다 :
@ 19 tsypanov.strings.benchmark.concatenation.BrokenConcatenationBenchmark::fast (30 bytes) force inline by CompileCommand
@ 6 java.lang.Class::getName (18 bytes) inline (hot)
@ 14 java.lang.Class::initClassName (0 bytes) native method
@ 14 java.lang.StringBuilder::<init> (7 bytes) inline (hot)
@ 19 java.lang.StringBuilder::append (8 bytes) inline (hot)
@ 23 java.lang.StringBuilder::append (8 bytes) inline (hot)
@ 26 java.lang.StringBuilder::toString (35 bytes) inline (hot)
즉, 컴파일러는 모든 것을 인라인 할 수 BrokenConcatenationBenchmark.slow()
있습니다.
@ 19 tsypanov.strings.benchmark.concatenation.BrokenConcatenationBenchmark::slow (28 bytes) force inline by CompilerOracle
@ 9 java.lang.StringBuilder::<init> (7 bytes) inline (hot)
@ 3 java.lang.AbstractStringBuilder::<init> (12 bytes) inline (hot)
@ 1 java.lang.Object::<init> (1 bytes) inline (hot)
@ 14 java.lang.StringBuilder::append (8 bytes) inline (hot)
@ 2 java.lang.AbstractStringBuilder::append (50 bytes) inline (hot)
@ 10 java.lang.String::length (6 bytes) inline (hot)
@ 21 java.lang.AbstractStringBuilder::ensureCapacityInternal (27 bytes) inline (hot)
@ 17 java.lang.AbstractStringBuilder::newCapacity (39 bytes) inline (hot)
@ 20 java.util.Arrays::copyOf (19 bytes) inline (hot)
@ 11 java.lang.Math::min (11 bytes) (intrinsic)
@ 14 java.lang.System::arraycopy (0 bytes) (intrinsic)
@ 35 java.lang.String::getChars (62 bytes) inline (hot)
@ 58 java.lang.System::arraycopy (0 bytes) (intrinsic)
@ 18 java.lang.Class::getName (21 bytes) inline (hot)
@ 11 java.lang.Class::getName0 (0 bytes) native method
@ 21 java.lang.StringBuilder::append (8 bytes) inline (hot)
@ 2 java.lang.AbstractStringBuilder::append (50 bytes) inline (hot)
@ 10 java.lang.String::length (6 bytes) inline (hot)
@ 21 java.lang.AbstractStringBuilder::ensureCapacityInternal (27 bytes) inline (hot)
@ 17 java.lang.AbstractStringBuilder::newCapacity (39 bytes) inline (hot)
@ 20 java.util.Arrays::copyOf (19 bytes) inline (hot)
@ 11 java.lang.Math::min (11 bytes) (intrinsic)
@ 14 java.lang.System::arraycopy (0 bytes) (intrinsic)
@ 35 java.lang.String::getChars (62 bytes) inline (hot)
@ 58 java.lang.System::arraycopy (0 bytes) (intrinsic)
@ 24 java.lang.StringBuilder::toString (17 bytes) inline (hot)
문제는 이것이 JVM의 적절한 동작인지 컴파일러 버그입니까?
일부 프로젝트에서 여전히 Java 8을 사용하고 있기 때문에 릴리스 업데이트 중 하나에서 수정되지 않으면 Class.getName()
핫 스팟에서 수동으로 호출을 올리는 것이 합리적 입니다.
PS 최신 JDK (11, 13, 14-eap)에서는 문제가 재현되지 않습니다.
답변
HotSpot JVM은 바이트 코드 당 실행 통계를 수집합니다. 동일한 코드가 다른 컨텍스트에서 실행되는 경우 결과 프로파일은 모든 컨텍스트에서 통계를 집계합니다. 이 효과를 프로파일 오염이라고 합니다.
Class.getName()
벤치 마크 코드뿐만 아니라 분명히 호출됩니다. JIT가 벤치 마크 컴파일을 시작하기 전에 다음 조건 Class.getName()
이 여러 번 충족 되었음을 이미 알고 있습니다 .
if (name == null)
this.name = name = getName0();
적어도이 지점을 통계적으로 중요하게 다루기에 충분한 시간. 따라서 JIT는이 분기를 컴파일에서 제외하지 않았으므로 부작용으로 인해 문자열 연결을 최적화 할 수 없습니다.
이것은 네이티브 메소드 호출 일 필요조차 없습니다. 규칙적인 필드 할당 만 부작용으로 간주됩니다.
다음은 프로파일 오염이 추가 최적화에 해를 끼치는 방법의 예입니다.
@State(Scope.Benchmark)
public class StringConcat {
private final MyClass clazz = new MyClass();
static class MyClass {
private String name;
public String getName() {
if (name == null) name = "ZZZ";
return name;
}
}
@Param({"1", "100", "400", "1000"})
private int pollutionCalls;
@Setup
public void setup() {
for (int i = 0; i < pollutionCalls; i++) {
new MyClass().getName();
}
}
@Benchmark
public String fast() {
String clazzName = clazz.getName();
return "str " + clazzName;
}
@Benchmark
public String slow() {
return "str " + clazz.getName();
}
}
이것은 기본적으로 getName()
프로파일 오염을 시뮬레이션하는 수정 된 벤치 마크 버전입니다 . getName()
새로운 객체에 대한 예비 호출 수에 따라 문자열 연결의 추가 성능이 크게 달라질 수 있습니다.
Benchmark (pollutionCalls) Mode Cnt Score Error Units
StringConcat.fast 1 avgt 15 11,458 ± 0,076 ns/op
StringConcat.fast 100 avgt 15 11,690 ± 0,222 ns/op
StringConcat.fast 400 avgt 15 12,131 ± 0,105 ns/op
StringConcat.fast 1000 avgt 15 12,194 ± 0,069 ns/op
StringConcat.slow 1 avgt 15 11,771 ± 0,105 ns/op
StringConcat.slow 100 avgt 15 11,963 ± 0,212 ns/op
StringConcat.slow 400 avgt 15 26,104 ± 0,202 ns/op << !
StringConcat.slow 1000 avgt 15 26,108 ± 0,436 ns/op << !
나는 그것을 버그 나 “적절한 행동”이라고 부를 수 없다. 이것이 바로 핫스팟에서 동적 적응 컴파일이 구현되는 방식입니다.
답변
약간 관련이 없지만 Java 9 및 JEP 280 이후 : Indify String Concatenation 이제 문자열 연결이 수행 invokedynamic
되고 그렇지 않습니다 StringBuilder
. 이 기사 에서는 Java 8과 Java 9 사이의 바이트 코드 차이점을 보여줍니다.
최신 Java 버전에서 벤치 마크를 다시 실행해도 문제가 표시되지 않으면 javac
컴파일러는 이제 새로운 메커니즘을 사용하기 때문에 버그가 거의 없습니다 . 최신 버전에 상당한 변화가있을 경우 Java 8 동작에 뛰어 들지 않는지 확실하지 않습니다.