Dev

만화로 소개하는 ArrayBuffer 와 SharedArrayBuffer

ingeeC 2024. 5. 25. 08:51

한국 Mozilla Hacks에 올렸던 글을 편집해서 복사합니다.
한국 모질라 기술 블로그의 상태가 회복되기를 기원합니다.

 

---

만화로 소개하는 ArrayBuffer 와 SharedArrayBuffer

 

이 글은 3부작 시리즈의 두번째 글입니다.

  1. 메모리 특강
  2. 만화로 소개하는 ArrayBuffer 와 SharedArrayBuffer
  3. Atomics 를 이용해서 SharedArrayBuffer 레이스 컨디션 피하기

 

지난 글에서는, JavaScript 같은 메모리 자동 관리 랭귀지의 메모리 관리 방식을 설명했습니다. 그리고 또 C 같은 메모리 수동 관리 랭귀지의 메모리 관리 방식도 설명했습니다.

ArrayBuffersSharedArrayBuffers 에 대해 이야기 하려고 하는데, 왜 이런 얘기가 필요한 걸까요?

왜냐하면 ArrayBuffers 를 이용하면 JavaScript 를 사용하는 경우에도 데이터를 수동으로 관리할 수 있는 여지가 생기기 때문입니다. JavaScript 랭귀지가 메모리 자동 관리 랭귀지이지만 말입니다.

어떨 때 메모리를 수동으로 관리하고 싶어질까요?

지난 글에서 이야기한 것처럼, 메모리 자동 관리 방식에는 장단점이 존재합니다. 메모리 자동 관리 방식이 개발자가 쓰기에는 편리하지만, 실행 성능 측면에서는 약간의 오버헤드를 감수해야 합니다. 어떤 상황에서는 이런 오버헤드가 문제 될 수 있습니다.

 

저울의 눈금이 '메모리 자동 관리' 방식의 경우, 쓰기 쉽지만 실행 속도를 높이기 어렵다는 사실을 말해준다

 

예를 들어, 우리가 JS 에서 변수를 만들 때, JS 엔진은 변수의 타입과 변수 값의 표현하는 방식을 추측해야 합니다. 이것이 단지 추측이기 때문에, JS 엔진은 보통 변수가 정말로 필요로 하는 것보다 많은 공간을 확보합니다. 변수에 따라, 필요한 공간보다 2~8배 많은 메모리 슬롯(memory slot)을 확보합니다. 상당히 많은 공간이 낭비됩니다.

그리고 JS 객체를 만들고 사용하는 어떤 종류의 패턴은 가비지 컬렉터를 힘들게 만듭니다. 만약 우리가 메모리 수동 관리 방식을 사용한다면, 우리의 유즈케이스(use case)에 꼭 맞는 메모리 할당 정책과 해지 정책을 선택할 수 있습니다.

대부분의 경우는, 이런 일로 고민할 필요가 없습니다. 대부분의 유즈케이스는 메모리 수동 관리 방식을 선택할만큼 실행 성능에 민감하지 않습니다. 일반적인 유즈케이스에서는 메모리 수동 관리 방식의 실행 성능이 오히려 늦을 수도 있습니다.

하지만 우리가 작성한 코드에서 성능 최고치를 뽑아내기 위해 깊숙한 레벨(low-level)에서 일해야 하는 경우, ArrayBuffers 와 SharedArrayBuffers 가 도움 될 것입니다.

 

저울의 눈금이 '메모리 수동 관리' 방식의 경우, 성능을 높일 수 있는 여지는 높지만 더 많은 생각과 계획이 필요하다는 사실을 말해준다

 

그래서 ArrayBuffer 는 어떻게 동작하나요?

기본적으로 ArrayBuffer 는 다른 JavaScript 배열과 비슷하게 동작합니다. 다만, ArrayBuffer 를 이용할 때, 우리는 객체나 문자열 같은 JavaScript 가 제공하는 타입(type)들을 ArrayBuffer 에 저장할 수 없습니다. ArrayBuffer 에 저장할 수 있는 것은 오로지 (숫자로 표현할 수 있는) 바이트열(bytes) 뿐입니다.

 

2개의 배열, 일반 배열(array)은 숫자, 객체, 문자열 등을 담을 수 있고, ArrayBuffer는 bytes만 담을 수 있다

 

여기서 분명히 해둘 것은 우리가 바이트 값을 ArrayBuffer 에 직접 추가하지 않는다는 것입니다. ArrayBuffer 스스로는 바이트열의 크기가 얼마나 큰지, 또 바이트열로부터 어떤 타입의 숫자를 변환해야 하는지 알지 못합니다.

ArrayBuffer 자체는 단지 0 과 1 이 한 줄로 나열된 덩어리일 뿐입니다. ArrayBuffer 는 배열 첫째 요소와 둘째 요소 사이의 구분이 어디에 위치하는지도 모릅니다.

 

