박종훈 기술블로그
close menu

JDK-8379512 / JDK-8379513 기여기: C2 매크로 노드 플래그 동기화와 guaranteed_safepoint 개선

이번 글에서는 서로 연결된 두 이슈 JDK-8379512와 JDK-8379513에 기여한 과정을 정리한다.

  • JDK-8379512: 매크로 노드 표시(Flag_is_macro)를 각 노드 생성자가 따로 붙이던 것을, 매크로 목록에 넣고 뺄 때 함께 붙이고 떼도록 한 곳으로 모았다.
  • JDK-8379513: 그 결과 “이 노드가 아직 매크로 상태인가”를 믿을 수 있게 되었고 이를 이용해 boxing/unboxing 호출의 guaranteed_safepoint()를 바로잡아 JDK-8378005의 임시 workaround를 걷어냈다.

첫 번째 작업이 두 번째 작업의 전제가 된다.

이슈 내용

JDK-8379512: 매크로 노드 플래그 동기화

C2는 매크로 노드를 두 가지 방법으로 관리한다. 하나는 Compile이 들고 있는 _macro_nodes 리스트이고, 다른 하나는 노드 자체에 붙는 Flag_is_macro 플래그다. 둘은 항상 같은 내용을 가리켜야 하지만 관리하는 곳이 달랐다.

  • 리스트는 add_macro_node() / remove_macro_node()가 관리한다.
  • 플래그는 각 노드 생성자가 init_flags(Flag_is_macro)로 직접 붙이고, 떼는 곳은 없다.

그래서 노드가 리스트에서 빠져도 플래그는 그대로 남는다. 이 이슈에서는 플래그도 add_macro_node() / remove_macro_node()에서 함께 처리하도록 바꾼다.

JDK-8379513: guaranteed_safepoint()의 boxing method 처리

guaranteed_safepoint()는 “이 호출 지점에 safepoint가 반드시 있는가”를 답하는 함수다. boxing/unboxing CallStaticJavaNode도 여기에 true를 반환하는데 실제로는 이 호출이 boxing elimination이나 late inlining으로 사라질 수 있어서 safepoint가 보장되지 않는다.

이 문제는 JDK-8378005에서 발견되었고 당시에는 loopnode.cpp에서 boxing 호출만 따로 걸러내는 임시 workaround로 막아 두었다. JDK-8379512로 플래그 관리가 정리된 뒤, 이 이슈에서는 이 workaround를 guaranteed_safepoint() 자체를 고치는 방식으로 대체한다.

배경 지식

매크로 노드란

C2 컴파일러는 대략 다음 순서로 동작한다.

파싱 → 최적화 (GVN, 루프 등) → 매크로 확장 → 매칭 → 코드 생성

매크로 노드(macro node)는 최적화 단계 동안 고수준 형태를 유지하다가 매크로 확장 단계에서 저수준 노드들로 분해되는 노드다. Flag_is_macro는 “이 노드는 아직 최종 형태가 아니다”라는 표시다.

왜 처음부터 저수준 노드로 만들지 않고 이렇게 미뤄 둘까. 객체 할당이나 lock 같은 연산은 결국 수십 개의 저수준 노드(메모리 연산, 비교, 분기, 런타임 호출 등)로 바뀌어야 한다. 매칭 단계는 기계어로 바로 옮길 수 있는 노드만 다루기 때문이다. 그런데 이렇게 풀어 놓으면 “이 노드들이 모여서 하나의 할당이었다”는 사실이 그래프에서 보이지 않는다. 할당을 통째로 없애거나 lock을 합치는 최적화는 바로 그 사실을 알아야 가능하다.

그래서 C2는 이런 연산을 노드 하나로 묶어 둔 채 고수준 최적화를 먼저 돌리고, 그 최적화들이 끝난 뒤에야 저수준 노드로 펼친다. 최적화 덕분에 아예 사라진 노드는 펼칠 필요도 없다. 대표적인 예를 보자.

AllocateNode — escape analysis로 할당 제거

