박종훈 기술블로그
close menu

JDK-8382880 기여기: C2 Final Reshape Counts의 dead code 제거

이번 글에서는 OpenJDK의 JDK-8382880 이슈에 기여하며, C2 컴파일러의 최종 그래프 재구성 단계에서 사용되던 카운트 필드들이 어떤 역할을 했고 왜 제거되었는지를 정리해본다.

이슈의 제목과 내용은 다음과 같다.

C2: Remove unused Final_Reshape_Counts::get_*_count() methods

JDK-8351152 removed the last calls to Final_Reshape_Counts::get_*_count() which are now unused. This can be cleaned up.

이슈 내용

JDK-8351152에서 UseSSE < 2 관련 코드가 제거되면서, Final_Reshape_Counts 구조체의 다음 getter 메서드들이 더 이상 호출되지 않게 되었다.

int get_call_count () const { return _call_count ; }
int get_float_count () const { return _float_count ; }
int get_double_count() const { return _double_count; }

이슈는 이 미사용 getter들을 정리하는 것이 목적이다.

코드를 살펴보니, getter를 제거하면 대응하는 필드(_call_count, _float_count, _double_count)와 increment 메서드(inc_call_count(), inc_float_count(), inc_double_count())도 더 이상 사용처가 없어진다는 것을 알 수 있었다. getter뿐 아니라 이들까지 함께 정리하기로 했다.

Final_Reshape_Counts란

Final_Reshape_Counts는 C2 JIT 컴파일러의 최종 그래프 재구성(final graph reshaping) 단계에서 사용되는 구조체다.

C2는 Java 바이트코드를 Ideal Graph라는 중간 표현(IR)으로 변환한 뒤 다양한 최적화를 수행하고, 마지막에 이 그래프를 기계어 매칭 가능한 형태로 변환한다. 여기서 Ideal Graph란, 연산 하나, 변수 하나, 상수 하나가 각각 노드(Node)로 표현되는 그래프 자료구조다. 예를 들어 a + b * c라는 코드는 Add, Mul, a, b, c 각각이 노드가 된다. Final_Reshape_Counts는 이 마지막 단계에서 그래프를 순회하면서 노드 타입별 통계를 수집하는 역할을 한다.

FPU 모드와 제거된 카운트의 과거 용도

이 카운트들이 왜 존재했는지 이해하려면, x87 FPU의 정밀도 모드를 알아야 한다.

x87 FPU란

x87 FPU는 Intel이 1980년에 출시한 8087 칩에서 시작된 부동소수점 전용 보조 프로세서 계열이다. 초기 CPU(8086 등)는 정수 연산만 할 수 있었기 때문에, 부동소수점 연산을 하려면 별도의 칩(8087)을 꽂아야 했다. 이후 80486부터 FPU가 CPU에 내장되었고, 이 내장 FPU가 여전히 x87 명령어 세트를 사용하기 때문에 “x87 FPU”라는 이름이 남아있다.

x87 FPU 정밀도 모드

x87 FPU는 부동소수점 연산의 정밀도(precision)를 제어하는 모드 비트를 가지고 있다.

  • 24-bit (single precision) — float 연산용
  • 53-bit (double precision) — double 연산용
  • 64-bit (extended precision) — x87 기본값

Java는 float은 32비트, double은 64비트로 엄격하게 정의하는데, x87 FPU는 내부적으로 80비트 확장 정밀도로 계산한다. Java 스펙에 맞추려면 FPU 제어 워드를 전환해야 했다.

float 연산과 double 연산이 섞여 있으면 정밀도 모드를 24-bit와 53-bit 사이에서 반복적으로 전환해야 하는데, 이 전환에는 비용이 발생한다. 현대 x86에서는 SSE/SSE2 명령어를 사용하므로 x87 FPU 모드 전환이 필요 없다. SSE는 float/double을 각각 별도 명령어로 처리하기 때문이다.

JDK-8351152에서 제거된 최적화 코드

JDK-8351152에서 제거된 코드는 다음과 같다.

#ifdef IA32
  // If original bytecodes contained a mixture of floats and doubles
  // check if the optimizer has made it homogeneous, item (3).
  if (UseSSE == 0 &&
      frc.get_float_count() > 32 &&
      frc.get_double_count() == 0 &&
      (10 * frc.get_call_count() < frc.get_float_count()) ) {
    set_24_bit_selection_and_mode(false, true);
  }
#endif // IA32

SSE 없이(UseSSE == 0) x87 FPU만 사용하는 32비트 x86(IA32)에서, float 연산이 32개 이상이고 double 연산이 없으며 함수 호출이 float 연산의 1/10 미만이면, FPU를 24-bit 정밀도 모드로 고정하는 최적화였다. float만 쓰는 메서드라면 모드 전환 비용을 아예 제거할 수 있었던 것이다. 단, 함수 호출이 많으면 callee가 다른 정밀도를 사용할 수 있어 이 최적화를 포기한다.

