|
| 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