escape analysis는 객체가 메서드 밖으로 빠져나가는지(escape) 분석한다. new Point(x, y)로 만든 객체가 메서드 안에서만 쓰인다면, 힙에 할당하지 않고 필드값만 레지스터나 스택에 두는 것(scalar replacement)으로 대체할 수 있다. 하지만 AllocateNode가 이미 TLAB 할당, GC 배리어 등 수십 개 노드로 확장된 뒤라면 이것이 하나의 할당이었다는 사실을 알 수 없어서 이 분석이 불가능하다.

LockNode — lock coarsening/elimination

LockNode는 Java의 synchronized 블록을 C2 IR로 표현한 것이다. 같은 객체에 대한 synchronized 블록이 연달아 나오면 lock/unlock 두 번을 한 번으로 합칠 수 있다(coarsening).

// 변경 전: lock/unlock을 두 번 한다
synchronized (obj) { a++; }
synchronized (obj) { b++; }

// coarsening 후: 한 번으로 합친다
synchronized (obj) { a++; b++; }

escape analysis 결과 객체가 현재 스레드에서만 쓰인다면 lock 자체를 없앨 수도 있다(elimination). 다른 스레드가 그 객체를 볼 수 없으니 경쟁할 일도 없기 때문이다.

String build() {
    StringBuffer sb = new StringBuffer(); // 메서드 밖으로 나가지 않는 객체
    sb.append("a");                       // append()는 synchronized 메서드
    sb.append("b");
    return sb.toString();
}
// append()가 인라인되면 sb에 대한 lock/unlock이 보이는데,
// sb는 이 스레드에서만 쓰이므로 lock을 통째로 지울 수 있다.

이미 CAS, 조건 분기, slow path 등으로 확장된 뒤라면 원래 lock이었다는 사실을 알 수 없어 이런 판단을 하지 못한다.

Boxing call — 래퍼 객체 생성 제거

Integer x = 42 같은 autoboxing은 Integer.valueOf(42) 호출로 바뀐다. 이 호출이 매크로 노드로 남아 있으면 escape analysis가 결과 객체의 탈출 여부를 분석할 수 있고, 탈출하지 않는다면 Integer 객체를 만들지 않고 안의 int 값만 레지스터에 두는 것으로 대체할 수 있다.

매크로 노드의 종류

이 글을 쓰는 시점에 코드베이스에 있는 매크로 노드 타입은 아래와 같다. 이전 글에서 소개한 클래스 계층 위에 표시했다.

Node
├── CallNode
│   ├── CallJavaNode
│   │   └── CallStaticJavaNode    ← boxing/unboxing method일 때 매크로
│   ├── CallLeafNode
│   │   └── CallLeafPureNode
│   │       ├── ModFloatingNode   ← ModD, ModF 매크로
│   │       └── PowDNode          ← 매크로
│   ├── AllocateNode              ← 매크로
│   ├── LockNode                  ← 매크로
│   └── UnlockNode                ← 매크로
├── CmpNode
│   └── SubTypeCheckNode          ← 매크로
├── AddNode (MinMaxNode 계열)
│   ├── MaxLNode                  ← 매크로
│   └── MinLNode                  ← 매크로
├── Opaque1Node                   ← 매크로
├── OpaqueConstantBoolNode        ← 매크로
├── OpaqueInitializedAssertionPredicateNode ← 매크로
├── LoopNode
│   └── OuterStripMinedLoopNode   ← 매크로
├── LoopLimitNode                 ← 매크로
├── ArrayCopyNode                 ← 매크로
└── VectorNode 계열
    ├── VectorBoxNode             ← 매크로
    ├── VectorBoxAllocateNode     ← 매크로
    └── VectorUnboxNode           ← 매크로

보다시피 매크로 노드는 별도의 클래스 계층이 아니다. 여러 계층에 흩어진 노드들이 나중에 확장이 필요하다는 공통 속성을 Flag_is_macro로 표시할 뿐이다.

매크로 노드가 처리되는 흐름

앞의 흐름도를 매크로 노드 기준으로 자세히 펼쳐 보자. 모두 compile.cpp의 Compile::Optimize() 안에서 일어난다.

[파싱] → [IGVN 최적화] → [Escape Analysis] →

  ① eliminate_macro_nodes
     - Escape Analysis 결과를 바탕으로 제거 가능한 노드 제거
     - Allocate → scalar replacement로 힙 할당 제거
     - Lock/Unlock → lock elimination으로 불필요한 동기화 제거
     - CallStaticJava (boxing/unboxing) → eliminate_boxing_node로 제거

