JDK-8394577 기여기: Serviceability Agent가 모르는 Vector 위치 타입
예전부터 Vector API 프로젝트에 관심이 많아서 언젠가 기여해 보고 싶었다. 그런데 막상 이슈를 찾아보면 C2 내부 최적화나 플랫폼별 intrinsic처럼 처음 들어가기에는 진입 장벽이 높은 것들이 대부분이라 적당한 이슈를 찾지 못하고 있었다. 그러다 vectorapi 라벨이 붙은 JDK-8394577 을 발견했다. P4 이슈이고 담당자가 없으며, serviceability 팀에서 “우리 팀 우선순위가 아니고 Location과 Vector 쪽 전문성도 부족하다”는 댓글이 달려 있었다. Vector API 쪽 코드를 들여다볼 좋은 계기라고 생각해서 문제 파악을 시작했다.
이슈의 제목은 다음과 같다.
VectorAPI: Servicability agent is missing type support for Location::vector
이번 글에서는 이슈를 이해하는 데 필요한 배경과 원인을 정리하고, 직접 작성한 수정 내용과 그 과정에서 고민한 부분, 리포터의 재현 코드로 수정을 확인한 결과, 그리고 jtreg 회귀 테스트를 만든 과정까지 정리한다.
배경지식 1: Vector API는 무엇을 하려는가
SIMD
요즘 CPU에는 레지스터 하나에 값 여러 개를 담아 한 명령어로 같이 계산하는 SIMD(Single Instruction, Multiple Data) 명령어가 있다. x86의 SSE/AVX/AVX-512, ARM의 NEON/SVE, RISC-V의 RVV 같은 것들이다. 예를 들어 128비트 레지스터에는 int 4개가 들어가므로 덧셈 한 번으로 int 4쌍을 동시에 더할 수 있다.
a: [ 1 | 2 | 3 | 4 ]
b: [ 5 | 6 | 7 | 8 ] + (명령어 1개)
-----------------------
[ 6 | 8 |10 |12 ]
자동 벡터화의 한계
HotSpot의 C2 컴파일러에도 평범한 루프를 SIMD 명령어로 바꿔 주는 자동 벡터화(auto-vectorization, SuperWord)가 있다. 하지만 자동 벡터화는 컴파일러가 “이 루프는 벡터화해도 안전하다”고 증명할 수 있을 때만 동작한다. 루프 모양이 조금만 복잡해지거나 분기가 들어가거나 배열끼리 겹칠 가능성이 있으면 포기해 버린다. 개발자가 결과를 예측하거나 강제할 방법도 없다.
Vector API
Vector API(JEP 338 이후 계속 인큐베이팅 중)는 이 문제를 반대 방향에서 푼다. 컴파일러가 알아서 찾아 주길 기다리지 않고 개발자가 벡터 연산을 Java 코드로 직접 적는다.
static final VectorSpecies<Integer> SPECIES = IntVector.SPECIES_PREFERRED;
void add(int[] a, int[] b, int[] c) {
int i = 0;
for (; i < SPECIES.loopBound(a.length); i += SPECIES.length()) {
var va = IntVector.fromArray(SPECIES, a, i);
var vb = IntVector.fromArray(SPECIES, b, i);
va.add(vb).intoArray(c, i);
}
for (; i < a.length; i++) { // 남은 꼬리 부분
c[i] = a[i] + b[i];
}
}
목표는 크게 두 가지다.
- 플랫폼 독립성: 같은 코드가 x86, AArch64, RISC-V에서 각 하드웨어의 SIMD 명령어로 컴파일된다.
SPECIES_PREFERRED를 쓰면 벡터 길이도 실행하는 CPU에 맞게 정해진다. - 신뢰할 수 있는 성능: C2가 이 코드를 보면
IntVector연산을 SIMD 명령어로 바꾸는 intrinsic을 적용한다. 지원하지 않는 플랫폼에서는 일반 Java 코드로 동작하기 때문에(graceful degradation) 결과는 항상 같다.
모듈은 jdk.incubator.vector다. Valhalla 프로젝트의 value class가 들어오면 벡터를 값 타입으로 바꿀 계획이라 그때까지 인큐베이터에 머물고 있다.
벡터는 객체다, 그런데 레지스터에 있다
이번 이슈와 바로 이어지는 부분이다. Java 코드 입장에서 IntVector는 내부에 int[] 배열(payload)을 가진 평범한 객체다. 그런데 C2는 성능을 위해 이 객체를 실제로 만들지 않고 언박싱해서 SIMD 레지스터에만 둔다. 위 루프에서 va, vb는 힙에 할당되지 않고 XMM/YMM 같은 레지스터에만 존재한다.
문제는 safepoint다. C2 코드가 실행되다가 역최적화(deoptimization)가 일어나면 인터프리터로 돌아가야 한다. 그런데 인터프리터는 레지스터에 흩어진 값이 아니라 진짜 IntVector 객체가 필요하다. 그래서 C2는 safepoint마다 “이 객체는 지금 몇 번 레지스터(혹은 스택 위치)에 이런 모양으로 들어 있다”는 디버그 정보를 남긴다. 실제로 역최적화가 일어날 때에만 이 정보를 보고 객체를 다시 만든다(rematerialize). safepoint마다 객체를 미리 만들어 두면 너무 비싸기 때문이다.
이때 “값이 어디에, 어떤 타입으로 있는가”를 나타내는 것이 HotSpot의 Location이다.
// src/hotspot/share/code/location.hpp
// A Location describes a concrete machine variable location
// (such as integer or floating point register or a stack-held
// variable). Used when generating debug-information for nmethods.
//
// Encoding:
//
// bits (use low bits for best compression):
// Type: [3..0]
// Where: [4]
// Offset: [31..5]
class Location {
...
enum Type {
invalid, // Invalid location
normal, // Ints, floats, double halves
oop, // Oop (please GC me!)
int_in_long, // Integer held in long register
lng, // Long held in one register
float_in_dbl, // Float held in double register
dbl, // Double held in one register
vector, // Vector in one register
addr, // JSR return address
narrowoop // Narrow Oop (please GC me!)
};
Location은 32비트 정수 하나에 타입(하위 4비트), 위치 종류(스택인지 레지스터인지, 1비트), 오프셋(나머지)을 담는다. Vector API가 들어오면서 vector 타입이 추가됐고, 역최적화 시에는 VectorSupport::allocate_vector가 이 타입의 위치를 보고 레지스터 값을 payload 배열로 복사해 객체를 다시 만든다.
// src/hotspot/share/prims/vectorSupport.cpp
Handle VectorSupport::allocate_vector_payload(InstanceKlass* ik, frame* fr, RegisterMap* reg_map, ScopeValue* payload, TRAPS) {
if (payload->is_location()) {
Location location = payload->as_LocationValue()->location();
if (location.type() == Location::vector) {
// Vector value in an aligned adjacent tuple (1, 2, 4, 8, or 16 slots).
return allocate_vector_payload_helper(ik, fr, reg_map, location, THREAD); // safepoint
}
배경지식 2: Serviceability Agent(SA)는 무엇인가
Serviceability Agent는 실행 중인 JVM 프로세스나 코어 덤프를 바깥에서 들여다보는 디버거다. jhsdb 명령어로 사용할 수 있다.
jhsdb clhsdb --pid 1234 # 명령줄 디버거
jhsdb hsdb --pid 1234 # GUI 디버거
jhsdb jstack --mixed --pid 1234 # Java + 네이티브 스택
jhsdb jmap --core ./core.1234 --exe ./java # 코어 덤프에서 힙 정보
일반적인 jstack이나 jcmd는 대상 JVM 안에 붙어서 JVM에게 “정보 좀 줘”라고 요청한다. 그래서 JVM이 멈춰 있거나 이미 죽어서 코어 덤프만 남아 있으면 쓸 수 없다. SA는 방식이 다르다. 대상 프로세스를 ptrace 같은 OS 디버깅 기능으로 멈춰 세우거나 코어 파일을 연 다음, 메모리를 직접 읽어서 HotSpot의 C++ 자료구조를 Java로 해석한다. 대상 JVM은 아무 코드도 실행하지 않는다.
그러려면 SA는 HotSpot 내부 구조를 알고 있어야 한다. 클래스의 필드 오프셋, 타입 크기, enum 상수 값 같은 것들이다. 이 정보는 HotSpot이 vmStructs.cpp에 표로 만들어 바이너리 안에 내보내고 SA는 이 표를 읽어서 쓴다.
// src/hotspot/share/runtime/vmStructs.cpp
declare_constant(Location::OFFSET_MASK) \
...
/* constants from Location::Type enum */ \
declare_constant(Location::normal) \
declare_constant(Location::oop) \
...
// src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/code/Location.java
TYPE_NORMAL = db.lookupIntConstant("Location::normal").intValue();
TYPE_OOP = db.lookupIntConstant("Location::oop").intValue();
SA의 sun.jvm.hotspot.* 패키지에는 HotSpot C++ 클래스와 짝을 이루는 Java 클래스들이 있다. location.hpp에 대응하는 것이 SA의 Location.java다. C++ 쪽에 무언가 추가되면 SA 쪽도 같이 맞춰 줘야 한다. 이번 이슈는 이 짝이 어긋난 경우다.
이슈 내용
리포터는 C2 컴파일된 프레임의 디버그 정보를 SA 내부 API로 읽는 에이전트를 만들어 돌렸다. IntVector128을 쓰는 메서드를 컴파일시키고 safepoint에서 멈춘 상태를 SA로 읽어 보는 재현이다. 스칼라 치환된 IntVector128 객체(scalarObj[55])까지는 잘 나오다가 그 payload 위치를 출력하는 순간 예외가 난다.
## ObjectValue:
scalarObj[55]
## -> klass mirror:
Oop for java/lang/Class @ 0x000000061705d2e8 (object size = 160)
- name: {48} :"jdk.incubator.vector.IntVector128" @ 0x000000061705d4b8
...
# ScopeValue:
Value 7, Exception in thread "main" java.lang.RuntimeException: should not reach here
at jdk.hotspot.agent/sun.jvm.hotspot.code.Location.getType(Location.java:231)
at jdk.hotspot.agent/sun.jvm.hotspot.code.Location.isIllegal(Location.java:276)
at jdk.hotspot.agent/sun.jvm.hotspot.code.Location.printOn(Location.java:299)
at jdk.hotspot.agent/sun.jvm.hotspot.code.LocationValue.printOn(LocationValue.java:63)
at MyAgent.main(MyAgent.java:100)
Value 7은 Location의 원래 정수 값이다. 하위 4비트가 타입이므로 타입 값은 7이고, enum 순서를 세어 보면 invalid(0), normal(1), oop(2), int_in_long(3), lng(4), float_in_dbl(5), dbl(6), vector(7) 이다. SA가 vector 타입을 만나서 죽은 것이다.
리포터는 이 문제가 Vector API가 처음 들어온 JDK 16부터 있었을 것이고, 통합 당시 놓친 부분 같다고 적었다.
원인
코드를 보면 이유가 바로 보인다. 두 군데가 빠져 있다.
1. vmStructs에서 상수를 내보내지 않는다
vmStructs.cpp의 Location::Type 상수 목록에 vector가 없다.
/* constants from Location::Type enum */ \
declare_constant(Location::normal) \
declare_constant(Location::oop) \
declare_constant(Location::narrowoop) \
declare_constant(Location::int_in_long) \
declare_constant(Location::lng) \
declare_constant(Location::float_in_dbl) \
declare_constant(Location::dbl) \
declare_constant(Location::addr) \
declare_constant(Location::invalid) \
내보내지 않으니 SA는 이 값이 무엇인지 알 방법이 없다.
2. SA의 Location.java가 vector 타입을 모른다
Location.java의 Type은 enum이 나오기 전 스타일의 “type safe enum” 클래스다. 여기에도 VECTOR가 없다.
public static class Type {
public static final Type NORMAL = new Type("normal");
public static final Type OOP = new Type("oop");
public static final Type NARROWOOP = new Type("narrowoop");
public static final Type INT_IN_LONG = new Type("int_in_long");
public static final Type LNG = new Type("lng");
public static final Type FLOAT_IN_DBL = new Type("float_in_dbl");
public static final Type DBL = new Type("dbl");
public static final Type ADDR = new Type("addr");
public static final Type INVALID = new Type("invalid");
...
그리고 원시 값을 Type으로 바꾸는 getType()은 아는 값을 하나씩 비교하다가 끝까지 못 찾으면 예외를 던진다. 스택 트레이스의 Location.java:231이 여기다.
public Type getType() {
int type = (value & TYPE_MASK) >> TYPE_SHIFT;
if (type == TYPE_NORMAL) {
return Type.NORMAL;
} else if (type == TYPE_OOP) {
...
} else if (type == TYPE_INVALID) {
return Type.INVALID;
} else {
throw new RuntimeException("should not reach here");
}
}
isIllegal(), holdsOop() 같은 판별 메서드가 전부 getType()을 거친다. 그래서 vector 위치가 하나라도 있으면 그 Location에 대한 거의 모든 동작이 예외로 끝난다.
정리하면 고칠 곳은 다음과 같다.
| 위치 | 빠진 것 |
|---|---|
vmStructs.cpp | declare_constant(Location::vector) |
Location.java initialize() | Location::vector 상수 조회 |
Location.java Type | VECTOR 상수와 getValue() 분기 |
Location.java getType() / printOn() | vector 분기 |
Location.java (다른 타입과 맞추려면) | holdsVector() 같은 판별 메서드 (결국 넣지 않았다) |
HTMLGenerator.java locationAsString() | vector 분기 (나중에 찾았다) |
수정 내용
원인을 정리한 표대로 고쳤다. 먼저 vmStructs.cpp에서 Location::vector를 내보낸다. 위치는 enum 순서대로 dbl 다음에 두었다.
declare_constant(Location::lng) \
declare_constant(Location::float_in_dbl) \
declare_constant(Location::dbl) \
+ declare_constant(Location::vector) \
declare_constant(Location::addr) \
declare_constant(Location::invalid) \
Location.java는 기존 타입이 들어 있는 곳마다 vector를 하나씩 끼워 넣는 식이다. 상수 필드와 initialize()에서의 조회부터 넣었다.
TYPE_DBL = db.lookupIntConstant("Location::dbl").intValue();
+ TYPE_VECTOR = db.lookupIntConstant("Location::vector").intValue();
TYPE_ADDR = db.lookupIntConstant("Location::addr").intValue();
private static int TYPE_DBL;
+ private static int TYPE_VECTOR;
private static int TYPE_ADDR;
Type에는 VECTOR 상수를 추가했다. 주석은 HotSpot 쪽 // Vector in one register를 따르되, SA의 다른 주석이 “Double held in one register”처럼 “X held in …” 형태라서 거기에 맞춰 “Vector held in one register”로 적었다.
/** Double held in one register */
public static final Type DBL = new Type("dbl");
+ /** Vector held in one register */
+ public static final Type VECTOR = new Type("vector");
/** JSR return address */
public static final Type ADDR = new Type("addr");
그 다음은 정수 값과 Type 사이를 오가는 두 메서드다. getType()은 정수 값을 Type으로, Type.getValue()는 Type을 정수 값으로 바꾼다. 서로 역함수 관계라서 한쪽만 고치면 안 된다. getValue()는 Location(Where, Type, int) 생성자가 부르는 setType()에서 쓰인다. 그래서 여기에 분기가 없으면 SA 안에서 vector 타입 Location을 만들려고 할 때 같은 종류의 예외가 다시 난다.
} else if (this == DBL) {
return TYPE_DBL;
+ } else if (this == VECTOR) {
+ return TYPE_VECTOR;
} else if (this == ADDR) {
return TYPE_ADDR;
} else if (type == TYPE_DBL) {
return Type.DBL;
+ } else if (type == TYPE_VECTOR) {
+ return Type.VECTOR;
} else if (type == TYPE_ADDR) {
return Type.ADDR;
마지막으로 리포터의 스택 트레이스에 나왔던 printOn()에도 출력 분기를 넣었다.
} else if (type == Type.DBL) {
tty.print(",double");
+ } else if (type == Type.VECTOR) {
+ tty.print(",vector");
} else if (type == Type.ADDR) {
tty.print(",address");
HTMLGenerator에도 같은 분기가 있다
Location.java만 고치면 끝일 줄 알았는데 SA 안에 Location.Type으로 분기하는 코드가 하나 더 있었다. HTMLGenerator.locationAsString()이다.
HTMLGenerator는 무엇인가
sun.jvm.hotspot.ui.classbrowser.HTMLGenerator는 SA가 메모리에서 읽어 낸 HotSpot 자료구조를 사람이 읽을 수 있는 문서로 바꿔 준다. 클래스(InstanceKlass), 메서드, 상수 풀, JIT 컴파일된 코드(NMethod), 코드 블롭, 인터프리터 코드렛, 스레드의 Java 스택 트레이스까지 대상마다 genHTML(...) 계열 메서드가 하나씩 있다. SA의 “출력 담당”이라고 보면 된다.
이름에는 HTML이 들어가 있지만 HTML만 만드는 것은 아니다. 생성자에 플래그가 있어서 false를 넘기면 태그 없는 일반 텍스트를 만든다. 내부의 Formatter가 이 플래그를 보고 <pre>, <br>, 링크 같은 태그를 붙일지 말지 정한다.
public HTMLGenerator() {
this(true); // HTML 모드
}
public HTMLGenerator(boolean html) {
genHTML = html; // false면 일반 텍스트 모드
...
}
이 클래스를 쓰는 곳은 크게 두 군데다.
- HSDB(GUI):
jhsdb hsdb의 Class Browser, Code Viewer, Java Stack Trace 패널이 HTML 모드로 쓴다. 만든 HTML을 SwingJEditorPane에 띄우고, 사용자가 링크를 누르면genHTMLForHyperlink()가klass=,method=,nmethod=,pc=같은 href를 해석해서 다음 화면을 만든다. 클래스에서 메서드로, 메서드에서 컴파일된 코드로 타고 들어가는 브라우저 화면이 전부 이 클래스에서 나온다. - clhsdb(명령줄):
CommandProcessor가new HTMLGenerator(false)로 텍스트 모드 객체를 만들어 쓴다.dis,disassemble,jdis,print,printall,where,dumpcodecache,livenmethods같은 명령의 출력이 이렇게 만들어진다.
Location이 여기서 출력되는 경로
Location과 엮이는 곳은 컴파일된 코드를 디스어셈블할 때다. HTMLGenerator는 nmethod를 명령어 단위로 출력하다가 그 명령어에 safepoint 디버그 정보(ScopeDesc)가 붙어 있으면 genSafepointInfo()로 디버그 정보를 같이 찍는다.
genHTML(NMethod)
└─ (디스어셈블하며 명령어마다) genSafepointInfo(nm, sd)
├─ genScope() : 지역 변수 / 표현식 스택 / 모니터
│ └─ genHTMLForLocals() 등 → scopeValueAsString() → locationAsString()
└─ genScObjInfo() : 스칼라 치환된 객체의 필드
└─ scopeValueAsString() → locationAsString()
genScObjInfo()는 스칼라 치환된 객체마다 ScObj0 <클래스 이름>={ [필드 이름 :인덱스]=(#값) ... } 형태로 필드를 나열한다. 필드 값이 레지스터나 스택에 있으면 locationAsString()이 stack[16], oop처럼 “위치, 타입” 문자열을 만든다. IntVector128이 스칼라 치환된 safepoint라면 payload 필드가 바로 Location::vector 위치로 나온다.
clhsdb에서 Vector API를 쓰는 메서드의 nmethod 주소로 disassemble을 하거나 HSDB Code Viewer에서 같은 nmethod를 열기만 해도, 리포터가 겪은 것과 같은 문제를 만날 수 있다. 리포터처럼 SA 내부 API로 에이전트를 직접 짜지 않아도 닿는 경로다.
locationAsString()의 타입 분기는 이렇게 생겼다.
// src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/ui/classbrowser/HTMLGenerator.java
buf.append(", ");
if (type == Location.Type.NORMAL) {
buf.append("normal");
} else if (type == Location.Type.OOP) {
buf.append("oop");
...
} else if (type == Location.Type.DBL) {
buf.append("double");
} else if (type == Location.Type.ADDR) {
buf.append("address");
} else if (type == Location.Type.INVALID) {
buf.append("invalid");
}
위에서 본 genScObjInfo() 경로는 스칼라 치환된 객체의 payload 위치를 출력한다는 점에서 리포터가 밟은 경로와 같다. 수정 전에는 앞에서 getType()을 부르는 순간 예외가 났고, Location.java만 고친 상태라면 예외는 사라지지만 타입 자리가 비어서 stack[16], 처럼 출력된다. 여기에도 Location.printOn()과 같은 방식으로 vector 분기를 넣었다. 이 파일도 헤더의 저작권 연도를 2002, 2023에서 2002, 2026으로 바꿨다.
} else if (type == Location.Type.DBL) {
buf.append("double");
+ } else if (type == Location.Type.VECTOR) {
+ buf.append("vector");
} else if (type == Location.Type.ADDR) {
buf.append("address");
재현해보기
재현 코드
리포터는 이슈에 Test.java와 MyAgent.java를 첨부해 두었다. 구조는 단순하다.
Test.java:IntVector.fromArray(SPECIES_128, ...)로 벡터를 만들고checkpoint()를 부른 뒤reduceLanes로 값을 쓴다.checkpoint()를 호출하는 시점에 벡터가 살아 있으니 이 호출 지점의 디버그 정보에 벡터가 들어간다. 워밍업이 끝나면checkpoint()안에서START_SPINNING을 찍고 무한히 돈다.MyAgent.java:Test를-Xbatch -XX:-TieredCompilation,compileonly Test::test,dontinline Test::checkpoint옵션으로 띄운다.START_SPINNING이 나오면HotSpotAgent로 그 프로세스에 attach한다. 그다음 main 스레드에서 컴파일된test프레임을 찾아 스칼라 치환된 객체의 필드ScopeValue를 하나씩 출력한다.
MyAgent에 control 인자를 주면 -XX:-EnableVectorReboxing으로 띄운다. 이 경우 벡터가 SIMD 값이 아니라 oop으로 기록되어 원래도 예외가 나지 않는다. 대조군으로 쓰라고 넣어 둔 옵션이다.
J=build/macosx-aarch64-server-release/images/jdk/bin
EXPORTS=(
--add-modules jdk.incubator.vector,jdk.hotspot.agent
--add-exports jdk.hotspot.agent/sun.jvm.hotspot=ALL-UNNAMED
--add-exports jdk.hotspot.agent/sun.jvm.hotspot.code=ALL-UNNAMED
--add-exports jdk.hotspot.agent/sun.jvm.hotspot.debugger=ALL-UNNAMED
--add-exports jdk.hotspot.agent/sun.jvm.hotspot.oops=ALL-UNNAMED
--add-exports jdk.hotspot.agent/sun.jvm.hotspot.runtime=ALL-UNNAMED
--add-exports jdk.hotspot.agent/sun.jvm.hotspot.types=ALL-UNNAMED
)
$J/javac "${EXPORTS[@]}" -d out Test.java MyAgent.java
sudo $J/java "${EXPORTS[@]}" \
--add-opens jdk.hotspot.agent/sun.jvm.hotspot.code=ALL-UNNAMED \
-cp out MyAgent
macOS에서는 SA가 다른 프로세스에 attach하려면 권한이 필요하다. Developer mode(DevToolsSecurity)가 꺼져 있으면 root여야 해서 sudo로 실행했다. JDK의 SA 테스트 유틸(SATestUtils.skipIfCannotAttach())도 이 조건이 안 맞으면 테스트를 건너뛴다. 또 배포판 JDK처럼 hardened runtime으로 서명된 JDK에는 아예 attach할 수 없다. 그래서 수정 전 동작은 직접 돌리지 않고 리포터가 올린 출력으로 비교했다.
결과
수정한 빌드로 돌린 결과 중 관련 부분만 옮겼다.
#### CompiledVFrame:
ScopeDesc for Method Test.test([I)I@0x00000001194014c8 @bci 9 reexecute=false
...
## ObjectValue:
scalarObj[45]
## -> klass mirror:
Oop for java/lang/Class @ 0x000000060058ec40 (object size = 160)
- name: {48} :"jdk.incubator.vector.IntVector128" @ 0x000000060058f008
...
# ScopeValue:
Value 7, stack[0],vector
##### Thread: Reference Handler
...
| 수정 전 (리포터 출력) | 수정 후 | |
|---|---|---|
# ScopeValue: | Value 7, 출력 후 RuntimeException: should not reach here | Value 7, stack[0],vector |
| 이후 진행 | 예외로 종료 | 나머지 스레드까지 끝까지 출력 |
리포터와 같은 Test.test @bci 9 지점의 IntVector128 스칼라 치환 객체다. IntVector128의 인스턴스 필드는 payload 하나뿐이라 ScopeValue도 하나만 나온다. 그 위치가 Location::vector 타입으로 제대로 디코드됐다.
벡터가 레지스터가 아니라 stack[0]에 있는 것도 이상하지 않다. checkpoint()는 인라인하지 않은 실제 호출이고, 호출을 거치면 SIMD 레지스터 값은 보존되지 않는다(caller-saved). 그래서 C2가 호출 전에 벡터를 스택에 내려 두고(spill), 디버그 정보에도 그 스택 위치가 기록된다. Value 7을 비트로 풀면 타입 7(vector), Where 0(스택), 오프셋 0이니 출력과도 맞는다.
이 재현으로 확인한 것은 Location.printOn() 경로다. HTMLGenerator.locationAsString() 경로는 아직 따로 실행해 보지 않았다.
재현 코드 분석하기
테스트는 이미 리포터가 재현 코드로 만들어 두었다. 해당 재현 코드를 분석해보면 다음과 같다.
Test.java는 SA가 들여다볼 대상 프로그램이다.
static int test(int[] array) {
IntVector v = IntVector.fromArray(IntVector.SPECIES_128, array, 0);
checkpoint(); // vector will be put in debug state of frame
return v.reduceLanes(VectorOperators.ADD);
}
static void checkpoint() {
if (isWarmup) { return; }
System.out.println("START_SPINNING");
while (trueFlag) {
Thread.onSpinWait();
}
}
v는 checkpoint() 호출을 넘어 살아 있어야 하는 값이라, 호출 지점의 디버그 정보에 payload 위치가 Location::vector로 기록된다. main()은 워밍업으로 test()를 C2 컴파일시킨 뒤 한 번 더 호출하고, 이번에는 checkpoint()가 무한 루프에 들어간다. 그러면 main 스레드 스택에 vector를 들고 있는 컴파일된 test() 프레임이 남는다.
MyAgent.java는 이 프로그램을 다음 옵션으로 띄운다.
"-Xbatch", "-XX:-TieredCompilation",
"-XX:CompileCommand=compileonly,Test::test",
"-XX:CompileCommand=dontinline,Test::checkpoint"
-Xbatch -XX:-TieredCompilation은 워밍업이 끝날 때 test()가 확실히 C2로 컴파일되게 하고, compileonly는 컴파일 대상을 test() 하나로 좁힌다. dontinline은 꼭 필요하다. checkpoint()가 인라인되면 한 번도 실행되지 않은 스핀 분기가 uncommon trap으로 바뀌고, 마지막 호출에서 역최적화가 일어나 컴파일된 프레임이 남지 않는다. compileonly만으로는 인라인이 막히지 않는다.
START_SPINNING이 출력되면 attach해서 main 스레드의 프레임을 거슬러 올라간다.
for (var frame = thread.getLastJavaVFrameDbg(); frame != null; frame = frame.javaSender()) {
if (frame instanceof sun.jvm.hotspot.runtime.CompiledVFrame compiled &&
compiled.getMethod().getName().asString().equals("test")) {
ScopeDesc activeScope = compiled.getScope();
...
for (ScopeValue value : object.getFieldsValue()) {
value.printOn(System.out);
컴파일된 test 프레임을 찾으면 getScope()로 checkpoint() 호출 지점의 ScopeDesc를 얻고, 스칼라 치환된 객체(getObjects())의 필드 값을 printOn()으로 출력한다. 수정 전에는 이 printOn() 안에서 getType()이 should not reach here를 던진다.
마무리
수정 자체는 vmStructs.cpp에 상수 한 줄을 내보내고 SA의 Location.java, HTMLGenerator.java에 vector 분기를 채워 넣은 게 전부다. 원인도 리포터가 짚어 둔 그대로였다. 시간이 더 든 건 회귀 테스트였다. 재현 코드는 이미 있었지만 LingeredApp과 SATestUtils로 옮기려면 SA 테스트가 대상 프로세스를 어떻게 띄우고 붙는지부터 알아야 했고, dontinline 하나가 빠지면 컴파일된 프레임이 남지 않는다는 것도 직접 확인해야 했다.
Vector API에 기여하고 싶어서 고른 이슈였는데, 막상 들여다본 건 대부분 SA 쪽 코드였다. 그래도 C2가 벡터를 디버그 정보에 어떻게 남기는지 따라가 볼 수 있어서 좋은 시작이었다고 생각한다. 다음에는 Vector API 안쪽으로 조금 더 들어가 보고 싶다.