길이 2의 배열에서 다음 두 코드 조각을 고려하십시오.
boolean isOK(int i) {
for (int j = 0; j < filters.length; ++j) {
if (!filters[j].isOK(i)) {
return false;
}
}
return true;
}
과
boolean isOK(int i) {
return filters[0].isOK(i) && filters[1].isOK(i);
}
충분한 예열 후에이 두 조각의 성능이 비슷해야한다고 가정합니다. 여기 및 여기에
설명 된대로 JMH 마이크로 벤치마킹 프레임 워크를 사용하여이를 확인 했으며 두 번째 스 니펫이 10 % 이상 빠릅니다.
질문 : Java가 기본 루프 언 롤링 기술을 사용하여 첫 번째 스 니펫을 최적화하지 않은 이유는 무엇입니까?
특히 다음을 이해하고 싶습니다.
- 2 필터의 경우에 최적 인 코드를 쉽게 생성 할 수 있으며 다른 필터 수의 경우에도 작동 할 수 있습니다 (간단한 빌더를 상상해보십시오)
return (filters.length) == 2 ? new FilterChain2(filters) : new FilterChain1(filters)
. JITC가 동일한 작업을 수행 할 수 있습니까? - JITC가 ' filters.length == 2 '가 가장 빈번한 경우 임을 감지하고 예열 후이 경우에 가장 적합한 코드를 생성 할 수 있습니까? 수동으로 롤링 한 버전과 거의 동일해야합니다.
- JITC가 특정 인스턴스 가 매우 자주 사용되는 것을 감지 한 다음 이 특정 인스턴스에 대한 코드 를 생성 할 수 있습니까 (필터 수는 항상 2임을 알고 있습니까)?
업데이트 : JITC가 수업 수준에서만 작동한다는 답변을 받았습니다. 알았어
이상적으로는 JITC의 작동 방식에 대해 깊이 이해 한 사람으로부터 답변을 받고 싶습니다.
벤치 마크 실행 세부 사항 :
- 최신 버전의 Java 8 OpenJDK 및 Oracle HotSpot에서 시도한 결과는 비슷합니다.
- 사용 된 Java 플래그 : -Xmx4g -Xms4g -server -Xbatch -XX : CICompilerCount = 2 (팬시 플래그없이 유사한 결과를 얻음)
- 그건 그렇고, JMH가 아닌 루프에서 단순히 수십 억 번 실행하면 비슷한 런타임 비율을 얻습니다. 즉 두 번째 스 니펫이 항상 명확하게 빠릅니다.
일반적인 벤치 마크 출력 :
벤치 마크 (filterIndex) 모드 Cnt 점수 오류 단위
LoopUnrollingBenchmark.runBenchmark 0 avgt 400 44.202 ± 0.224 ns / op
LoopUnrollingBenchmark.runBenchmark 1 avgt 400 38.347 ± 0.063 ns / op
첫 번째 줄은 첫 번째 스 니펫에 해당하고 두 번째 줄은 두 번째 줄에 해당합니다.
완전한 벤치 마크 코드 :
public class LoopUnrollingBenchmark {
@State(Scope.Benchmark)
public static class BenchmarkData {
public Filter[] filters;
@Param({"0", "1"})
public int filterIndex;
public int num;
@Setup(Level.Invocation) //similar ratio with Level.TRIAL
public void setUp() {
filters = new Filter[]{new FilterChain1(), new FilterChain2()};
num = new Random().nextInt();
}
}
@Benchmark
@Fork(warmups = 5, value = 20)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public int runBenchmark(BenchmarkData data) {
Filter filter = data.filters[data.filterIndex];
int sum = 0;
int num = data.num;
if (filter.isOK(num)) {
++sum;
}
if (filter.isOK(num + 1)) {
++sum;
}
if (filter.isOK(num - 1)) {
++sum;
}
if (filter.isOK(num * 2)) {
++sum;
}
if (filter.isOK(num * 3)) {
++sum;
}
if (filter.isOK(num * 5)) {
++sum;
}
return sum;
}
interface Filter {
boolean isOK(int i);
}
static class Filter1 implements Filter {
@Override
public boolean isOK(int i) {
return i % 3 == 1;
}
}
static class Filter2 implements Filter {
@Override
public boolean isOK(int i) {
return i % 7 == 3;
}
}
static class FilterChain1 implements Filter {
final Filter[] filters = createLeafFilters();
@Override
public boolean isOK(int i) {
for (int j = 0; j < filters.length; ++j) {
if (!filters[j].isOK(i)) {
return false;
}
}
return true;
}
}
static class FilterChain2 implements Filter {
final Filter[] filters = createLeafFilters();
@Override
public boolean isOK(int i) {
return filters[0].isOK(i) && filters[1].isOK(i);
}
}
private static Filter[] createLeafFilters() {
Filter[] filters = new Filter[2];
filters[0] = new Filter1();
filters[1] = new Filter2();
return filters;
}
public static void main(String[] args) throws Exception {
org.openjdk.jmh.Main.main(args);
}
}
@Setup(Level.Invocation)
: 확실하지 않습니다 (javadoc 참조).
final
있지만 JIT는 클래스의 모든 인스턴스 가 길이 2의 배열을 얻는다는 것을 알지 못합니다 . createLeafFilters()
배열이 항상 2 길다는 것을 알기에 충분히 깊이 코드를 분석하고 분석하십시오. JIT 옵티마이 저가 코드에 깊이 들어가는 이유는 무엇입니까?