→ [루프 최적화] →

  ② eliminate_opaque_looplimit_macro_nodes
     - Opaque1Node → 감싸고 있던 값을 노출
     - OpaqueConstantBoolNode → 상수(true/false)로 치환
     - LoopLimitNode → IGVN worklist로 이동
     - CallStaticJava (살아남은 boxing/unboxing) → 매크로 목록에서 빼고 IGVN worklist로 이동
     - MaxLNode/MinLNode → CMoveL 노드로 치환

  ③ expand_macro_nodes
     - 제거되지 못하고 남은 노드들을 저수준 노드들로 확장
     - 처리 순서: ArrayCopy → Lock/Unlock → SubTypeCheck
       → ModD/ModF/PowD → Allocate (마지막)

→ [Barrier Expansion] → [매칭] → [레지스터 할당] → [코드 생성]

뒤에서 중요하게 쓰이는 사실이 하나 있다. 루프 최적화는 ②보다 먼저 돌고, 살아남은 boxing/unboxing 호출은 ②에서 매크로 목록에서 빠진다. 그러니 루프 최적화 동안 boxing 호출은 항상 매크로 상태이고, 매칭 시점에는 더 이상 매크로가 아니다.

_macro_nodes 리스트와 Flag_is_macro

  • Compile::_macro_nodes: 확장 대상 노드 리스트. ①~③ 단계에서 이 리스트를 순회한다.
  • Flag_is_macro: 노드 자체에 달린 플래그. is_macro()로 빠르게 확인할 때 쓴다.

같은 사실을 두 군데에 기록하고 있으므로 둘은 항상 일치해야 한다. JDK-8379512에서 이 둘을 동기화한다.

Safepoint란

safepoint는 JVM이 실행 중인 Java 스레드를 안전하게 멈출 수 있는 지점이다.

GC 같은 작업은 Java 스레드를 모두 멈춘 상태(stop-the-world)에서 진행된다. 그런데 스레드를 아무 기계어 위치에서나 멈추면 그 순간 레지스터와 스택에 있는 값 중 무엇이 객체 참조(oop)인지 알 수 없다. 그래서 JVM은 이 정보(OopMap)와 디버그 정보를 미리 기록해 둔 지점에서만 스레드를 멈춘다. 이 지점이 safepoint다.

스레드는 safepoint에 도달할 때마다 “지금 멈춰야 하는가”를 확인(poll)하고 요청이 있으면 그 자리에서 멈춘다. 반대로 말하면 safepoint에 도달하지 않는 스레드는 멈출 수 없고 safepoint를 요청한 쪽은 그 스레드를 계속 기다리게 된다.

C2에서는 safepoint가 IR 노드로 표현된다.

  • SafePointNode: safepoint 자체를 나타내는 노드다. 파서는 바이트코드의 뒤로 가는 분기(루프 back edge)를 만나면 SafePointNode를 넣는다(parse2.cpp의 “If this is a backwards branch in the bytecodes, add Safepoint”). 루프가 아무리 오래 돌아도 한 바퀴마다 poll할 기회를 주기 위해서다.
  • CallNode: CallNode는 SafePointNode를 상속한다(class CallNode : public SafePointNode). 호출된 메서드 안에서 스레드가 멈출 수 있으므로 호출 지점도 safepoint로 다루고 호출의 복귀 주소에 OopMap과 디버그 정보를 남긴다.

루프 안의 SafePointNode는 poll 비용이 들기 때문에 루프 최적화는 필요 없다고 판단되는 SafePoint를 지운다(IdealLoopTree::remove_safepoints()). 이때 판단 근거 중 하나가 “루프 안에 safepoint를 보장하는 호출이 반드시 실행되는가”이고 그 답을 주는 함수가 뒤에서 다룰 guaranteed_safepoint()다. 호출이 safepoint를 보장한다고 답했는데 실제로는 그 호출이 사라지면 poll할 곳이 없는 루프가 남을 수 있다.

Boxing과 Unboxing

