Page tree
Skip to end of metadata
Go to start of metadata

컴퓨터는 우리가 하는 말을 이해할 수 없다.

그렇기 때문에 컴퓨터가 알아들을 수 있는 말(바이너리 코드 혹은 머신 코드)로 바꿔줘야한다.

우리들은 흔히 프로그래밍 언어 (java, C, C++, PHP, Perl)를 이용해서 컴퓨터에게 명령한다.

각각의 프로그래밍 언어들은 각자의 방식으로 컴퓨터가 이해할 수 있는 바이너리 코드로 번역하는데 대표적으로 두 가지 방법.

컴파일러

실행하기 전에 프로그램 코드를 기계어(머신코드 or 바이너리코드)로 번역합니다.

인터프리터

고급 명령어들을 중간 형태로 번역함 다음, 그것을 실행한다. 이 방법은 각 라인이 실행되면서 프로그램의 각 코드를 한 줄씩 바이너리 코드로 변역한다.

동일한 프로그램 소스 코드는 머신이 같은 인터프리터(즉 PHP나 PERL과 같은 프로그램)를 가지고 있으면 어떤 CPU에서든 실행할 수 있다.(이식성)


"인터프리트된 코드는 거의 언제나 컴파일된 코드보다 느리다."

  • 한 번에 코드 한 줄 씩 살펴보는 인터프리터는 다음 라인의 코드 정보를 충분히 갖고 있지 않다.
  • 반면에 좋은 컴파일러는 충분한 정보를 가지고 있다. 그래서 최적의 순서로 바이너리 코드의 순서를 정한다.

JAVA

자바는 절충안을 찾으려고 한다. JVM을 이용해 플랫폼 독립적이고 JIT 컴파일러라는 좋은 컴파일러를 이용한 성능의 장점도 있다.

JVM 과 JIT 컴파일러

  • 저스트 인 타임(JIT) 컴파일러는 JVM의 핵심이다.
  • JVM 내에서 컴파일러보다 성능에 더 영향을 주는 요소는 없다. 런타임 시 바이트 코드를 원시 시스템 코드로 컴파일하여 애플리케이션의 성능을 향상시킨다.
  • JIT 컴파일러는 위의 컴파일과 인터프리트 두 가지 방식을 혼합한 방식이다. 실행 시점에서는 인터프리터와 같이 기계어 코드를 생성하면서 해당 코드가 컴파일 대상이 되면 컴파일하고 그 코드를 캐싱한다.

    • JVM내의 컴파일러 동작
    1. HelloWorld.java 소스코드 작성
    2. javac.exe(Java 컴파일러)를 이용해 최적화된 어셈블리 언어로 컴파일된다. 이는 바이트코드(HelloWorld.class)를 뜻한다.
    3. JVM 에서 각 운영체제에 맞는 기계어로 번역합니다.


    • JIT(Just In Time) 컴파일러

      - 핫스팟 컴파일 : 
      a. 대부분 사용하는 JVM은 오라클의 스탠다드 JVM(hotspot JVM)이다. 
      b. 핫스팟이라는 이름은 코드 컴파일에 대한 접근법에서 유래했다. 
      c. 일반적인 프로그램에서 전체 코드 중 일부만 자주 실행 되며 애플리케이션의 성능은 주로 이 영역의 코드가 얼마나 빨리 실행되는가에 의해 좌우된다. 
      d. 이 영역을 애플리케이션의 핫스팟이라 한다.

      - 동작방식 : 
      a. 실행할 때 컴파일을 하면서 해당 코드를 캐싱한다.(코드 캐쉬) 
      b. 그 이후에는 바뀐 부분만 컴파일 하고 나머지는 캐싱된 코드를 사용한다. 
      c. JVM은 코드를 바로 (바이너리 코드) 컴파일 하지 않는다. 그 이유는 대표적으로 두가지가 있다.
    1. 코드가 한 번만 실행된다면 컴파일은 헛수고다. 한 번만 실행되는 프로그램이라면 컴파일해서 컴파일된 코드를 한 번만 실행하는 것보다 자바 바이트 코드를 인터프리트하는 편이 더 빠를 것이다. 하지만 자주 호출되는 메서드거나 많이 반복 실행되는 루프라면 컴파일할 가치가 있다. 처음엔 코드를 컴파일하기 위해 시간이 걸리겠지만 다음 번 호출부터는 그만큼 시간을 절약할 수 있다. 이것이 바로 컴파일러가 다른 일을 하기 전에 인터프리트된 코드를 실행하는 이유이다. 이 과정을 통해 컴파일러는 컴파일하기에 충분한 코드가 무엇인지 알아낼 수 있다.
    2. 최적화. 다음의 코드를 보자.

      RegisterTest
      public class RegisterTest {
          private int sum;
      
          public void calculateSum(int n) {
              for(int i = 0; i < n; i++) {
                  sum += i;
              }
          }
      }

