공부하고 기록하는, 경제학과 출신 개발자의 노트

학습일지/Language

JIT Compiler

inspirit941 2021. 3. 11. 09:11
반응형

Just in Time Compilation, and the Code Cache

스크린샷 2021-03-05 오후 3 19 44

자바 컴파일러가 .java -> .class 바이트코드 파일로 변경. 이 변경된 .class 파일이 JVM 위에서 실행된다.

  • 바이트코드로 변환된 후 jvm에서 runtime에 실행되는 구조 : 어느 하드웨어에서건 동일한 실행이 가능하다.
  • 단순히 바이트코드 변환하는 용도뿐만 아니라, 일반적인 인터프리터보다 효율적으로 동작하도록 설계되어 있음.

기본적으로 인터프리터는 코드 한 줄씩 런타임에서 읽어들여 실행하는 구조. 느린 편이다.

 

스크린샷 2021-03-05 오후 4 48 04

 

JVM의 바이트코드 interpretation은 느린 속도를 해결하기 위해 JIT compilation (Just in time compilation) 을 사용한다.

  • 어떤 코드가 가장 자주 실행되는지를 파악하고 (보통 loop인 코드가 해당함)
  • 자주 쓰이는 코드는 JVM이 machine code로 컴파일해서 속도를 높임. (해당 OS가 읽어들일 수 있는 형태로 변환)
  • 실행기록들을 모아서, 자주 쓰이는 코드들을 native code로 변환하는 작업을 JIT Compilation이 수행한다.

즉, 프로그램을 오래 실행할수록 프로그램의 실행속도가 빨라진다.

 

바이트코드를 native machine code로 변환하는 작업은 JVM 내 별도의 쓰레드에서 이루어진다. JVM은 멀티쓰레드 프로그램이므로, 당연히 실행중인 프로그램은 이 변환작업에 영향을 받지 않는다.

 

compilation 작업이 끝나면, JVM은 Seamlessly switch to use the compiled version instead of more byte code.

만약 CPU Bound 무거운 작업을 하고 있는 프로그램이라면, JIT compiler가 작동 중일 때에는 프로그램이 느려진 걸 체감할 수도 있다.

 

스크린샷 2021-03-05 오후 5 06 36

 

어떤 종류의 compilation이 내부적으로 작동하는지 확인할 수 있는 명령어.

  • -XX : advanced Option을 의미함.
  • + / - : 해당 옵션의 on / off 설정.
  • 옵션 이름.
  • flag 이름은 대소문자 구별한다.

해당 애플리케이션의 every bit of compiling that is going on within the VM을 확인할 수 있다.

 

스크린샷 2021-03-05 오후 5 15 48

  • 가장 왼쪽 column : 프로그램 시작 후 몇 ms가 지났는지
  • 그 다음 column : 어느 코드블록이 컴파일되었는지
  • 그 다음 column의 n : native method. s: synchronized method. %의 경우 해당 코드는 natively compiled되었고,
    코드 캐시라는 특정 메모리 공간에 저장되었다는 것을 의미함. -> 가장 최적의 방식으로 코드를 실행하고 있다는 의미임.

스크린샷 2021-03-05 오후 5 21 59

 

소수를 input 개수만큼 구하는 (2,3,5,7...) 메소드의 input을 5000으로 만들었을 때

  • isPrime이라는 메소드에 %표시가 되어 있다. 자주 호출하는 메소드를 코드 캐시에 따로 저장했고, 따라서 가장 최적의 상태로 코드를 실행하고 있다는 뜻.
  • 0부터 4까지 있는 숫자의 의미
    • 0: No compilation. 해당 코드는 바로 interpreted되었다는 뜻
    • 1~4 : 해당 숫자만큼의 deeper compilation이 반영되었다는 뜻. %가 붙은 코드의 level은 4 -> code cache로 전환되었음. 그런데, 같은 level 4인데도 어떤 코드는 %가 붙지 않았다.

왜 이런 상황이 발생했을까?


스크린샷 2021-03-05 오후 5 39 06