1과 0의 덩어리가 한 줄로 나열되어 있다

 

문맥에 맞는 정보를 제공하려면, 그러니까 이 덩어리를 적절한 규격의 박스로 나누려면, ArrayBuffer 를 view 라고 불리는 것으로 감싸야 합니다. 우리는 view 로 표현된 데이터를 typed array (형식화 배열)에 추가할 수 있습니다. JavaScript 는 view 를 다룰 수 있는 다양한 typed array (형식화 배열)를 제공합니다.

예를 들어, 우리는 Int8 typed array (Int8 형식화 배열)를 이용해서 데이터 덩어리를 8-bit 단위의 바이트 값들로 나눌 수 있습니다.

 

8-bit 단위의 상자로 구분된 1과 0

 

또는 unsigned Int16 배열을 이용해서 데이터 덩어리를 16-bit 단위의 바이트 값들로 나눌 수 있습니다. 그래서 나뉘어진 값들을 unsigned integer 값들로 다룰 수 있습니다.

 

16-bit 단위의 상자로 구분된 1과 0

 

심지어 우리는 한개의 버퍼에 여러개의 view 를 적용하는 것도 할 수 있습니다. 적용하는 view 가 달라지면 동일한 연산의 수행 결과도 달라집니다.

예를 들어, 우리가 Int8 view 를 통해 ArrayBuffer 에서 0 번째 요소와 1 번째 요소를 가져오는 경우, 그 값은 Uint16 view 를 통해 가져오는 값과 다를 것입니다. ArrayBuffer 가 완전히 동일한 bit 값들을 갖고 있음에도 불구하고 말이죠.

 

8-bit 단위의 상자와 16-bit 단위의 상자로 구분된 1과 0

 

이런 방식으로, ArrayBuffer 는 기본적으로 메모리 자체인 것처럼 동작합니다. ArrayBuffer 는 C 같은 랭귀지를 사용할 때처럼 메모리를 직접 다루는 방식과 비슷한 효과를 만들어 냅니다.

아마 당신은 프로그래머에게 메모리를 직접 다루는 수단을 주지 않고 이런 추상적인 계층을 만든 이유가 궁금할 것입니다. 메모리에 대한 직접적인 접근을 허용하면 보안 측면에서 헛점이 노출되기 쉽습니다. 다음에 다른 글을 통해 이 주제에 대해 설명하겠습니다.

 

SharedArrayBuffer 는 또 뭔가요?

SharedArrayBuffer 를 설명하기 전에, JavaScript 를 이용한 병렬 처리 코드에 대해 조금 설명해야 할 것 같습니다.

우리는 프로그램을 좀더 빠르게 만들기 위해, 또는 프로그램이 사용자 이벤트에 좀더 빠르게 반응하도록 만들기 위해 병렬 처리 코드를 사용합니다. 이를 위해, 우리는 작업을 분할합니다.

통상적인 어플리케이션에서는 작업을 한 사람(즉, 메인 스레드)이 처리합니다. 제가 예전에 이에 대해 설명한 적이 있는데, 그때 저는 메인 스레드를 풀-스택 개발자에 비유했었습니다. 메인 스레드가 JavaScript 실행, DOM 처리, 레이아웃 처리 등 모든 일을 담당합니다.

메인 스레드의 작업을 보조하는 것이라면 무엇이더라도 작업 효율을 개선합니다. 이런 상황에서는, ArrayBuffer 가 메인 쓰레드의 작업을 보조할 수 있습니다.

 

책상에 앉은 메인 스레드가 서류 더미를 처리하고 있다. 방금 서류 더미의 맨 윗 부분을 처리했다.

 

하지만 언젠가 메인 쓰레드의 작업을 보조하는 것으로 충분하지 않은 때가 옵니다. 무엇인가를 결단해야 하는 순간… 그러니까 작업을 분리해야 하는 순간이 옵니다.

대부분의 프로그래밍 랭귀지들의 경우, 스레드(thread)라는 것을 이용해서 작업을 분할합니다. 이것은 기본적으로 여러 명이 한 프로젝트를 함께 수행하는 것과 비슷합니다. 만약 다른 작업들과 특별히 연관 없는 어떤 작업이 존재한다면, 해당 작업을 별도의 스레드로 처리할 수 있습니다. 그러면, 2개의 쓰레드가 분리된 작업을 동시에 처리합니다.

JavaScript 에서는 이런 일은 web worker 를 이용해서 구현합니다. web worker 는 다른 랭귀지의 스레드와 조금 다릅니다. 기본적으로 web worker 는 메모리를 공유하지 않습니다.

 

2개의 스레드가 나란히 책상에 앉아 있다. 각자의 서류 더미 높이는 예전의 절반이다. 각자의 발밑에 메모리 덩어리가 놓여있다. 2개의 메모리 덩어리는 연결되어 있지 않다.

 