위의 코드에서 가장 중요한 최적화 중 하나를 살펴볼 수 있다. 컴파일러가 다음의 두가지를 결정하도록 하는 것이다.

    1) 메인 메모리에서 값을 사용할 시기
    2) 레지스터 내의 값을 저장할 시기

sum 변수는 인스턴스 변수이므로 메인 메모리 내에 있어야 하지만, 루프가 반복될 때마다 sum 값을 메인 메모리에서 조회/저장해야 한다면 성능은 형편없어진다. 대신 컴파일러는 sum의 초기 값을 레지스터로 로드하고 레지스터내의 값을 이용해 루프를 수행한 다음 결과 값을 메인 메모리에 저장한다. 레지스터를 이용하는 건 컴파일의 일반적인 최적화 방법이다.

- 클라이언트 컴파일러, 서버 컴파일러, 티어드 컴파일러.

      • JIT 컴파일러는 두 가지 형태로 나뉜다. 두 컴파일러의 차이는 컴파일 있어서의 적극성이다.
        1. 클라이언트 컴파일러 : 컴파일하는 시기가 빠르다.
        2. 서버 컴파일러 : 컴파일하는 시기가 느리다. 
        3. 티어드 컴파일 : 두 가지 컴파일러를 모두 사용.

클라이언트 컴파일러는 서버 컴파일러보다 상대적으로 더 많은 코드를 컴파일했기 때문에 코드가 실행되기 시작하면 클라이언트 컴파일러가 더 빠를 것이다. 하지만 상대적으로 늦게 컴파일하는 서버 컴파일러는 그만큼 코드를 검사하는 시간을 갖고 그만큼 최적화된 코드 컴파일을 할 수 있다. 애플리케이션 개발자는 프로그램이 수행되는 기간과 초기 스타트업 시간의 중요도를 기반으로 컴파일러를 선택할 수 있다. 다른 선택사항도 있다. 바로 티어드 컴파일이다. 초기에는 클라이언트 컴파일러를 사용하고 많이 쓰이게 되면 서버 컴파일러로 다시 컴파일된다.

이제 어떤 상황에서 어떤 컴파일러를 사용해야하는지 알아보자.


애플리케이션클라이언트서버티어드 컴파일
HelloWorld0.080.080.08
NetBeans2.833.923.07
BigApp51.554.052.0


위의 표는 각자 다른 애플리케이션의 스타트업 시간이다.

  1. 단순한 HelloWorld 애플리케이션에서는 차이점을 알아차리기 어렵다.
  2. NetBeans는 중간 크기의 자바 GUI 애플리케이션으로 스타트업할 때 약 10,000개의 클래스를 로드하고 몇개의 그래픽객체를 초기화시키는 등의 작업을 한다.
  3. 마지막으로 BigApp이 있다. 20,000개 이상의 클래스를 로드하고 막대한 초기화를 수행하는 대규모 서버 프로그램이다. 이건 애플리케이션 서버이므로 미래를 생각한다면 분명히 서버 컴파일러를 사용할 필요가 있다. 역시 스타트업 시간은 클라이언트 컴파일러가 빠르다. 하지만 JVM 외 다른 문제들로 인해 압도적으로 빠르진 않다.


주식의 개수클라이언트서버티어드 컴파일
10.142초0.176초0.165초
100.211초0.348초0.226초
1000.454초0.674초0.472초
1,0002.556초2.158초1.910초
10,00023.78초14.03초13.56초

위의 표는 배치 애플리케이션을 실행하기 위한 시간을 측정한것이다.N개의 주식에 대해 1년치의 이력(이력의 평균과 표준편차)을 요청한다.

  1. 100개만 처리하는 것이라면 클라이언트 컴파일러가 최선의 선택이다. 그 후 성능상의 이점은 생각했던 것과 같이 서버 컴파일러(특히 티어드 컴파일을 하는 서버 컴파일러) 쪽으로 기운다.
  2. 흥미로운 사실은 티어드 컴파일이 항상 서버 컴파일러보다 약간 더 빠르다. 이말은 즉, 서버 컴파일러를 통해 코드 컴파일을 진행할 경우 절대로 컴파일이 되지 않는 코드 영역이 존재한다는 말이다.
  3. 그 영역이 컴파일되지 않고 인터프리티드 모드에서 실행되므로 그 영역까지 컴파일하는 티어드 컴파일보다 느리게된다. 사실 애플리케이션이 영원히 실행된다 해도 서버 컴파일러는 절대 모든 코드를 컴파일하지 않는다. (그 이유는 바로 컴파일 임계치라는 개념에 있다. 이 부분은 밑에 설명하겠다.)

