[F-Lab 모각코 챌린지 7일차] C1 컴파일러, C2 컴파일러, 인라이닝, 루프 언롤링
학습 목표
- 팀코칭 질문으로 학습
TIL
- JIT 컴파일러
- C1 컴파일러
- 핫스팟
- C2 컴파일러
- 오버헤드 발생 감소
- 인라이닝
- 루프 언롤링
- 오버헤드 발생 감소
- C1 컴파일러
- 오버헤드
- 함수 호출
JIT 컴파일러 (C1C2 컴파일러)
Java는 기본적으로 JIT (Just-In-Time) 컴파일러를 이용하여 프로그램을 실행합니다. 이 컴파일러는 Java 바이트코드를 런타임에 기계 코드로 컴파일하는 역할을 합니다. 이 과정은 런타임 중에 발생하므로, 프로그램의 성능을 향상시키는 데 큰 역할을 합니다.
Oracle의 HotSpot JVM에서는 JIT 컴파일러가 두 가지, 즉 C1 (Client) 컴파일러와 C2 (Server) 컴파일러로 나뉩니다. 이 두 컴파일러는 각각 특정한 상황에 더 잘 맞도록 설계되었습니다.
C1 컴파일러는 메소드가 처음 호출될 때 빠르게 컴파일하는 것을 목표로 합니다. 이 컴파일러는 코드를 빠르게 실행 가능한 수준으로 최적화하며, 그 결과로 더 빠른 시작 시간 (startup time)을 제공합니다.
반면에 C2 컴파일러는 프로그램의 실행을 통해 수집된 런타임 데이터를 이용하여 코드를 더 깊게 분석하고 최적화합니다. 이 컴파일러는 실행 시간이 길어질수록 더 좋은 성능을 제공하는 "최적의 성능"을 목표로 합니다.
일반적으로, Java 애플리케이션은 두 컴파일러를 함께 사용합니다. 이는 "티어드 컴파일"이라는 기능을 통해 가능하며, 이 기능은 빠른 시작 시간을 제공하는 C1 컴파일러와 최적의 성능을 제공하는 C2 컴파일러의 장점을 결합합니다.
C1 컴파일러는 실제로 자주 사용되는 메소드나 루프 등의 "핫스팟"을 식별하고, 그러한 코드를 우선적으로 컴파일합니다.는 무슨 뜻이야??
"핫스팟"이라는 용어는 특정 프로그램의 코드에서 자주 실행되는 부분을 가리킵니다. 즉, 프로그램이 실행되는 동안 가장 많이 사용되는, 또는 CPU 시간을 가장 많이 소비하는 부분을 말합니다.
C1 컴파일러는 프로그램이 실행되면서 이러한 핫스팟을 식별합니다. 즉, JVM은 프로그램이 실행되는 동안 어떤 메소드나 루프가 가장 자주 호출되는지를 모니터링하고, 이런 정보를 바탕으로 어떤 코드 부분을 먼저 컴파일할지 결정합니다.
예를 들어, 어떤 메소드가 프로그램에서 수천 번 또는 수만 번 호출된다면, 그 메소드는 핫스팟으로 간주될 수 있습니다. 따라서 C1 컴파일러는 그러한 메소드를 먼저 컴파일하고 최적화합니다. 이렇게 하면 프로그램의 전체 성능이 향상될 수 있습니다.
이러한 방식은 프로그램의 실행 동안 실제로 어떤 코드가 자주 실행되는지에 따라 컴파일 전략을 동적으로 조정할 수 있게 해주므로, 전체적인 실행 성능을 크게 향상시킬 수 있습니다.
C2 컴파일러
C2 컴파일러는 Oracle의 HotSpot JVM에 있는 또 다른 JIT 컴파일러입니다. C1 컴파일러가 빠른 컴파일을 목표로 한다면, C2 컴파일러는 프로그램의 실행을 통해 수집된 런타임 데이터를 기반으로 더 깊은 분석과 최적화를 수행합니다. 이 과정은 코드를 더 효율적으로 실행할 수 있게 만들며, 프로그램의 실행 시간을 줄입니다.
C2 컴파일러는 다음과 같은 최적화를 수행합니다:
- 인라이닝: 이 최적화는 한 메소드에서 다른 메소드를 호출하는 경우에 적용됩니다. 호출하는 메소드의 본문에 호출된 메소드의 코드를 삽입하여 메소드 호출 오버헤드를 제거합니다.
- 루프 언롤링: 이 최적화는 루프의 반복을 줄입니다. 예를 들어, 10번 반복하는 루프를 2번 반복하는 루프로 바꾸고, 각 반복에서 루프 본문을 5번 실행하게 됩니다.
- 데드 코드 제거: 사용되지 않는 코드를 제거합니다.
- 상수 전파: 컴파일 시점에 알려진 상수 값을 사용하여 표현식을 간소화합니다.
이러한 최적화는 프로그램의 성능을 향상시키지만, 더 많은 컴파일 시간을 요구합니다. 따라서 C2 컴파일러는 일반적으로 프로그램의 초기 실행 단계에서는 사용되지 않으며, 프로그램이 더 오랫동안 실행될 때 사용됩니다. 이렇게 함으로써 프로그램의 성능은 시간이 지남에 따라 개선될 수 있습니다.
인라이닝
인라이닝(inlining)은 컴파일러 최적화의 한 종류로, 한 함수나 메서드에서 다른 함수나 메서드를 호출할 때 사용됩니다. 인라이닝은 호출하는 함수의 본문에 호출되는 함수의 코드를 직접 삽입하는 방법입니다.
이렇게 하면 함수 호출에 대한 오버헤드가 줄어들고, 실행 시간이 단축될 수 있습니다. 왜냐하면 함수를 호출할 때마다 발생하는 컨텍스트 스위치, 즉 호출자 함수의 실행을 일시 중단하고 호출된 함수로 제어를 넘기는 과정이 생략되기 때문입니다.
그러나 인라이닝은 주의해서 사용해야 합니다. 너무 많이 사용하면 컴파일된 코드의 크기가 커질 수 있습니다. 이는 메모리 사용량을 증가시키고, 캐시 효율성을 감소시킬 수 있습니다. 따라서 컴파일러는 일반적으로 코드의 크기와 실행 시간 사이에 균형을 맞추려고 시도합니다.
또한 모든 메소드나 함수가 인라이닝에 적합한 것은 아닙니다. 재귀 호출이나 매우 큰 함수 등은 인라이닝하기 어렵습니다. 그래서 일반적으로 작고 자주 호출되는 함수에 대해 인라이닝을 적용합니다.
루프 언롤링(loop unrolling)
루프 언롤링(loop unrolling)은 컴퓨터 프로그래밍에서 성능 최적화 기법 중 하나입니다. 이 기법은 루프의 반복을 명시적으로 풀어서 (즉, "언롤링"하여) 프로그램의 실행 시간을 줄이는데 사용됩니다.
기본적으로, 루프 언롤링은 루프의 각 반복을 별도의 코드 라인으로 바꾸는 과정입니다. 이를 통해 프로그램은 루프의 오버헤드 (예를 들어, 루프 카운터의 증가, 루프 종료 조건의 확인 등)를 줄일 수 있습니다.
예를 들어, 다음과 같은 루프가 있다고 생각해봅시다:
for (int i = 0; i < 4; i++) {
System.out.println(i);
}
루프 언롤링을 적용하면 이 코드는 다음과 같이 변경될 수 있습니다:
System.out.println(0);
System.out.println(1);
System.out.println(2);
System.out.println(3);
루프 언롤링은 일반적으로 컴파일러가 자동으로 수행하는 최적화입니다. 하지만 너무 많은 루프 언롤링은 코드의 크기를 증가시킬 수 있으며, 이는 캐시 사용률에 부정적인 영향을 미칠 수 있습니다. 따라서 루프 언롤링은 적절하게 사용해야 합니다.
오버헤드
컴퓨터 과학에서 "오버헤드(overhead)"는 어떤 작업을 수행하는 데 필요한 처리 시간, 메모리, 디스크 공간 등과 같은 추가적인 컴퓨팅 리소스를 말합니다.
오버헤드는 종종 특정 작업을 수행하기 위한 "직접적인" 비용 이외의 "간접적인" 비용을 의미합니다. 예를 들어, 파일을 다운로드하는데 필요한 데이터 패킷들을 인터넷을 통해 전송하는 데는 비용이 발생하지만, 패킷을 관리하고 오류를 확인하고 재전송하는 데도 추가적인 비용이 발생합니다. 이런 추가적인 비용을 "오버헤드"라고 합니다.
또 다른 예로, 함수 호출 오버헤드를 들 수 있습니다. 함수를 호출하려면 함수의 매개변수를 스택에 넣고, 반환 주소를 저장하고, 함수가 끝난 후에 원래의 실행 상태를 복구하는 등의 작업이 필요합니다. 이런 추가적인 작업들은 직접적인 함수의 실행과는 별개로 필요한 작업들로, 이들 또한 "오버헤드"로 간주됩니다.
오버헤드를 최소화하는 것은 시스템의 성능을 향상시키는 중요한 부분입니다. 때로는 특정 작업의 오버헤드를 줄이기 위해 다른 방법을 사용하거나, 그 작업을 덜 자주 수행하거나, 또는 작업을 병렬로 수행하는 등의 방법이 사용될 수 있습니다.
함수 호출 단계
- 매개변수 전달: 호출자는 호출되는 함수에게 필요한 정보(매개변수)를 전달합니다. 이 정보는 일반적으로 스택에 저장됩니다.
- 스택 프레임 생성: 호출된 함수의 실행을 위해 새로운 스택 프레임이 생성됩니다. 이 스택 프레임은 함수의 지역 변수를 저장하고, 호출자로부터 제어를 되돌려 받을 수 있는 방법을 제공합니다.
- 함수 실행: 호출된 함수의 코드가 실행됩니다.
- 값 반환: 함수가 값을 반환하면, 이 값은 호출자에게 전달됩니다.
- 스택 프레임 제거: 함수의 실행이 종료되면, 해당 스택 프레임이 제거됩니다.