웹어셈블리에서 최대 4GB 메모리
소개
크롬과 엠스크립텐에서 최근 작업 덕분에, 이제 웹어셈블리 애플리케이션에서 최대 4GB의 메모리를 사용할 수 있습니다. 이전에는 2GB로 제한되었습니다. 왜 처음부터 제한이 있었는지 의아할 수 있습니다 - 예를 들어, 512MB 또는 1GB 메모리를 사용하는 데는 아무 작업이 필요하지 않았기 때문입니다! 하지만 브라우저와 툴체인에 있어서 2GB에서 4GB로의 도약에는 특별한 일이 발생한다고 밝혀졌습니다. 이 글에서는 그 내용을 설명하겠습니다.
32비트
더 자세한 내용에 들어가기 전에 배경을 살펴보겠습니다. 새로운 4GB 제한은 현재 웹어셈블리가 지원하는 32비트 포인터, 즉 LLVM 및 기타 곳에서 알려진 "wasm32"에서 가능한 최대 메모리 양입니다. 포인터가 64비트인 "wasm64" (wasm 스펙에서 "memory64")로 가는 작업이 이루어지고 있으며, 이를 통해 16백만 테라바이트 이상의 메모리를 사용할 수 있게 될 것입니다 (!). 하지만 그 때까지는 4GB가 우리가 접근할 수 있는 최대 용량입니다.
32비트 포인터가 허용하는 것이 4GB이기 때문에 항상 4GB를 접근할 수 있어야 할 것처럼 보입니다. 그런데 왜 우리는 절반인 2GB로 제한되었던 걸까요? 브라우저와 툴체인 측면에서 여러 가지 이유가 있습니다. 먼저 브라우저부터 시작해 설명하겠습니다.
크롬/V8 작업
원칙적으로 V8에서의 변화는 간단해 보입니다: 웹어셈블리 함수에 대한 모든 코드와 메모리 관리 코드가 메모리 인덱스와 길이에 대해 부호 없는 32비트 정수를 사용하도록 보장하면 됩니다. 그러나 실제로는 그렇지 않습니다! 웹어셈블리 메모리를 자바스크립트의 ArrayBuffer로 내보낼 수 있기 때문에, 자바스크립트 ArrayBuffers, TypedArrays, 그리고 Web Audio, WebGPU, WebUSB 같은 ArrayBuffers와 TypedArrays를 사용하는 모든 웹 API의 구현을 변경해야 했습니다.
첫 번째로 해결해야 했던 문제는 V8이 TypedArray 인덱스와 길이에 대해 Smis를 사용했다는 점입니다(즉, 31비트 부호 있는 정수). 따라서 최대 크기는 실제로 230-1, 약 1GB였습니다. 또한 모든 것을 32비트 정수로 바꾸는 것만으로는 충분하지 않았습니다. 왜냐하면 4GB 메모리의 길이는 실제로 32비트 정수 안에 들어갈 수 없기 때문입니다. 예를 들면: 십진수에서는 두 자리 숫자가 100개 있습니다(0부터 99까지). 하지만 "100" 자체는 세 자리 숫자입니다. 유사하게, 4GB는 32비트 주소로 접근할 수 있지만, 4GB 자체는 33비트 숫자입니다. 조금 낮은 제한으로 설정할 수도 있었겠지만, TypedArray 코드를 모두 수정해야 했던 만큼, 작업 도중 더 큰 미래 제한에 대비하기를 원했습니다. 따라서 TypedArray 인덱스나 길이를 다루는 모든 코드를 64비트 정수 타입 또는 자바스크립트와의 인터페이스가 필요한 경우에는 자바스크립트 Numbers로 변경했습니다. 추가적인 장점으로, 이것은 wasm64에서 더 큰 메모리를 지원하는 것이 이제 비교적 직선으로 가능하다는 것입니다!
두 번째 난관은 배열 요소에 대해 자바스크립트의 특수 처리와 일반 명명된 속성 간의 차이에 대처하는 것이었습니다. 이는 객체 구현에서 드러납니다. (이 문제는 자바스크립트 스펙과 관련된 다소 기술적인 문제입니다. 모든 세부 사항을 따라가기 어렵더라도 걱정하지 않아도 됩니다.) 아래 예를 고려해 보세요:
console.log(array[5_000_000_000]);
만약 array
가 일반 자바스크립트 객체나 배열이라면, array[5_000_000_000]
는 문자열 기반 속성 탐색으로 처리될 것입니다. 런타임은 "5000000000"라는 문자열 속성을 찾을 것입니다. 그러한 속성을 찾을 수 없는 경우, 프로토타입 체인을 탐색해 해당 속성을 찾거나 결국 체인의 끝에서 undefined
를 반환합니다. 그러나 array
자체, 또는 프로토타입 체인에 있는 객체가 TypedArray인 경우, 런타임은 인덱스 5,000,000,000에서 인덱싱된 요소를 찾거나 이 인덱스가 범위를 벗어난 경우 즉시 undefined
를 반환해야 합니다.
즉, TypedArrays에 대한 규칙은 일반 배열과 상당히 다르며 차이는 주로 큰 인덱스에서 나타납니다. 따라서 더 작은 TypedArray만 허용하면 구현이 상대적으로 간단할 수 있었습니다. 특히 속성 키를 한 번 살펴보는 것으로 "인덱싱" 또는 "명명된" 탐색 경로를 선택하는 것이 충분했습니다. 더 큰 TypedArray를 허용하려면, 이제 프로토타입 체인을 따라가며 이 구분을 반복적으로 해야 하며, 기존 자바스크립트 코드가 반복 작업과 오버헤드로 인해 느려지는 것을 피하기 위해 신중한 캐싱이 필요합니다.
툴체인 작업
툴체인 측에서도 작업을 해야 했습니다. 대부분은 WebAssembly의 컴파일된 코드가 아닌 JavaScript 지원 코드와 관련이 있었습니다. 주요 문제는 Emscripten이 메모리 접근을 다음과 같은 형태로 작성해왔다는 점입니다:
HEAP32[(ptr + offset) >> 2]
ptr + offset
주소에서 32비트(4바이트)를 부호 있는 정수로 읽습니다. 이는 HEAP32
가 Int32Array이기 때문에 가능한데, 배열의 각 인덱스는 4바이트를 가집니다. 그래서 바이트 주소(ptr + offset
)를 4로 나눠서 인덱스를 구해야 하며, 이것이 >> 2
가 수행하는 작업입니다.
문제는 >>
가 부호 있는 연산이라는 점입니다! 주소가 2GB 이상인 경우 입력이 오버플로우되어 음수로 변환됩니다:
// 2GB 바로 아래는 괜찮습니다. 이 출력값은 536870911입니다.
console.log((2 * 1024 * 1024 * 1024 - 4) >> 2);
// 2GB는 오버플로우되어 -536870912이 됩니다 :(
console.log((2 * 1024 * 1024 * 1024) >> 2);
해결 방법은 부호 없는 쉬프트 연산자인 >>>
를 사용하는 것입니다:
// 원하는 값인 536870912이 출력됩니다!
console.log((2 * 1024 * 1024 * 1024) >>> 2);
Emscripten은 2GB 이상의 메모리를 사용할 가능성이 있는지 컴파일 시점에 알고 있습니다(사용 중인 플래그에 따라 다름; 자세한 내용은 나중에 설명). 플래그가 2GB 이상의 주소를 가능하게 하면 컴파일러가 모든 메모리 접근을 자동으로 >>
대신 >>>
를 사용하도록 재작성합니다. 여기에는 위의 예처럼 HEAP32
등의 접근뿐만 아니라 .subarray()
와 .copyWithin()
같은 작업도 포함됩니다. 즉, 컴파일러가 부호 있는 포인터 대신 부호 없는 포인터를 사용하도록 전환합니다.
이 변환은 각 쉬프트에 한 글자가 추가되므로 코드 크기가 약간 증가합니다. 따라서 2GB 이상의 주소를 사용하지 않는다면 굳이 이를 실행하지 않습니다. 차이는 일반적으로 1% 미만이지만 불필요하고 피하기 쉬운 작업이며, 작은 최적화를 많이 하면 큰 차이를 만듭니다!
JavaScript 지원 코드에서 다른 드문 문제가 발생할 수 있습니다. 일반적인 메모리 접근은 앞서 설명한 대로 자동으로 처리되지만 부호 있는 포인터와 부호 없는 포인터를 수동으로 비교하는 경우(주소가 2GB 이상일 때) false
를 반환할 수 있습니다. 이러한 문제를 찾아내기 위해 Emscripten의 JavaScript를 감사했고 모든 내용을 주소 2GB 또는 이상의 특별 모드에서 테스트 슈트를 실행했습니다. (참고로, 직접 JavaScript 지원 코드를 작성할 경우 일반적인 메모리 접근 외에 포인터를 수동으로 처리하는 경우 이를 수정해야 할 수도 있습니다.)
사용해 보기
이를 테스트하려면 최신 Emscripten 릴리스를 다운로드하세요. 적어도 버전 1.39.15이 있어야 합니다. 그런 다음 다음과 같은 플래그로 빌드합니다.
emcc -s ALLOW_MEMORY_GROWTH -s MAXIMUM_MEMORY=4GB
이것들은 메모리 증가를 가능하게 하고 프로그램이 최대 4GB까지 메모리를 할당할 수 있도록 허용합니다. 기본적으로는 최대 2GB까지만 할당 가능하며, 명시적으로 2-4GB 사용을 허용해야 합니다(위에서 언급한 대로 >>>
대신 >>
를 사용함으로써 더 짧은 코드를 생성할 수 있기 때문입니다).
Chrome M83(현재 베타 버전) 이상에서 테스트해야 합니다. 문제가 있으면 꼭 알려주세요!
결론
최대 4GB 메모리 지원은 웹을 기본 플랫폼만큼 강력하게 만드는 또 다른 단계입니다. 이는 32비트 프로그램이 일반적과 동일한 양의 메모리를 사용할 수 있게 합니다. 이렇게만으로 완전히 새로운 클래스의 애플리케이션을 가능하게 하지는 않지만, 대형 게임 레벨이나 그래픽 편집기에서 큰 콘텐츠를 다루는 등 고급 경험을 가능하게 합니다.
앞서 언급했듯이 64비트 메모리 지원도 계획되어 있어 4GB 이상의 접근이 가능하게 될 것입니다. 그러나 wasm64는 기본 플랫폼의 64비트와 마찬가지로 포인터가 두 배의 메모리를 차지한다는 단점이 있습니다. 따라서 wasm32의 4GB 지원은 매우 중요합니다: 코드 크기가 wasm처럼 여전히 컴팩트한 상태에서 이전보다 두 배 많은 메모리를 접근할 수 있습니다.
항상 여러 브라우저에서 코드를 테스트하고, 2-4GB가 많은 메모리라는 점을 잊지 마세요! 그렇게 많은 메모리를 사용해야 한다면 사용하는 것이 맞지만, 그렇지 않은 경우 불필요하게 사용하지 마세요. 많은 사용자의 기기에서 충분한 여유 메모리가 없을 수 있습니다. 가능한 초기 메모리를 작게 시작해서 필요하면 메모리를 증가시키고, 증가를 허용하는 경우 malloc()
실패 상황을 우아하게 처리하는 것을 권장합니다.