- 코드 캐쉬 튜닝

      • 코드 캐시는 고정 크기이며 가득 차면 JVM은 더 이상 코드를 추가적으로 컴파일할 수 없다.
      • 코드 캐시가 너무 작다면 충분한 코드 컴파일이 이뤄지지 않아 많은 양의 인터프리트된 코드를 실행하게 될 것이다. 코드 캐시가 차면 JVM 은 그 영행에 대해 다음과 같이 경고한다. "CodeCache is full."
      • 특정 애플리케이션이 필요로 하는 코드 캐시가 어느 정도인지는 알 수 없다. 일반적으로 단순히 디폴트의 두 배 또는 네 배로 늘려본다.
      • 코드 캐시의 최대 크기는 -XX:ReservedCodeCacheSize=N 플레그를 통해 설정된다.
      • 코드 캐시 크기를 큰 값을 지정하면 불이익이 있을까? 그건 해당 머신의 사용가능한 자원에 달려있다. 해당 메모리는 필요로 하기 전까지는 할당되지 않지만 여전히 예약되어 있으므로 예약 조건을 만족시키기 위해 머신에 이용 가능한 메모리가 충분해야만 한다는 것을 의미한다.
      • 코드 캐시는 jconsole을 이용해 모니터링할 수 있다.


- 컴파일 임계치

      • 앞서 설명한 바와 같이 JIT 컴파일러가 컴파일하는 조건은 얼마나 자주 코드가 실행됐는가 이다. 일정한 횟수만큼 실행되고 나면 컴파일 임계치에 도달하고 컴파일러는 컴파일하기에 충분한 정보가 쌓였다고 생각한다.
      • 임계치는 메서드가 호출된 횟수, 메서드의 루프를 빠져 나오기까지 돈 횟수 두 개를 기반으로 한다. 이 두 수의 합계를 확인하고 메서드가 컴파일될 자격이 있는지 여부를 결정한다. 자격이 있다면 메서드는 컴파일되기 위해 큐에서 대기한다.(일반 컴파일)
      • 아주 오래동안 돌아가는 루프문의 카운터가 임계치를 넘어가면 해당 루프는 컴파일 대상이 된다. JVM은 루프를 위한 코드의 컴파일이 끝나면 루프가 다시 반복될 때는 코드를 컴파일된 코드로 교체하고 더 빠르게 실행된다. 이 교체 과정을 "스택상의 교체(on-stack replacement, ORS)"라고 부른다.
      • XX:CompileThreshold=N (클라이언트 컴파일 : 1,500, 서버 컴파일 : 10,000)
      • CompileThreshold 플래그를 변경하도록 추천한다. 특히 서버 컴파일러에서 컴파일 임계치를 낮추면 덜 최적으로 코드가 컴파일될 위험이 있지만, 8,000 정도로 낮웠을 경우 최적화에 한해 거의 차이가 없다. 다음의 두 가지 이유를 참고하기 바란다.
        1) 애플리케이션이 워밍업하는데 필요한 시간을 약간 절약한다.
        2) 절대로 컴파일되지 않았을 일부 서버 메서드들을 컴파일할 수 있다. 주기적으로 (JVM이 세이프포인트(safepoint)에 이르렀을 때 ) 각 카운터의 값은 감소한다. 사실상 임계치 카운터란 메서드나 루프의 최근 움직임에 대한 상대적 측정 값이다.

요약

  •  저스트 인타임 컴파일러의 튜닝 관점에서 봤을 때 거의 모든 것에 대해 티어드 컴파일을 사용하는 편이 간단하다.(컴파일러 관련 성능 이슈의 90%를 해결)
  • 코드 캐쉬는 충분히 크게 잡고, 컴파일러의 가능한 성능 대부분을 사용해야한다.

예상 질문.

1. Machine Code와 binary code, assembly code는 어떻게 다른가?
2. interpreting 없이 모든 코드를 바로 compile 후 code cache에 저장하면 안될까?
3. JIT 컴파일러는 사용 하지 않을 수 있나? 사용하지 않을 수 있다면 어떻게 동작할까?



이미지 출처 : d2.naver.com/helloworld

  • No labels

1 Comment

  1. client compiler vs server compiler

    two different binaries

    1. Client compiler does not try to execute many of the more complex optimizations, but server compiler did
    2. Server compiler contains an advanced adaptive compiler
      - the same types of optimizations which c++ compiler did
      - some optimizations that cannot be done by traditional compilers
      - adaptive optimization technology is very flexible