News Feed

자동 벡터화의 한계를 넘다…자바 벡터 API 병렬의 세계

컨텐츠 정보

  • 조회 763

본문

개발자와 데브옵스가 공통적으로 집착하는 한 가지가 있다면, 바로 애플리케이션 성능 개선이다. 궁극적으로 성능이 좋을수록 비용이 낮아지거나(리소스 사용 감소) 수익이 커진다(향상된 서비스를 제공해 더 많은 고객 유치).

성능을 개선하는 방법은 무수히 많지만 가장 확실한 방법은 “쪼개서 해결하기”다. 알고리즘을 최적화했고 하드웨어도 업그레이드했는데 여전히 필요한 성능을 달성하지 못했다고 가정해 보자. 이 문제를 해결하려면 스택 더 깊이, 벡터(Vector) 연산이 여러 데이터 포인트를 동시에 처리할 수 있는 CPU 수준까지 들어가야 한다. 한 번에 여러 작업을 할 수 있으면 작업을 완료하는 데 걸리는 시간도 대체로 줄어든다(물론 항상 그렇지는 않다).

동시성과 병렬성은 성능 개선에 대한 대화에서 혼용되는 경우가 많다. 우선 이 둘의 차이점부터 알아본다.

첫 번째 작업이 시작된 후 그 작업이 끝나기 전에 두 번째 작업이 실행되면 두 작업이 동시에 실행된다고 한다. 두 작업이 모든 시점에서 항상 동시에 실행될 필요는 없다. 이 기술은 특히 OS에서 오랫동안 사용됐다. 멀티 코어, 멀티 CPU 머신이 등장하기 전에는 하나의 실행 단위를 모든 프로세스가 공유해야 했다. 프로세스가 동시에 실행되고 있다는 착시 효과를 제공하기 위해 여러 프로세스를 병렬로 실행하면서 실행 단위를 매우 빠르게 전환하는 방식을 사용했는데, 이를 시분할 운영체제(time-sharing operating system)라고 한다.

작업이 병렬로 실행되기 위해서는 단순히 ‘실행이 겹치는 것(동시성)’이 아니라 ‘실제로 동시에 실행(병렬성)’돼야 한다.

벡터 처리는 어떻게 작동하는가

무어의 법칙이 같은 공간에 더 많은 트랜지스터를 집적했다면, 이제는 동시 프로세스를 병렬로 실행할 수 있게 해주는 멀티 코어 프로세서를 통해 더 높은 성능을 끌어낼 수 있다. CPU의 하위 수준에는 벡터 연산이라는 특정 유형의 작업을 병렬로 실행하기 위한 하드웨어가 포함돼 있다.

숫자 집합이 있고, 이 집합의 모든 숫자에 동일한 연산을 적용하려는 경우를 가정해 보자. 예를 들어, 모든 값을 1씩 증가시켜야 한다. 자바에서 이와 같은 연산을 처리하는 일반적인 방법은 모든 값을 배열에 저장하고, 배열을 반복 처리하는 루프를 만들어서 루프 본문에서 각 값에 1을 추가하는 것이다. 자바 애플리케이션을 실행하면 자주 사용되는 코드는 가상 머신 명령어 집합의 바이트코드에서 네이티브 명령어로 컴파일된다. JVM은 JIT(Just-In-Time) 컴파일러를 사용해서 이 작업을 수행한다.

똑똑한 JIT는 기반 프로세서 아키텍처를 이해하고 벡터 연산을 사용하도록 루프를 최적화한다. 이를 자동 벡터화(autovectorization)라고 한다.

벡터 처리에서는 2개 이상의 값을 저장하기 위해 매우 넓은 레지스터를 사용한다. 예를 들어 AVX-2 인텔 명령어는 256비트 폭의 레지스터를 사용한다. 자바 정수는 32비트로 저장되므로 각 벡터 레지스터는 8개의 자바 정수(int)를 담을 수 있다. JIT는 배열에서 값을 8개씩 그룹으로 로드하는 코드를 생성한다. 그러면 코드는 AVX-2 명령어 중 하나를 사용해 CPU에 이 8개 값 각각에 1을 더하도록 지시할 수 있다. 또한 오버플로우를 처리해서 인접한 값이 손상되지 않도록 한다.

