JAVA JIT 컴파일러 /고급 컴파일러 튜닝 정리

25gStroy·2022년 5월 31일
0

JAVA

목록 보기
5/18

고급 컴파일러 튜닝

컴파일이 동작하는 방법에 대한 상세 내역의 일부를 다루며 처리 과정에서 영향을 줄 수 있는 부가적인 튜닝을 소개하겠습니다.

하지만 이 값을 변경한다고 크게 달라지는 부분은 없습니다.

컴파일 스레드

컴파일 임계치에서 메소드가 컴파일 대상이 되면 컴파일 큐에 들어가 대기하게 됩니다.

큐는 한개 이상의 백그라운드 스레드에 의해 처리됩니다. 이건 컴파일이 비동기 프로세스로 대상 코드가 컴파일 중인 동안에도 계속해서 프로그램이 실행된다는 의미입니다.

메소드가 일반 컴파일을 한다면 다음 메소드를 호출하는 경우 컴파일된 메소드를 실행할 것입니다.

루프가 OSR을 통해 컴파일 된다면 루프가 다음 반복될 때 바로 컴파일된 코드를 실행합니다.

컴파일 큐는 FIFO 방식은 아닙니다. 호출 카운터 가 더 높은 메소드가 우선순위가 더 높습니다. 즉 가장 중요한 코드가 먼저 컴파일되게 해줍니다.

컴파일이 동작하는 방법에 대한 상세 내역의 일부를 다루며 처리 과정에서 영향을 줄 수 있는 부가적인 튜닝을 소개하겠습니다.

하지만 이 값을 변경한다고 크게 달라지는 부분은 없습니다.

티어드 컴파일을 수행할 때 JVM은 대상 플랫폼의 CPU 개수와 이중 로그에 관한 복잡한 방정식에 근거해 디폴트 스레드를 정합니다.

컴파일러 스레드 개수는 -X:CICompilerCount=N 플레그 설정을 통해서 조정이 가능합니다.

그럼 언제 이값을조정해야 할까요?

프로그램이 단일 cpu에서 실해오딘다면 스래드는 한개만 있는편이 났습니다.

준비 기간에서는 컴파일러 스레드가 더 많으면 스타트업 시간에는 우위가 있을 것입니다. 하지만 현실에서는 이 이득이 그렇게 크지 않습니다.

준비 기간 이후에는 많이 쓰이는 메소드들이 점점 컴파일 되므로 CPU를 두고 경쟁하는 일이 없어질 것이므로 CPU 보다 적게 스레드를 두는게 낫습니다.

컴파일 스레드에 또 다른 설정은 XX:+BackgroundCompilation 플래그 입니다. 기본값은 true로 큐가 비동기 방식으로 처리되는 것을 말합니다.

플래그를 false로 설정한다면 메소드가 컴파일 되면 그걸 실행하길 원하는 코드는 실제로 컴파일될 때까지 기다리게 됩니다.

인라이닝

컴파일러가 하는 최적화 기법 중 하나는 인라인 메소드로 만드는것입니다.

훌륭한 객체 지향 설계를 따르는 코드는 때로 getter(), setter()로 접근되는 메소드가 많습니다.

이런 메소드의 호출로 인한 오버헤드는 생각외로 매우 큽니다. 그래서 자바 초창기에 이런 종류의 캡슐화에 대한 반대 의견도 많았습니다.

다행히 JVM은 이런 종류의 메소드들을 기계적으로 인라인으로 만들 수 있습니다.

예는 다음과 같습니다.

Point p = getPoint(); 
p.setX(p.getX() * 2)

위코드에서 인라인 메소드로 컴파일된 코드는 다음과 같습니다.

Point p = getPoint(); 
p.x = p.x * 2;

인라이닝은 디폴트로 사용 가능합니다. 실제로 반드시 사용해야 할 정도로 성능을 매우 효과적으로 향상시킵니다.

하지만 -XX:-Inline 플래그를 통해 사용하지 않을 수도 있습니다.

기본적으로 메소드의 인라인화를 결정하는 요소는 얼마나 자주 호출되는 가와 메소드의 크기 입니다.

JVM은 내부 연산을 바탕으로 메소드가 얼마나 인기 있는지를 판단합니다.

이런 자주 호출되는 메소드의 경우에는 바이트 코드 크기가 325바이트보다 작다면 인라인화 됩니다.

이 크기는 =XX:MaxFreqInlineSize=N 플래그로 설정할 수 있습니다.

자주 호출되지 않는 메소드의 경우에는 바이트 코드가 35바이트보다 작다면 인라인화 됩니다.

이 크기는 -XX:MaxInlineSize=N 플래그로 설정할 수 있습니다.

때로 MaxInlineSize를 보다 높여서 더 많은 메소드를 인라인화 하기도 합니다. 하지만 35 바이트보다 키운다면 메소드가 처음 호출될 때 인라인화 될지도 모릅니다.

그리고 자주 호출되는 메소드의 경우 325바이트보다 작다면 인라인화 되므로 애플리케이션에 크게 영향을 주진 않습니다.

탈출 분석

탈출 분석(Escape Analysis)은 객체의 스코프를 분석해서 특정 스코프를 벗어날 수 있는지 여부를 판단해 최적화를 적용하는 기법입니다.

탈출 분석(Escape Analysis)은 기본적으로 활성화 되있습니다. -XX:+DoEscapeAnalysis 플래그로 설정 할 수 있습니다.

예를 들어 다음 팩토리얼 클래스를 보겠습니다.

public class Factorial {
    private BigInteger factorial;
    
    private int n;

    public Factorial(int n) {
        this.n = n;
    }
    
    public synchronized BigInteger getFactorial(){
        if(factorial == null) {
            factorial = BigInteger.valueOf(0);
        }
        return factorial;
    }
}

....

// 팩토리얼 값 100개를 저장
List<BigInteger> list = new ArrayList<>();

for (int i = 0; i < 100 ; i++) {
    Factorial factorial = new Factorial(i);
    list.add(factorial.getFactorial());
}

위의 코드 실행 흐름을 보면 팩토리얼 객체의 스코프는 루프 내에서만 참조 됩니다. 그 외 다른 코드에서 팩토리얼 객체를 접근할 순 없습니다.

이와 같은 탈출 분석을 통해서 JVM은 최적화를 할 수 있습니다.

  • getFactorial() 메소드를 호출할 때 동기화 락(synchronization lock)을 걸 필요가 없습니다.
  • 팩토리얼 객체에 있는 메모리 필드에 n을 저장할 필요가 없습니다. 어짜피 루프가 끝나면 사라지므로 그냥 레지스터에서 값을 유지하면 됩니다.
  • 팩토리얼 객체를 힙에 할당하지 않아도 됩니다. 어짜피 루프가 끝나면 사라지므로 그냥 로컬 스택을 사용해도 됩니다.

이런 종류의 최적화는 꽤 복잡합니다. 그리고 때론 탈출 분석의 최적화는 더 느려지는 경우가 되기도 합니다.

이 경우에는 코드를 단순하게 만든다면 해결할 수 있습니다.

profile
애기 개발자

0개의 댓글