이 코드가 get_float_count(), get_double_count(), get_call_count()의 유일한 호출부였고, 이것이 제거되면서 세 getter가 미사용 상태가 된 것이다.

수정 내용

파일: src/hotspot/share/opto/compile.cpp

1. 미사용 getter 제거

이슈에서 직접적으로 요청한 부분이다. 더 이상 호출되지 않는 다음 메서드들을 제거했다.

int get_call_count () const { return _call_count ; }
int get_float_count () const { return _float_count ; }
int get_double_count() const { return _double_count; }

2. 필드와 increment 메서드 제거

getter가 사라지면서 대응하는 필드와 increment 메서드도 dead code가 되었다. 함께 제거했다.

  • 필드: _call_count, _float_count, _double_count
  • 메서드: inc_call_count(), inc_float_count(), inc_double_count()

3. 호출부에서 inc_* 호출 제거

final_graph_reshaping_main_switch() 내의 다음 호출들을 제거했다.

  • frc.inc_float_count() (2곳)
  • frc.inc_double_count() (2곳)
  • frc.inc_call_count() (2곳)

4. case label은 유지

float/double 관련 Op 노드들의 case label을 제거하면 default case로 빠지면서 assert(!n->is_Mem()) assertion이 발생한다. 따라서 case label은 유지하되, 카운트 증가 없이 break만 수행하도록 변경했다.

5. if/else 구조 정리

frc.inc_call_count()만 수행하던 if 분기를 제거하고, else 분기(uncommon call의 shared argument clone)의 조건을 반전시켜 정리했다.

정리 후 남은 필드

참고로, 이번 정리 대상이 아닌 Final_Reshape_Counts의 나머지 필드들은 다음과 같은 역할을 한다.

필드설명
_java_call_count인라인되지 않은 Java 호출 수
_inner_loop_count정렬이 필요한 내부 루프 수
_visited노드 방문 여부 추적용 비트셋
_tests분기 노드 목록 (무결성 검사용)

_tests는 그래프 순회 중 만나는 분기 노드들을 모아두는 리스트다. IfNode(if-else 조건 분기, 나가는 경로 2개)와 PCTableNode(switch문이나 예외 처리 분기, 나가는 경로 여러 개)을 수집해두었다가, 나중에 “나가는 경로 수가 맞는지” 검증한다. 예를 들어 IfNode는 true/false 2개의 경로가 있어야 하는데, 최적화 과정에서 한쪽이 도달 불가능(dead code)으로 제거되면 경로가 1개만 남을 수 있다. 이런 불일치를 잡아내는 무결성 검사용이다.

_java_call_count_inner_loop_count는 코드 생성 직전에 노드 수를 예측하는 데 사용된다. 코드 생성 단계에서 Ideal Node들이 MachNode(기계어 명령 노드)로 변환되면서, 정렬용 NOP이나 호출 관련 보조 명령어가 추가되어 노드 수가 늘어난다. 노드가 너무 많아지면 메모리를 과도하게 사용하게 되므로, 미리 예측해서 한도를 초과하면 C2 컴파일을 포기하고 인터프리터나 C1으로 폴백한다.

  • _java_call_count: Java 호출 하나당 약 3개의 MachNode가 추가로 생긴다고 추정하여 노드 수 예측에 반영한다. 또한 Java 호출이 하나라도 있으면 stack banging(스택 공간 사전 체크) 코드를 삽입하여 스택 오버플로우를 미리 감지한다.
  • _inner_loop_count: 루프는 성능을 위해 메모리 주소 정렬(alignment)을 맞추는데, 이때 NOP 명령어를 패딩으로 삽입한다. 루프 하나당 최대 (OptoLoopAlignment - 1)개의 NOP 노드가 추가될 수 있으므로, 이를 미리 반영한다.

이번에 제거된 _call_count, _float_count, _double_count는 이와는 다른 용도로, 과거 x87 FPU 모드 최적화를 위해 존재하던 필드들이다.

마무리

코드 변경 자체는 단순한 dead code 제거지만, 히스토리를 따라가면서 C2 컴파일러의 최종 그래프 재구성 단계가 어떤 일을 하는지, 그리고 현대 x86에서는 더 이상 필요 없어진 x87 FPU 모드 최적화가 과거에 어떤 역할을 했는지를 알 수 있었다. 매번 새로운 것을 배우게 된다.

부동소수점을 이해하는것은 항상 쉽지 않다.

categories: 개발

tags: JDK , OpenJDK , HotSpot , C2 , 오픈소스 기여