이는 만약 우리가 다른 스레드와 어떤 데이터를 공유하고 싶다면, 그 데이터를 복사해서 전달해야만 한다는 뜻입니다. 이 작업은 postMessage 함수에 의해 처리됩니다.

postMessage 에 어떤 객체를 전달하면, postMessage 함수는 그것을 직렬화해서(serialize) 다른 web worker 에 전달합니다. 그러면 데이터를 전달 받은 web worker 가 데이터를 풀어서(deserialize) 메모리에 복사합니다.

 

스레드1이 스레드2와 메모리를 공유한다. 공유할 메모리를 직렬화(serialize)하고, 전송해서, 스레드2의 메모리에 복사한다.

 

이건 매우 느린 작업입니다.

ArrayBuffer 같은 메모리의 경우, 메모리 전달하기(transferring memory)라는 것이 가능합니다. 메모리 전달하기란 메모리의 특정 블록 소유권을 다른 web worker 로 이전하는 것입니다.

그러면 원래 해당 메모리 블록을 소유하고 있던 web worker 는 더이상 그 블록에 접근할 수 없게 됩니다.

 

스레드1이 메모리 전달하기(trasfer) 방법으로 스레드2에게 메모리를 공유한다. 스레드1은 더이상 그 메모리에 접근할 수 없다.

 

어떤 유스케이스에서는 이 방식이 적절합니다. 하지만 고성능 병렬 처리 코드가 필요한 많은 경우, 우리가 정말 원하는 것은 공유 메모리 (shared memory)입니다.

SharedArrayBuffer 가 바로 이 기능을 제공합니다.

 

두 스레드가 모두 함께 접근할 수 있는 공유 메모리를 얻는다

 

SharedArrayBuffer 를 쓰면 2개의 web work (즉 2개의 스레드) 모두가 메모리의 같은 영역을 읽고 쓸 수 있습니다.

이는 더이상 postMessage 를 쓸 때 감수해야 했던 통신 오버헤드와 시간지연을 겪지 않아도 된다는 뜻입니다. 2개의 web worker 모두가 데이터에 즉시 접근할 수 있습니다.

이렇게 2개의 스레드가 동시에 즉각적으로 접근할 수 있기 때문에 어떤 위험한 상황을 감수해야 합니다. 즉 레이스 컨디션(race condition)이 발생할 수 있습니다.

 

두 스레드가 메모리를 향해 달려간다 (racing)

 

레이스 컨디션에 대해서는 다음 글에서 설명하겠습니다.

 

SharedArrayBuffer 의 현재 상태를 알고 싶은가요?

곧 SharedArrayBuffer 를 모든 주요 브라우저들이 지원할 것입니다.

 

하이 파이브 하고 있는 메이저 브라우저들

 

Safari (Safari 10.1) 는 이미 SharedArrayBuffer 를 지원합니다. Firefox 와 Chrome 은 7월/8월 버전에서 SharedArrayBuffer 를 지원하기 시작할 것입니다. Edge 는 가을에 있을 윈도우즈 업데이트를 통해 SharedArrayBuffer 를 지원할 것입니다.

ArrayBuffer 와 SharedArrayBuffer 가 모든 주요 브라우저들에서 지원되더라도, 어플리케이션 개발자가 ArrayBuffer 와 SharedArrayBuffer 를 직접 이용하지는 않을 것입니다. 사실, 우리는 그러지 않는 것을 추천합니다. 당신은 가능한 가장 높은 수준의 추상화 도구를 선택하는 것이 좋습니다.

우리는 JavaScript 라이브러리 개발자들이 당신을 위해 SharedArrayBuffer 를 쉽고 안전하게 사용할 수 있는 라이브러리를 개발하리라고 기대합니다.

덧붙여, 일단 SharedArrayBuffer 가 플랫폼에 장착되면, WebAssembly 가 이를 이용해서 스레드를 지원할 수 있게 됩니다. 그게 현실이 되면, 우리는 컨커런시(concurrency)를 추상적인 레벨에서 쉽게 사용할 수 있습니다. 마치 Rust 같은 랭귀지처럼요. Rust 랭귀지는 두려움 없이 컨커런시를 쓰는 것을 목표로 하는 랭귀지 입니다.

다음 글에서, 우리는 도구들 (Atomics)을 살펴볼 것입니다. 이 도구들은 라이브러리 개발자들이 레이스 컨디션을 회피하는 용도로 사용하는 것입니다.

 

SharedArrayBuffer + Atomics와 JS 라이브러리 + WebAssembly의 계층 구조

 

이 글은 Lin Clark이 쓴 A cartoon intro to ArrayBuffers and SharedArrayBuffers의 한국어 번역본입니다.

한국 Mozilla Hacks에 올렸던 글을 편집해서 복사합니다.