Java에는 int 같은 원시 타입과 이를 객체로 감싼 래퍼 타입(Integer 등)이 따로 있다. 원시 값을 래퍼 객체로 바꾸는 것을 boxing이라 하고 반대로 래퍼 객체에서 원시 값을 꺼내는 것을 unboxing이라 한다. 컴파일러(javac)는 이 변환을 메서드 호출로 바꿔 넣는다.

List<Integer> list = ...;
list.add(42);        // boxing:   list.add(Integer.valueOf(42))
int x = list.get(0); // unboxing: int x = list.get(0).intValue()

C2가 boxing/unboxing 메서드로 취급하는 것은 ciMethod::is_boxing_method()와 is_unboxing_method()에 정해져 있다. 래퍼 클래스의 intrinsic 메서드들이다.

  • boxing: Boolean.valueOf, Byte.valueOf, Character.valueOf, Short.valueOf, Integer.valueOf, Long.valueOf, Float.valueOf, Double.valueOf
  • unboxing: booleanValue, byteValue, charValue, shortValue, intValue, longValue, floatValue, doubleValue

boxing은 객체를 만들 수 있으므로 비용이 든다. 그런데 Integer.valueOf(x).intValue()처럼 만들자마자 다시 꺼내 쓰는 경우가 흔하고 이때 객체는 사실 필요 없다. 그래서 C2는 boxing elimination(EliminateAutoBox 옵션, 기본값 켜짐)으로 이런 객체 생성을 없애려 한다.

이를 위해 C2는 boxing/unboxing 호출을 일반 호출과 다르게 다룬다.

  • 매크로 노드로 등록한다: CallStaticJavaNode 생성자는 eliminate_boxing()이 켜져 있고 메서드가 boxing/unboxing이면 add_macro_node()를 호출한다. 호출을 아직 최종 형태가 아닌 노드로 남겨 둔다.
  • 인라인을 늦춘다: boxing 메서드는 바로 인라인하지 않고 late inline 대상으로 미룬다(Compile::should_delay_boxing_inlining()). 먼저 escape analysis 같은 고수준 최적화가 호출을 통째로 없앨 기회를 주기 위해서다.

그래서 boxing/unboxing 호출의 운명은 컴파일 도중에 정해진다. 호출 자체가 지워질 수도 있고 인라인되어 호출이 아닌 코드로 바뀔 수도 있고 끝까지 실제 호출로 남을 수도 있다.

PcDesc란

PcDesc는 컴파일된 코드(nmethod)의 기계어 주소 하나를 Java 소스 위치와 연결하는 기록이다. pcDesc.hpp에는 이렇게 설명되어 있다.

// PcDescs map a physical PC (given as offset from start of nmethod) to
// the corresponding source scope and byte code index.
class PcDesc {
  int _pc_offset;           // offset from start of nmethod
  int _scope_decode_offset; // offset for scope in nmethod
  int _obj_decode_offset;
  int _flags;
  ...
};
  • _pc_offset: nmethod 시작점에서 얼마나 떨어진 기계어 위치인지
  • _scope_decode_offset: 그 위치에 해당하는 scope 정보(어느 메서드의 어느 bytecode index인지, 인라인된 경우 호출자 체인까지)를 nmethod 안에서 찾을 수 있는 위치
  • _obj_decode_offset: scalar replacement로 없어진 객체를 다시 만들 때 필요한 정보의 위치
  • _flags: 호출이 oop을 반환하는지(PCDESC_return_oop), 재실행이 필요한지(PCDESC_reexecute) 같은 부가 정보

코드 생성 단계(output.cpp)는 기계 노드를 하나씩 내보내다가 observe_safepoint가 true인 노드를 만나면 Process_OopMap_Node()를 호출한다.

// output.cpp
if (observe_safepoint) {
  ...
  non_safepoints.observe_safepoint(mach->as_MachSafePoint()->jvms(), current_offset);
  Process_OopMap_Node(mach, current_offset);
}

Process_OopMap_Node()는 호출 노드라면 복귀 주소(current_offset + ret_addr_offset())를 기준으로 그 지점의 OopMap과 JVMState(scope 정보)를 DebugInfoRecorder에 기록한다. 이 기록이 최종적으로 nmethod의 PcDesc와 scope 데이터가 된다.

