Skip to content

Commit d455e73

Browse files
add post 'JDK-8379512 / JDK-8379513 기여기: C2 매크로 노드 플래그 동기화와 guaranteed_safepoint 개선'
Signed-off-by: jonghoonpark <dev@jonghoonpark.com>
1 parent 455524b commit d455e73

1 file changed

Lines changed: 266 additions & 0 deletions

File tree

Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
---
2+
layout: "post"
3+
title: "JDK-8379512 / JDK-8379513 기여기: C2 매크로 노드 플래그 동기화와 guaranteed_safepoint 개선"
4+
description: "OpenJDK JDK-8379512와 JDK-8379513에 기여한 과정을 소개합니다. C2 컴파일러에서 매크로 노드의 Flag_is_macro 플래그와 _macro_nodes 리스트의 불일치를 해소하고, boxing CallStaticJavaNode에 대한 guaranteed_safepoint() 오버라이드를 추가하여 JDK-8378005의 임시 workaround를 제거한 작업을 정리합니다."
5+
categories:
6+
- "개발"
7+
tags:
8+
- "JDK"
9+
- "OpenJDK"
10+
- "HotSpot"
11+
- "C2"
12+
- "오픈소스 기여"
13+
date: "2026-05-11 00:00:00 +0900"
14+
toc: true
15+
---
16+
17+
이번 글에서는 서로 연결된 두 이슈 [JDK-8379512](https://bugs.openjdk.org/browse/JDK-8379512)[JDK-8379513](https://bugs.openjdk.org/browse/JDK-8379513)에 기여한 과정을 정리한다. 매크로 노드 플래그 관리를 중앙화하는 작업과, 그 과정에서 `guaranteed_safepoint()`의 boxing method 처리를 개선한 작업이다.
18+
19+
## 이슈 내용
20+
21+
### JDK-8379512: 매크로 노드 플래그 동기화
22+
23+
`add_macro_node()``remove_macro_node()``_macro_nodes` 리스트만 조작하고, 노드의 `Flag_is_macro` 플래그는 건드리지 않는다. 플래그는 각 노드 생성자가 개별적으로 호출하여 설정해야 했다. 하지만 로직상 그럴 필요가 없다. `add_macro_node()``remove_macro_node()` 호출 시 함께 처리되도록 개선 한다.
24+
25+
### JDK-8379513: guaranteed_safepoint()의 boxing method 처리
26+
27+
`guaranteed_safepoint()`가 boxing `CallStaticJavaNode`에 대해 `true`를 반환하는데, 실제로는 해당 노드가 매크로 확장으로 제거될 수 있어서 safepoint가 보장되지 않는다. 이 문제는 [JDK-8378005](https://bugs.openjdk.org/browse/JDK-8378005)에서 발견되었고, 당시에는 `loopnode.cpp`에서 boxing method를 별도로 예외 처리하는 임시 workaround로 해결했다. JDK-8379512에서 매크로 노드 관리를 정리한 뒤 이 workaround를 올바르게 대체하는 것이 이 이슈의 목표다.
28+
29+
## 배경 지식
30+
31+
### 매크로 노드와 Flag_is_macro
32+
33+
매크로 노드(macro node)란, 최적화 단계에서는 고수준 형태를 유지하다가 나중에 저수준 노드들로 확장(expand)되어야 하는 노드를 말한다. `Flag_is_macro`는 이 매크로 노드에 달리는 플래그로, "이 노드는 아직 최종 형태가 아니다"라는 표시다.
34+
35+
C2 컴파일러의 흐름을 보면 이해가 쉽다.
36+
37+
```
38+
파싱 → 최적화 (GVN, 루프 등) → 매크로 확장 → 매칭 → 코드 생성
39+
```
40+
41+
매크로 노드는 최적화 단계에서 고수준 형태를 유지하다가, 매크로 확장 단계에서 저수준 노드들로 분해된다.
42+
43+
### 매크로 노드 예시
44+
45+
고수준 형태를 유지하면 최적화에 유리하다. 대표적인 예시를 보자.
46+
47+
#### AllocateNode — escape analysis로 할당 제거
48+
49+
escape analysis는 객체가 메서드 밖으로 빠져나가는지(escape) 분석한다. `new Point(x, y)`로 만든 객체가 메서드 안에서만 쓰이고 밖으로 나가지 않으면, 굳이 힙에 할당할 필요 없이 객체를 만들지 않고 내부 필드값만 꺼내서 CPU 레지스터나 스택에 놓는 것(scalar replacement)으로 대체할 수 있다.
50+
51+
하지만 `AllocateNode`가 이미 TLAB 할당 + GC 배리어 등 수십 개 노드로 확장된 뒤라면, "이것이 하나의 할당이었다"는 정보가 사라져서 이 분석이 불가능해진다.
52+
53+
#### LockNode — lock coarsening/elimination
54+
55+
`LockNode`는 Java의 `synchronized` 블록이 C2 IR에서 표현된 것이다.
56+
57+
같은 객체에 대해 `synchronized` 블록이 연속으로 나타나면 lock/unlock을 두 번 하던 것을 한 번으로 합칠 수 있다(coarsening). escape analysis 결과 해당 객체가 현재 스레드에서만 쓰인다면 다른 스레드와 경쟁할 일이 없으므로 lock 자체를 제거(elimination)할 수도 있다.
58+
59+
`LockNode` 하나로 남아 있어야 "이것이 lock이다"라는 의미가 보존되어 이런 판단이 가능하다. 이미 CAS + 조건 분기 + slow path 등으로 확장된 뒤라면 원래 lock이었다는 정보가 사라져서 최적화할 수 없다.
60+
61+
#### Boxing call — scalar replacement로 객체 생성 제거
62+
63+
Java에서 `Integer x = 42`처럼 원시 타입을 래퍼 객체로 변환하는 것을 autoboxing이라 하며, 컴파일러는 이를 `Integer.valueOf(42)` 호출로 변환한다. 이 boxing call이 매크로 노드로 남아 있으면, escape analysis가 결과 객체의 탈출 여부를 분석할 수 있다.
64+
65+
탈출하지 않는다고 판단되면 `Integer` 객체를 굳이 힙에 만들지 않고, 안에 든 `int` 값만 꺼내서 레지스터에 유지하는 것으로 대체(scalar replacement)할 수 있다.
66+
67+
### \_macro_nodes 리스트와 Flag_is_macro의 관계
68+
69+
- `Compile::_macro_nodes` -- 확장 대상 노드 리스트. 매크로 확장 단계에서 이 리스트를 순회하며 각 노드를 확장한다.
70+
- `Flag_is_macro` -- 노드 자체에 달린 플래그. `is_macro()` 같은 빠른 체크에 사용된다.
71+
72+
이번 변경의 핵심은 이 둘을 동기화하는 것이다.
73+
74+
## 매크로 노드의 종류
75+
76+
이 글을 쓰는 시점에서 코드베이스에 존재하는 매크로 노드 타입은 아래와 같다. [이전 글](/2026/05/07/openjdk-c2-노드-클래스-이해하기)에서 소개한 클래스 계층과의 관계를 함께 표시했다.
77+
78+
```
79+
Node
80+
├── CallNode
81+
│ ├── CallJavaNode
82+
│ │ └── CallStaticJavaNode ← boxing method일 때 매크로
83+
│ ├── CallLeafNode
84+
│ │ └── CallLeafPureNode
85+
│ │ ├── ModFloatingNode ← ModD, ModF 매크로
86+
│ │ └── PowDNode ← 매크로
87+
│ ├── AllocateNode ← 매크로
88+
│ ├── LockNode ← 매크로
89+
│ └── UnlockNode ← 매크로
90+
├── CmpNode
91+
│ └── SubTypeCheckNode ← 매크로
92+
├── AddNode (MinMaxNode 계열)
93+
│ ├── MaxLNode ← 매크로
94+
│ └── MinLNode ← 매크로
95+
├── Opaque1Node ← 매크로
96+
├── OpaqueConstantBoolNode ← 매크로
97+
├── OpaqueInitializedAssertionPredicateNode ← 매크로
98+
├── LoopNode
99+
│ └── OuterStripMinedLoopNode ← 매크로
100+
├── LoopLimitNode ← 매크로
101+
├── ArrayCopyNode ← 매크로
102+
└── VectorNode 계열
103+
├── VectorBoxNode ← 매크로
104+
├── VectorBoxAllocateNode ← 매크로
105+
└── VectorUnboxNode ← 매크로
106+
```
107+
108+
매크로 노드는 별도의 클래스 계층이 아니라, 다양한 클래스 계층에 걸쳐 분포하며 "나중에 확장이 필요하다"는 공통 속성을 `Flag_is_macro`로 표현한다.
109+
110+
### 매크로 노드가 JVM에서 처리되는 흐름
111+
112+
매크로 노드는 `compile.cpp``Compile::Optimize()` 안에서 3단계로 처리된다.
113+
114+
```
115+
[파싱] → [IGVN 최적화] → [Escape Analysis] →
116+
117+
① eliminate_macro_nodes
118+
- Escape Analysis 결과를 바탕으로 제거 가능한 노드 제거
119+
- Allocate → scalar replacement로 힙 할당 제거
120+
- Lock/Unlock → lock elimination으로 불필요한 동기화 제거
121+
- CallStaticJava (boxing) → eliminate_boxing_node로 제거
122+
123+
→ [루프 최적화] →
124+
125+
② eliminate_opaque_looplimit_macro_nodes
126+
- Opaque1Node → 감싸고 있던 값을 노출
127+
- OpaqueConstantBoolNode → 상수(true/false)로 치환
128+
- LoopLimitNode → IGVN worklist로 이동
129+
- MaxLNode/MinLNode → CMoveL 노드로 치환
130+
131+
③ expand_macro_nodes
132+
- 제거되지 못하고 남은 노드들을 저수준 노드들로 확장
133+
- 처리 순서: ArrayCopy → Lock/Unlock → SubTypeCheck
134+
→ ModD/ModF/PowD → Allocate (마지막)
135+
136+
→ [Barrier Expansion] → [매칭] → [레지스터 할당] → [코드 생성]
137+
```
138+
139+
## 변경 전 상태
140+
141+
`add_macro_node()`은 리스트에 추가만 하고, 플래그를 설정하지 않는다. 반대로 `remove_macro_node()`도 플래그를 해제하지 않는다.
142+
143+
```cpp
144+
// compile.hpp — 변경 전
145+
void add_macro_node(Node * n) {
146+
assert(!_macro_nodes.contains(n), "duplicate entry in expand list");
147+
_macro_nodes.append(n);
148+
}
149+
150+
void remove_macro_node(Node* n) {
151+
_macro_nodes.remove_if_existing(n);
152+
// Flag_is_macro는 해제하지 않음!
153+
...
154+
}
155+
```
156+
157+
플래그 설정은 각 노드 생성자에서 개별적으로 수행한다.
158+
159+
```cpp
160+
// 예: callnode.hpp - LockNode
161+
LockNode(Compile* C, const TypeFunc *tf) : AbstractLockNode( tf ) {
162+
init_class_id(Class_Lock);
163+
init_flags(Flag_is_macro); // 각자 개별 설정
164+
C->add_macro_node(this);
165+
}
166+
```
167+
168+
이런 패턴이 코드베이스 전체에 17곳 존재한다. 새로운 매크로 노드를 추가할 때 `init_flags(Flag_is_macro)` 호출을 빠뜨리기 쉬운 구조다.
169+
170+
## 변경 내용
171+
172+
### 1. add_macro_node / remove_macro_node에서 플래그 동기화
173+
174+
헤더에는 선언만 두고, 구현을 `compile.cpp`로 이동했다.
175+
176+
```cpp
177+
// compile.hpp
178+
void add_macro_node(Node* n);
179+
void remove_macro_node(Node* n);
180+
181+
// compile.cpp
182+
void Compile::add_macro_node(Node* n) {
183+
assert(!_macro_nodes.contains(n), "duplicate entry in expand list");
184+
n->add_flag(Node::Flag_is_macro);
185+
_macro_nodes.append(n);
186+
}
187+
188+
void Compile::remove_macro_node(Node* n) {
189+
_macro_nodes.remove_if_existing(n);
190+
n->remove_flag(Node::Flag_is_macro);
191+
if (coarsened_count() > 0) {
192+
remove_coarsened_lock(n);
193+
}
194+
}
195+
```
196+
197+
구현을 `.cpp`로 이동한 이유는 `compile.hpp` 시점에서 `Node`가 forward declaration만 되어 있어 멤버 접근이 불가능하기 때문이다. `compile.cpp`에서는 `node.hpp`가 이미 include되어 있으므로 `n->add_flag()` 같은 멤버 접근이 가능하다.
198+
199+
### 2. 개별 생성자에서 init_flags(Flag_is_macro) 제거
200+
201+
`add_macro_node()`에서 중앙 처리하므로 기존에 개별적으로 호출되던 코드들을 모두 제거했다. 이제 새로운 매크로 노드를 추가할 때는 `add_macro_node()`만 호출하면 플래그가 자동으로 설정된다.
202+
203+
### 3. is_boxing_method()에서 is_macro() 의존성 제거
204+
205+
기존 `is_boxing_method()`는 `is_macro()`에 의존하고 있었다.
206+
207+
```cpp
208+
// 변경 전
209+
bool is_boxing_method() const {
210+
return is_macro() && (method() != nullptr) && method()->is_boxing_method();
211+
}
212+
```
213+
214+
문제는 `remove_macro_node()`에서 플래그를 해제하면, 인라인 직후에도 `is_boxing_method()``false`를 반환하여 여러 곳에서 오작동이 생긴다는 것이다.
215+
216+
별도 멤버 `_is_boxing_method`를 도입하여 해결했다.
217+
218+
```cpp
219+
class CallStaticJavaNode : public CallJavaNode {
220+
bool _is_boxing_method;
221+
...
222+
CallStaticJavaNode(Compile* C, ..., ciMethod* method)
223+
: ..., _is_boxing_method(
224+
C->eliminate_boxing() && method && method->is_boxing_method()) {
225+
if (_is_boxing_method) {
226+
C->add_macro_node(this);
227+
}
228+
}
229+
bool is_boxing_method() const { return _is_boxing_method; }
230+
};
231+
```
232+
233+
### 4. guaranteed_safepoint() 오버라이드 (JDK-8379513)
234+
235+
이 부분이 JDK-8379513의 핵심이다. `CallNode`의 `guaranteed_safepoint()`는 기본적으로 `true`를 반환한다. 하지만 boxing `CallStaticJavaNode`은 매크로 확장 과정에서 제거될 수 있으므로 safepoint가 보장되지 않는다.
236+
237+
```cpp
238+
// CallNode (부모) — 기본값 true
239+
virtual bool guaranteed_safepoint() { return true; }
240+
241+
// CallStaticJavaNode (자식) — boxing 매크로일 때만 false
242+
virtual bool guaranteed_safepoint() {
243+
return !(is_boxing_method() && is_macro());
244+
}
245+
```
246+
247+
- 매크로 상태 (인라인 전): `false` → 루프 최적화에서 safepoint로 취급하지 않음
248+
- 매크로 확장 후: `is_macro() = false``true` → PcDesc 정상 생성
249+
250+
### 5. loopnode.cpp에서 boxing 예외 처리 제거
251+
252+
`guaranteed_safepoint()` 자체가 boxing을 처리하므로 별도 예외 처리가 불필요해졌다.
253+
254+
```cpp
255+
// 변경 전 (두 곳)
256+
if (n->is_Call() && n->as_Call()->guaranteed_safepoint()
257+
&& !(n->is_CallStaticJava()
258+
&& n->as_CallStaticJava()->is_boxing_method())) {
259+
260+
// 변경 후
261+
if (n->is_Call() && n->as_Call()->guaranteed_safepoint()) {
262+
```
263+
264+
## 마무리
265+
266+
코드 변경의 방향 자체는 단순하다. 플래그 설정을 17곳에서 개별적으로 하던 것을 한 곳으로 모은 것이다. 이번 작업을 통해 매크로 노드가 왜 사용되며, 어떤 의미를 가지는지 이해할 수 있었다.

0 commit comments

Comments
 (0)