모든 값이 하나의 기계 명령어 사이클에서 처리되므로 진정한 병렬 처리다. 여기서 얻는 효과는 배열을 처리하는 데 걸리는 시간이 자동 벡터화를 사용하지 않을 때의 1/8에 불과하다는 점이다.

귀가 솔깃한 이 말은 자바 개발자가 원하는 방식으로 코딩한 다음 JIT 컴파일러에 맡겨 런타임에 코드를 최적화하도록 할 수 있음을 의미한다.

그러나 이는 전체 그림의 일부일 뿐이다.

자동 벡터화는 위와 같은 단순한 상황에서는 잘 작동한다. 그러나 루프를 약간이라도 더 복잡하게 만들면 이 같은 JIT 컴파일러의 성능 개선 역량이 빠르게 무력화될 수 있다. 루프 본문에 값을 증가시켜야 하는지를 테스트하기 위한 간단한 조건을 추가하면 JIT는 벡터 연산을 사용하지 않고 순차적 접근 방식으로 돌아간다.

자바 벡터 API의 등장

한 가지 해결 방법은 자바 개발자가 벡터 연산을 어떻게 사용해야 하는지를 명시적으로 규정하는 코드를 작성하도록 하는 것이다. JIT 컴파일러는 자동 벡터화 없이 직접 이를 변환할 수 있다. JDK 16에서 인큐베이터 모듈로 도입된 자바 벡터 API의 용도가 바로 이것이다.

흥미롭게도 이 API는 오픈JDK에서 가장 장기간 인큐베이팅된 기능이라는 기록을 보유하고 있는데, JDK 24가 출시되면 9번째 버전이 된다. 참고로 이는 끊임없이 변화하기 때문이 아니라 발할라(Valhalla)라는 더 큰 프로젝트의 일부이기 때문이다. 자바에 값 타입을 추가할 발할라가 오픈JDK에 제공되는 시점에 벡터 API도 최종화될 것이다.

벡터 API는 폭넓은 기능을 제공한다. 첫째, 각 자바 프리미티브 숫자 타입을 벡터로 표현하기 위한 클래스가 있다. 벡터 종은 이런 프리미티브 벡터 형태를 CPU별 레지스터와 결합하므로 배열에서 데이터를 채우는 방법을 쉽게 이해할 수 있다. 벡터 조작에 사용할 수 있는 연산자는 103개로 풍부하다. 현실적으로 필요한 것은 모두 포함된다.

또한 벡터 API는 JIT 컴파일러가 수치 집약적 연산을 위해 고도로 최적화된 코드를 생성하는 데 필요한 모든 요소를 개발자에게 제공한다. 대부분 작업에서는 결과적으로 숫자를 다루므로(문자열은 숫자로 인코딩되는 문자 시퀀스일 뿐임) 이를 통해 상당한 성능 개선을 실현할 수 있다.

이상적인 상황은 벡터 API를 사용할 필요 없이 자동 벡터화를 통해 이를 투명하게 처리하는 것이다. 다행히 고성능 JVM에는 다른 JIT 컴파일러가 포함돼 있다. 팰콘(Falcon) JIT 컴파일러(오픈JDK C2 JIT를 대체함)는 마찬가지로 오픈소스 프로젝트인 LLVM을 기반으로 한다. 이는 벡터를 사용할 수 있는 사례를 훨씬 더 많이 인식하므로 코드 변경 없이 애플리케이션 성능을 더 높일 수 있다.

팰콘 JIT 컴파일러는 개발 및 평가 용도로 무료인 아줄 플랫폼 프라임 JDK(Azul Platform Prime JDK)에서 사용할 수 있다. TCK(기술 호환성 키트) 테스트를 거친 JDK인 아줄 플랫폼 프라임은 바로 사용 가능하다. 애플리케이션에 직접 사용해보기를 권한다.
dl-itworldkorea@foundryco.com

관련자료

댓글 0
등록된 댓글이 없습니다.
Member Rank