// output.cpp — Process_OopMap_Node()
} else if (mach->is_MachCall()) {
  ...
  safepoint_pc_offset += mcall->ret_addr_offset();
}
C->debug_info()->add_safepoint(safepoint_pc_offset, sfn->_oop_map);

코드 생성 단계에서 guaranteed_safepoint()는 이 호출의 복귀 주소에 safepoint용 디버그 정보(PcDesc, OopMap, scope)를 남길지 정하는 스위치다.

JDK-8379512: 플래그 관리를 한 곳으로

변경 전

add_macro_node()는 리스트에 추가만 하고 플래그를 붙이지 않는다. remove_macro_node()도 리스트에서 빼기만 하고 플래그를 떼지 않는다.

// compile.hpp — 변경 전
void add_macro_node(Node * n) {
    assert(!_macro_nodes.contains(n), "duplicate entry in expand list");
    _macro_nodes.append(n);
}

void remove_macro_node(Node* n) {
    _macro_nodes.remove_if_existing(n);
    // Flag_is_macro는 해제하지 않음!
    ...
}

플래그는 각 노드 생성자에서 따로 붙인다.

// 예: callnode.hpp - LockNode
LockNode(Compile* C, const TypeFunc *tf) : AbstractLockNode( tf ) {
    init_class_id(Class_Lock);
    init_flags(Flag_is_macro);   // 각자 개별 설정
    C->add_macro_node(this);
}

이런 패턴이 코드베이스 전체에 17곳 있다. 새 매크로 노드를 추가할 때 init_flags(Flag_is_macro)를 빠뜨리기 쉽고 한 번 붙은 플래그는 노드가 목록에서 빠진 뒤에도 남는다.

add_macro_node / remove_macro_node에서 플래그 동기화

두 함수가 플래그도 함께 붙이고 떼도록 바꾸고 구현은 compile.cpp로 옮겼다.

// compile.hpp
void add_macro_node(Node* n);
void remove_macro_node(Node* n);

// compile.cpp
void Compile::add_macro_node(Node* n) {
    assert(!_macro_nodes.contains(n), "duplicate entry in expand list");
    n->add_flag(Node::Flag_is_macro);
    _macro_nodes.append(n);
}

void Compile::remove_macro_node(Node* n) {
    _macro_nodes.remove_if_existing(n);
    n->remove_flag(Node::Flag_is_macro);
    if (coarsened_count() > 0) {
        remove_coarsened_lock(n);
    }
}

구현을 옮긴 이유는 compile.hpp에서는 Node가 forward declaration만 되어 있어 n->add_flag() 같은 멤버에 접근하지 못하기 때문이다. compile.cpp에는 node.hpp가 이미 include되어 있다.

이제 생성자 17곳의 init_flags(Flag_is_macro)는 필요 없으므로 모두 지웠다. 새 매크로 노드를 추가할 때는 add_macro_node()만 호출하면 된다.

is_boxing_method()에서 is_macro() 의존성 제거

플래그가 떨어질 수 있게 되자 영향을 받는 곳이 생겼다. 기존 is_boxing_method()와 is_unboxing_method()는 is_macro()를 조건에 포함하고 있었다.

// 변경 전
bool is_boxing_method() const {
    return is_macro() && (method() != nullptr) && method()->is_boxing_method();
}

플래그가 한 번 붙으면 떨어지지 않던 시절에는 문제없는 코드였다. 하지만 이제는 매크로 목록에서 빠진 boxing 호출에 대해 is_boxing_method()가 false를 반환한다. MemNode::can_see_stored_value()나 ProjNode::Identity() 같은 곳은 목록에서 빠진 뒤에도 boxing 호출인지를 묻는다. 그대로 두면 이런 곳의 동작이 바뀐다.

그래서 is_macro() 대신, 생성자가 매크로로 등록할 때 쓰는 조건을 그대로 쓰도록 바꿨다.

// 생성자: 이 조건이면 매크로로 등록
if (C->eliminate_boxing() && (method != nullptr) && (method->is_boxing_method() || method->is_unboxing_method())) {
  C->add_macro_node(this);
}