기본적으로 자바의 JIT Compiler는 두 개다.

  • C1과 C2. C1은 level 1 ~ 3까지, C2는 level 4를 담당한다. 이 레벨체계를 compilation tier라고도 부른다.
  • JVM은 해당 코드블록이 어느 컴파일러를 사용할지를 '코드의 사용빈도', 'complexity / time consuming'을 토대로 결정한다.
    -> 이 작업을 "profiling" 이라고 부른다.
    • 기본적으로는 C1 컴파일러를 사용하되, Level 4에 도달하면 C2를 사용한다. C2는 C1보다 더 최적화된 코드를 생성한다.
    • 또한 JVM에서 해당 코드를 code cache에 저장할지 결정한다. (special area of memory -> to access quickest way)
  • 모든 코드를 level 4 단계로 컴파일하지는 않는다. 자주 쓰인다 해도, complex한 작업을 담당하는 코드가 아니라면 굳이 code cache에 넣지 않음. (code cache까지 집어넣었을 때의 이점이 별로 없다.)

print로 console을 확인할 수 없을 때에는 아래 option이 도움이 된다.

java -XX:+UnlockDiagnosticVMOptions -XX:+LogCompilation <실행클래스 이름> <필요한 파라미터>


실행결과로 아래와 같은 log파일을 볼 수 있다.

스크린샷 2021-03-05 오후 5 52 36

 

 




Code cache의 크기는 제한되어 있다. 많은 양의 코드가 이곳에 저장되다 보면, 몇몇 코드는 이곳에서 제거되기도 한다. 즉, 매우 큰 애플리케이션과 많은 메소드가 존재하는 애플리케이션이라면, 코드 블록이 cache에 Moved in-out이 이루어진다.

이런 상황이라면, code cache의 크기를 늘리는 게 방법이 될 수 있다. 보통 이 상황이 오면 VM에서 CodeCache is full. Compiler has been disabled 경고 메시지가 뜬다. 코드 캐시에 저장된 모든 코드블록이 전부 사용중이라서 뭘 뺄 수도 없다는 것.

  • 코드캐시 크기를 확인하는 플래그: -XX:PrintCodeCache
  • 코드캐시는 해당 자바의 버전에 따라 다름. Java 8 이상 + 64bit JVM일 경우 약 240MB가 최대크기라고 함.

스크린샷 2021-03-05 오후 6 01 23

  • initialCodeCacheSize : 컴퓨터 메모리에 따라 다르지만, 대부분 160KB 정도 됨.
  • Reserved... : max size. 프로그램 내에서 자동으로 올릴 수 있는 최대 코드캐시 크기를 말한다.
  • CodeCacheExpansionSize: 코드캐시 크기를 늘릴 때, 한 번에 얼마나 크기를 증가시킬지를 지정하는 값.

 

 


 

코드캐시 사용량을 원격 서버에서 확인하고 싶은 경우 - JDK에서 제공하는 Jconsole로 확인 가능하다.

 

자바 애플리케이션이 처음 실행될 때, Jconsole이 모니터링하는 특정 폴더에 해당 애플리케이션 관련 정보를 저장한다.
윈도우의 경우 이 폴더의 권한에 writable이 없을 수 있으므로, 그걸 설정해줘야 한다.

  • Users -> 사용자이름 -> AppData -> Local -> Temp -> hsper<이름>_<이름> 형태의 폴더를 찾는다. Jconsole이 작동하기 위해서는 이 폴더에 쓰기 권한이 있어야 한다.

스크린샷 2021-03-05 오후 11 31 20

 

단, Jconsole로 remote 연결하는 건 권장하지 않음. VM에서 연결을 위한 추가 작업을 해야 하고, 이 작업에 필요한 코드들이 컴파일되어야 한다. -> 실제 코드캐시에 사용되는 것보다 대략 2MB가 더 필요하다. 즉 Jconsole을 활용해서 code cache를 모니터링하려면 그만한 메모리가 필요하다는 점에 유의.

 

32bit / 64bit 차이점

OS에 따라서 선택할 버전이 다름.

스크린샷 2021-03-06 오후 1 04 20

 

  • 32bit의 경우, 메모리 포인터 하나에 할당된 크기가 작다. 하지만 운영체제에서 지원할 수 있는 메모리 크기도 작다. 컴파일러도 C1 컴파일러 하나뿐이다.
    • 따라서 heap 사이즈가 3GB 미만인 경우 적절하고, 필요한 작업을 실행한 후 종료되는 형태의 애플리케이션 (Client Application)
    • 짧게 실행되고 끝나는 애플리케이션의 경우 startup time이 중요하다. 또한 금방 종료될 애플리케이션이므로 native code compilation이 필요한 정도의 메소드도 적기 마련.
  • 64bit의 경우, 더 큰 자료형 (Long, Double)을 사용할 경우 속도가 빠르다. 또한 heap 사이즈도 4GB 이상일 때 효과가 있다.
    • 최대 heap 사이즈는 OS에 따라 다르며, 컴파일러도 C1, C2 두 개 다 존재한다. 따라서 오래도록 실행되어야 하는 애플리케이션에 적절하다. (Server Application)
    • 상대적으로 startup time이 덜 중요하다.

