JDK 28 값 클래스, 평탄화 이득 뒤에 표현 전환과 구체화 비용도 있음
- JEP 401이 JDK 28 미리보기 기능으로 통합됐으며, 값 클래스는 정체성을 포기해 JVM이 위치별로 평탄화나 스칼라화 표현을 선택하게 함
FourLongs는 long 4개와 null 마커로 구성돼 현재 JVM의 원자적 평탄화 갱신 크기를 넘지만, record의 엄격히 초기화된 final 필드인Envelope.payload에는 33바이트 비원자 평탄화 레이아웃이 적용됨- 같은
FourLongs를 가변MutableEnvelope.payload에 넣으면 JVM은 참조 레이아웃을 사용함. 두 스레드의 4개 컴포넌트 쓰기가 섞여(1, 1, 0, 0)같은 찢어진 값을 관찰하는 일을 Java 메모리 모델이 허용하지 않기 때문임 - 루프에서
bumpA가 매번 새FourLongs를 만드는 소스 코드도 C2 컴파일러는 호출 지점의 컴포넌트 갱신으로 분석해 할당 없이 컴파일할 수 있음 - 값 클래스는 모든 곳에서 더 빠르지 않음. 어떤 메서드는 평탄 표현이 빠르고 다른 메서드는 참조 표현이 빨라, 서로 오갈 때 JVM이 구체화와 표현 변환 비용을 지불할 수 있음
Hacker News opinions
자바는 원래 값과 참조 같은 저수준 선택을 신경 쓰지 않아도, 충분히 똑똑한 컴파일러가 프로파일링해서 최선의 방법을 고른다는 식으로 설명했잖아. 왜 이제 와서 C++ 같은 기능을 넣는 건지 궁금함.
자바는 너무 동적이라 어떤 클래스가 값 친화적으로만 쓰인다는 걸 증명하기 어려움. 타입 소거, 다형성, 런타임 클래스 로딩이 있고, 다른 모듈에서 ==로 정체성을 볼 수도 있어서 전 프로그램 분석은 컴파일 시간도 망가뜨릴 수 있음.
이 논의가 갑자기 나온 건 아님. 이스케이프 분석만으로 부족해서 명시적 값 타입이 필요하다는 얘기는 적어도 10년 넘게 있었음.
아무리 똑똑한 컴파일러라도 프로그램 의미를 바꾸진 못함. 닫힌 세계라는 가정 없이는 다른 코드가 정체성을 관찰하지 않는다고 단정할 수 없고, 값 클래스는 그 의미를 정적으로 선택하게 하는 장치임.
90년대의 'CPU가 개발자보다 싸다'는 전제가 지금은 맞지 않음. CPU와 RAM의 속도 차이 때문에 객체 배열의 포인터 추적 비용이 커졌고, 메모리에 연속 배치되는 값 타입의 이점도 커졌음.
값과 참조의 선택은 저수준 구현 선택이 아니라 의미 선택임. 여기서는 C++처럼 레이아웃을 직접 지정하는 게 아니라 정체성 필요 여부를 선언하고, JVM이 위치별 표현을 고르는 방식이라 기존 원칙도 유지됨.
성능이 중요한 자바 코드를 짜는 사람이라면 컴파일러가 알아서 해결해 준다고 믿지 않음. 실제로 빠르게 만들려면 자바답지 않은 코드 모양이 나오는 경우가 많았음.
값 클래스의 효과는 대규모 배열에서 가장 클 듯함. double x, y를 든 Point를 많이 담으려면 예전에는 double[] x, double[] y나 직접 버퍼로 가야 했고, 난 PrintAssembly로 범위 검사 제거와 인라이닝을 확인하곤 했음.
성능 이전에 값 의미가 더 이해하기 쉬움. 숫자 2는 누구에게나 같은 2인데 (2,3)은 Java 객체가 되면 ==와 equals()를 구분하고 hashCode()도 챙겨야 함. 값 표현을 고르면 그 모델 자체가 단순해짐.
이스케이프 분석이 여기서 많은 일을 하긴 하지만, 완전히 믿을 수 있기 전까지는 할당이 사라질지 추측하는 셈임.
오히려 이스케이프 분석보다 값 클래스 ABI 쪽이 중요하다고 봄. 타입 소거되거나 원자적으로 써야 하는 지점에서는 원래 객체 하나였을 경우보다 추가 할당이 생길 수 있고, C#의 값 타입 박싱과 비슷함.
JVM 이스케이프 분석은 늘 될 때도 있고 안 될 때도 있었음. 이번 변화로 예측 가능성이 조금 나아진 건 반가움.