// 변경 후
bool is_boxing_method() const {
  return Compile::current()->eliminate_boxing() && (method() != nullptr) && method()->is_boxing_method();
}

bool is_unboxing_method() const {
  return Compile::current()->eliminate_boxing() && (method() != nullptr) && method()->is_unboxing_method();
}

이제 “boxing 호출인가”(is_boxing_method())와 “아직 매크로 상태인가”(is_macro())는 서로 다른 질문이다. JDK-8379513은 바로 이 구분을 이용한다.

JDK-8379513: guaranteed_safepoint() 바로잡기

guaranteed_safepoint()가 쓰이는 두 곳

CallNode::guaranteed_safepoint()는 “이 호출 지점에 safepoint가 반드시 있는가”를 답하며 기본값은 true다. 일반적인 Java 호출이라면 맞는 말이다.

이 값은 성격이 다른 두 곳에서 쓰인다.

  1. 루프 최적화: IdealLoopTree::check_safepts() 등에서 루프 안에 safepoint가 보장되는지 판단할 때 쓴다. 어떤 호출이 루프 머리에서 꼬리까지 반드시 실행된다면 그 루프는 별도의 SafePoint 노드가 없어도 된다고 판단한다. 이때는 안쪽 루프가 SafePoint를 지우지 못하게 보호하지 않는다.
  2. 코드 생성: 매칭 단계에서 이 값이 기계 노드로 복사되고 코드 생성 단계에서 그 호출의 복귀 주소에 디버그 정보(PcDesc)를 남길지 결정한다.
// matcher.cpp
mcall->set_guaranteed_safepoint(call->guaranteed_safepoint());

// output.cpp
observe_safepoint = mcall->guaranteed_safepoint();

boxing 호출 문제는 1번에서 생기는데 반환값을 바꾸면 2번도 함께 바뀐다. 이 점이 JDK-8378005 때 guaranteed_safepoint()를 직접 고치지 못한 이유다.

문제: boxing 호출은 사라질 수 있다

Integer.valueOf(i)를 예로 들면, 이 호출이 매크로 노드로 남아 있는 동안 C2는 아직 이 호출의 운명을 정하지 않았다. 이후 다음 중 하나가 일어날 수 있다.

  • boxing elimination: 결과 객체가 쓰이지 않거나 탈출하지 않으면 eliminate_boxing_node()가 호출 자체를 지운다.
  • late inlining: Integer.valueOf() 본문이 인라인되면 IntegerCache 배열 조회나 TLAB 할당 같은 fast path만 남는다. 여기에는 런타임 호출이 없으므로 safepoint도 없다.
  • IGVN 단순화: ProjNode::Identity() 등이 boxing/unboxing 호출의 결과 대신 입력값을 직접 쓰도록 바꿀 수 있다(예: Valhalla의 value class). 결과를 쓰는 곳이 없어지면 호출도 함께 제거된다.

어느 경우든 호출이 있으니 그 지점에 safepoint가 있다는 가정이 깨진다. 루프 최적화가 이 호출을 믿고 SafePoint를 보호하지 않았는데 호출이 나중에 사라지면 safepoint가 없는 루프가 남을 수 있다. JDK-8378005가 바로 이 문제였다. 그러니 루프 최적화 시점에는 false를 반환해야 한다.

그런데 PcDesc는 바뀌면 안 된다

반대로 boxing 호출이 끝까지 살아남아 실제 호출로 나간다면 코드 생성 시점에 예전과 똑같이 PcDesc가 기록되어야 한다. 두 요구를 합치면 guaranteed_safepoint()는

  • 루프 최적화 시점에는 false
  • 매칭 시점에는 true

여야 한다.

JDK-8378005 당시에는 Flag_is_macro가 한 번 붙으면 떨어지지 않았으므로 is_macro()로 두 시점을 구분할 수 없었다. 그래서 guaranteed_safepoint()는 그대로 두고 loopnode.cpp에서만 boxing 호출을 따로 걸러내는 임시방편을 택했다.

JDK-8379512 이후에는 구분이 가능하다. 앞에서 본 것처럼 살아남은 boxing/unboxing 호출은 루프 최적화가 끝난 뒤 ②에서 remove_macro_node()로 목록에서 빠지고 이제는 이때 플래그도 함께 떨어진다. 루프 최적화 동안은 is_macro() == true, 매칭 시점에는 is_macro() == false다.