맥과 같은 OS의 경우 32bit가 없다. 이 경우, runtime에서 어떤 컴파일러를 사용할 것인지 flag로 결정할 수 있다. client compiler (C1) 만 사용할 경우 빠른 startup time을 제공할 수 있음.

-client 설정으로 적용 가능함.

  • 몇몇 os에서는 -client 설정을 해도 C2 컴파일러를 사용하는 경우가 있다. 그렇다 해도 startup time을 줄여주는 역할은 함. (analysis of the code를 덜 하기 때문.)

스크린샷 2021-03-06 오후 6 51 31

 

간단히 테스트했을 때 이런 차이가 났다고 함.
프로그램마다, 실행환경마다 조금씩 다르지만 startup 속도를 빠르게 하는 건 확실하다.

 

스크린샷 2021-03-06 오후 6 52 05

 

  • -server : 32bit의 server compiler 선택
  • -d64 : 64bit server compiler 선택. windows / linux의 32bit 운영체제에서는 이걸 쓰면 에러가 발생한다. 반대로 64bit 운영체제에서는 -d64만 사용해야 함.

결론적으로, server 면에서는 이론적인 차이일 뿐이다. client flag는 여전히 의미가 있음.

 

  • -XX:-TieredCompilation : code cache 사용하는 일 없이, 그냥 인터프리터로만 코드 실행하도록 설정.

JVM은 기본적으로 언제, 어떤 메소드를 native code cache로 컴파일할지를

  • 해당 메소드가 호출된 횟수
  • 해당 메소드에 loop가 존재하는지

자세히 들어가면 너무 기술적인 내용이라 깊게는 다루지 않을 예정.

 

보통 애플리케이션의 퍼포먼스에 영향을 주는 요소는 두 가지.

  1. How many Threads available to run this compiling process.
  2. What's the threshold for native compilation.

두 가지 모두 flag 설정으로 추가할 수 있다.

설정 확인하기: java -XX:+PrintFlagsFinal에서 CICompilerCount 값 확인. 아래 예시의 경우 값이 3이며, 다시말해 compiling code에 사용되는 쓰레드 개수가 3개라는 뜻이다.

 

스크린샷 2021-03-06 오후 8 53 09

 

또는 jinfo 명령어로 볼 수 있는데, 이 명령어를 쓰려면 해당 자바가 실행되는 프로세스를 알아야 한다.

  • jps: 자바 프로세스의 id값을 확인할 수 있다
  • jinfo -flag CICompilerCount <process_id> 형태로도 확인 가능.

실제로 CompilerCount개수를 변경하려면 java -XX:CICompilerCount=6 -XX+PrintCompilation <실행할클래스명> <필요한 파라미터> 형태로 실행하면 된다. 컴파일을 많이 해야 하는 프로그램일수록 이 쓰레드 개수를 늘렸을 때 실행속도가 단축되는 효과가 있다.

컴파일 쓰레드의 최솟값은 2. Client Compiler / Server Compiler 각 하나씩.

Threshold: 해당 메소드를 native machine code로 컴파일하려면 최소 몇 번은 실행되어야 하는지의 기준치.

  • 이 값을 설정하는 법: -XX:CompileThreshold=n.
  • jinfo -flag CompileThreshold <process_id>를 찍어보면 값을 알 수 있다.
    • 예컨대 10,000일 경우, 메소드가 대략 10000번 실행된 메소드일 경우 compile해서 native code로 변경한다는 의미.
    • 물론 실제로는 더 복잡하며, 단순히 호출횟수만 세는 걸로 결정되지는 않는다.
반응형

'학습일지 > Language' 카테고리의 다른 글

Writing Beautiful Package in Go  (0) 2022.05.29
Go - Context 정리  (0) 2022.05.26
[Design Pattern] Facade  (0) 2020.12.28
[Design Pattern] Strategy  (0) 2020.12.24
[Design Pattern] Bridge  (0) 2020.12.23