OpenJDK C2 컴파일러의 노드 클래스 이해하기
들어가며
Java 프로그램을 실행하면 JVM 내부에서는 여러 단계의 컴파일이 일어난다. 그 중 C2 컴파일러는 자주 실행되는 코드(“핫 코드”)를 고도로 최적화된 기계어로 변환하는 역할을 한다.
이 글에서는 C2 컴파일러가 코드를 최적화할 때 사용하는 노드(Node) 클래스가 무엇인지, 어떤 계층 구조를 가지는지를 초보자 눈높이에서 설명해본다.
노드(Node)란?
C2 컴파일러는 Java 바이트코드를 바로 기계어로 번역하지 않는다. 대신, 먼저 코드를 Ideal Graph라는 중간 표현(IR, Intermediate Representation)으로 변환한다.
이 Ideal Graph는 노드(Node)들의 네트워크로 구성된다. 각 노드는 하나의 연산을 나타낸다.
예를 들어, 다음과 같은 Java 코드가 있다고 해보자.
int result = a + b;
이 코드는 내부적으로 대략 이런 노드들로 표현된다.
LoadNode(a) ─┐
├─ AddNode ─── StoreNode(result)
LoadNode(b) ─┘
LoadNode: 메모리에서 값을 읽어오는 노드AddNode: 두 값을 더하는 노드StoreNode: 결과를 메모리에 저장하는 노드
이렇게 모든 연산이 노드로 표현되고, 노드들이 서로 연결되어 하나의 그래프를 형성한다.
함수 호출도 마찬가지다. 예를 들어 다음과 같은 코드가 있다고 해보자.
int len = str.length();
이 코드는 대략 이런 노드들로 표현된다.
LoadNode(str) ─── CallNode(String::length) ─── StoreNode(len)
CallNode: 메서드 호출을 나타내는 노드
산술 연산은 AddNode 같은 단순한 노드로 표현되지만, 메서드 호출은 CallNode로 표현된다. CallNode는 호출 규약, 예외 처리, 메모리 부수효과 등 훨씬 많은 정보를 다뤄야 하기 때문에 별도의 노드 계층으로 분류된다.
노드 클래스의 계층 구조
C2 컴파일러에는 수백 가지의 노드 타입이 있다. 이것들은 객체 지향적으로 잘 정리된 클래스 계층 구조를 가지고 있다.
Node ← 모든 노드의 최상위 클래스
├── MemNode ← 메모리에 접근하는 노드들
│ ├── LoadNode ← 메모리에서 값을 읽는 노드
│ │ ├── LoadFNode ← float 값을 읽는 노드
│ │ ├── LoadDNode ← double 값을 읽는 노드
│ │ ├── LoadINode ← int 값을 읽는 노드
│ │ └── ...
│ └── StoreNode ← 메모리에 값을 쓰는 노드
│ ├── StoreFNode ← float 값을 쓰는 노드
│ ├── StoreDNode ← double 값을 쓰는 노드
│ └── ...
├── AddNode ← 덧셈 노드
│ ├── AddFNode ← float 덧셈
│ └── AddINode ← int 덧셈
├── MulNode ← 곱셈 노드
├── CmpNode ← 비교 노드
├── CallNode ← 함수 호출 노드
└── ...
핵심 포인트: MemNode과 CallNode
이 계층 구조에서 특히 중요한 두 가지 노드 분류가 있다.
- MemNode: 메모리에 접근하는 노드다.
LoadNode와StoreNode가 대표적이며,is_Mem()메서드로 확인할 수 있다. - CallNode: 함수 호출을 나타내는 노드다.
is_Call()메서드로 확인할 수 있다.
반면에, AddFNode(float 덧셈)이나 CmpFNode(float 비교) 같은 노드들은 메모리에 직접 접근하지 않는 순수 연산 노드이므로 MemNode도 CallNode도 아니다.
정리
- C2 컴파일러는 Java 코드를 노드(Node)로 구성된 그래프로 표현한다.
- 노드는 계층 구조를 가지며, 특히
MemNode(메모리 접근)과CallNode(함수 호출)이 중요한 분류다. MemNode을 상속하는 노드는 메모리에 접근하므로 별도의 처리가 필요하고, 순수 연산 노드(AddF, CmpF 등)는 메모리에 접근하지 않으므로 상대적으로 단순하게 다뤄진다.