CallStaticJavaNode에 오버라이드 추가

이제 is_macro()로 두 시점을 구분할 수 있으므로, CallStaticJavaNode에 다음 오버라이드를 추가했다.

// CallNode (부모) — 기본값 true
virtual bool guaranteed_safepoint() { return true; }
// CallStaticJavaNode (자식)
// Boxing and unboxing calls may not have a safepoint on the fast path.
// Since the macro flag is cleared before matching, it has no effect on PcDesc emission.
virtual bool guaranteed_safepoint() {
  return !(is_boxing_or_unboxing_call() && is_macro());
}

is_boxing_or_unboxing_call()은 Node에 이미 있던 헬퍼로, is_CallStaticJava()이면서 is_boxing_method() || is_unboxing_method()인지 확인한다.

왜 boxing/unboxing 호출만 따로 확인하는가

CallStaticJavaNode 중에서 boxing/unboxing 호출은 생성될 때부터 매크로 노드로 등록된다. 일부러 남겨 두었다가 나중에 지우거나 인라인하려는 호출이기 때문이다. 루프 최적화가 도는 동안에는 이 호출이 끝까지 남을지 아직 알 수 없다. 이런 호출에 safepoint가 보장된다고 믿으면 안 되기 때문에 guaranteed_safepoint()는 boxing/unboxing 호출이면서 아직 매크로 상태인지(is_boxing_or_unboxing_call() && is_macro())를 따로 확인한다.

주석 두 줄이 지금까지의 내용을 요약한다. 첫 줄은 “boxing 호출은 사라질 수 있으니 safepoint를 보장하지 않는다”는 문제를, 둘째 줄은 “매칭 전에 매크로 플래그가 떨어지므로 PcDesc 방출에는 영향이 없다”는 보증을 말한다.

시점별 값을 표로 보면 이렇다.

시점is_macro()guaranteed_safepoint()의미
파싱 직후 ~ 루프 최적화truefalse사라질 수 있으므로 safepoint로 믿지 않음
late inline 성공(노드 제거)-호출 자체가 없어짐
② 이후 살아남은 호출falsetrue실제 호출이 되므로 safepoint로 취급
매칭 / 코드 생성falsetruePcDesc 정상 기록

loopnode.cpp의 workaround 제거

guaranteed_safepoint()가 boxing/unboxing을 직접 처리하므로, IdealLoopTree::check_safepts()와 allpaths_check_safepts() 두 곳에 있던 예외 처리는 필요 없어졌다.

// 변경 전 (두 곳)
// Boxing and unboxing calls are excluded as they may lack a safepoint on the fast path. This is
// not done via CallStaticJavaNode::guaranteed_safepoint() as that also controls PcDesc emission.
// In the future, guaranteed_safepoint() should be reworked to correctly handle boxing methods
// to avoid this additional check.
if (n->is_Call() && n->as_Call()->guaranteed_safepoint() && !n->is_boxing_or_unboxing_call()) {

// 변경 후
if (n->is_Call() && n->as_Call()->guaranteed_safepoint()) {

조건과 함께 기존 주석도 지웠다. “PcDesc 방출 때문에 guaranteed_safepoint()에서 처리하지 않는다, 나중에 rework하라”는 내용이었는데 이번 변경이 바로 그 rework이므로 남겨 두면 사실과 다른 설명이 된다. 그 근거는 guaranteed_safepoint() 오버라이드 위 주석으로 옮겼다.

루프 최적화 동안 boxing 호출은 항상 is_macro() == true이므로, 이 두 곳의 동작은 변경 전과 완전히 같다.

마무리

코드 변경의 방향 자체는 단순하다. 17곳에서 따로 하던 플래그 설정을 한 곳으로 모았고, 그 덕분에 is_macro()가 지금 매크로 상태인지를 정확히 말해 주게 되어 JDK-8378005의 workaround를 제대로 된 수정으로 바꿀 수 있었다. 작업하면서 매크로 노드가 왜 쓰이고 어떤 의미인지도 알게 되었다.

categories: 개발

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