본문으로 바로가기

typed array?

es6에서 도입된 녀석이다. js의 배열은 raw binary data를 다루지 못했기 때문에 Typed Arrays(형식화 배열)가 도입되었다.

웹에서 영상, 음성, 파일과 같은 binary를 자주 다루게 되면서 여러 값을 담을 수 있고 크기가 가변적인 array보다 좀 더 효율적인 typed array가 필요해진 것이다. 뿐만 아니라 딥러닝에서의 이진 데이터 인코딩/디코딩, GPU 메모리로 데이터를 전달할 때도 typed array를 사용한다. 웹에서 ML하려면 typed array 합시다.

 

그리고 이름이 배열이고, length 속성을 통해서 byte를 얻을 수 있지만 유사 배열입니다.

따라서 Array.isArray()를 통해 체킹해보면 false를 반환하며 배열이 prototype으로 보통 가지고 있는 메서드 중 일부를 지원하지 않습니다. push, pop가 대표적입니다. 같은거요.

 

typed array에는 무엇이 있나

 

강타입 언어를 배워봤다면 친숙한 것들이 보일 겁니다. U는 unsigned, 숫자는 bit를 의미합니다.

Uint8Array의 경우 부호 없이 8bit의 숫자들을 담을 수 있으므로 2^8인 0 ~ 255까지 담을 수 있습니다.

 

Int8Array / Uint8Array / Uint8ClampedArray

Int16Array /  Uint16Array

Int32Array / Uint32Array

Float32Array / Float64Array 

 

일반적으로 웹 브라우저에서 머신 러닝 하는데 Float32Array가 주로 사용됨. Float64Array는 물론 더 유효 소수점 자리수가 넓어지지만 메모리를 더 많이 요구하기 때문에...

 

범위를 넘어가는 숫자는 최소값 혹은 최대값으로 판단됩니다. 이는 형식화 배열의 타입마다 다릅니다.

 

Uint8Array는 최대값 초과시 최소값인 0 할당합니다.

const arr = new Uint8Array(4) // [0, 0, 0, 0]으 초기화. 8bit 4개니 32bit=4byte 할당
arr[0] = 256 // Uint8Array의 최대값인 255 초과하는 값 할당
console.log(arr) // [0, 0, 0, 0]
console.log(arr.length) // 4 byte이므로 4

 

Uint8ClampedArray는 최대값 초과시 최대값을 할당합니다. 애초에 이름이 clamped니까요.

const arr = new Uint8ClampedArray(4) // [0, 0, 0, 0]으 초기화
arr[0] = 256 // Uint8ClampedArray 최대값인 255 초과하는 값 할당
console.log(arr) // [255, 0, 0, 0] 최대값이 255로 할당

 

이러한 형식화 배열을 일반 배열처럼 사용하고 접근할 수 있으나 실제 데이터는 ArrayBuffer에 저장됩니다.

 

ArrayBuffer는 또 뭐냐

ArrayBuffer는 연속된 이진 데이터입니다. 메모리에 고정 길이를 가진 buffer를 확보합니다. 

C에서와 같이 메모리에 직접 접근하는 방식을 사용합니다.

// buffer
const buffer = new ArrayBuffer(16); // arraybuffer 초기화. 16 byte임.
console.log(buffer); // <00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00> 00이 16개!
console.log(buffer.byteLength); // 버퍼의 length : 16. 16바이트임

// console.log([...buffer]); Error! 배열화 불가능.
// console.log(buffer[0]); undefined 임의 접근이나 수정 불가

 

다만 메모리 내에서 영역을 차지하지만 buffer를 조작하는 메서드도 없으며, 바이트 열의 크기나 반환해야 하는 타입을 모릅니다.

즉, ArrayBuffer는 액세스되거나, 임의로 수정될 수 없습니다.

(This chunk of raw binary data, cannot be accessed or modified on its own. - MDN)

 

ArrayBuffer를 조작하고 활용하려면 ArrayBuffer를 typed Array로 감싸야합니다. ArrayBuffer로 할당된 buffer를 조작하려면 typed Arrray로 감싸야하기 때문에 typed array를 ArrayBuffer의 뷰라고 부릅니다.

const arr = new Uint8Array(new ArrayBuffer(4)) // 4 Byte할당
console.log(arr) // Uint8Array(4) [ 0, 0, 0, 0 ]

 

참고로 ArrayBuffer에 저장된 바이너리 데이터에 접근하고 수정할 수 있게 해주는 것을 View로 부릅니다.

뷰는 2가지가 존재하는데, Typed Array, DataView가 있습니다.

요약하자면, ArrayBuffer로 byte 단위로 메모리 공간 할당하고 TypedArray나 DataView로 활용한다.

ArrayBuffer로 byte단위로 메모리 공간 할당하고 TypedArray나 DataView로 활용한다.

 

 

ArrayBuffer가 연속된 이진 데이터임을 고려하면 아래와 같은 동작을 이해할 수 있다. 

const buf = new ArrayBuffer(4)
const unit8 = new Uint8Array(buf)
const unit16 = new Uint16Array(buf)

console.log(unit8) // Uint8Array(4) [ 0, 0, 0, 0 ]
console.log(unit16) // Uint16Array(2) [ 0, 0 ]

unit8[0] = 1
unit8[1] = 1

console.log(unit8) // Uint8Array(4) [ 1, 1, 0, 0 ]
console.log(unit16) // Uint16Array(2) [ 257, 0 ]

같은 ArrayBuffer를 공유하고 있으므로 uint8에서의 조작이 unit16에서도 영향을 미쳤다.

uint8array의 입장에서는 0000000100000001 이므로 257이 되는 것이다.

 

 

 

ArrayBuffer의 또 다른 View인 DataView

DataView는 binary를 담고 있는 ArrayBuffer에서 여러 개의 숫자 형식을 읽고 사용할 수 있는 저 수준의 인터페이스를 제공한다. 즉! TypedArray에서는 특정 타입의 데이터만 저장할 수 있었지만 DataView에서는 여러 형식을 저장할 수 있다는 말이다.

 

하지만 이런 점 때문에 DataView는 데이터 형식을 관리하지 않기 때문에 액세스할 때마다 어떤 데이터가 저장되어 있는지 확인해야 한다.

구체적으로 이게 무슨 말인지 코드로 살펴보면 다음과 같다. 살펴볼 수 있듯, TypedArray처럼 [0]와 같이 간편하게 가져올 수가 없다..

const buf = new ArrayBuffer(4)
const dv = new DataView(buf)

// 주어진 buffer에 8bit int형 데이터를 넣어라
// setInt8(byteOffset: number, value: number): void
dv.setInt8(0, 2)

// 0번째 인덱스에 있는 8bit int형 데이터를 가져와.
console.log(dv.getInt8(0)) // 2

 

DataView의 Endian에 대해서

엔디안은 Byte가 저장된 순서를 정의한다. 하위 바이트부터 기록하는 little-endian, 상위 바이트부터 기록하는 big-endian 방식이 존재한다. DataView에서 endian의 default는 big-endian 방식이다.

 

big-endian은 사람이 사용하듯, 큰 것부터 먼저 기록하고 little-endian은 작은 순으로 기록하는 것이다.

이것이 직관적으로 와닺지 않을텐데 그림을 참고해보면 어느 정도 감을 잡을 수 있다. 

https://en.wikipedia.org/wiki/Endianness

 

const buf = new ArrayBuffer(4) // 4 bytes

// buf의 첫번째에 127을 할당하라. => 0000 0000 0111 1111
new DataView(buf).setInt16(0, 127, true) // little endian. 

// 그러면 little endian에 의해 0111 1111이 앞에, 0000 0000이 뒤에 할당되어
// [0111 1111 0000 0000]이 된다.
// 결과적으로 [0111 1111 / 0000 0000 / 0000 0000 / 0000 0000]
// 8비트로 쪼개서 4조각을 내서 출력해보면 다음과 같다.
console.log(new Uint8Array(buf)) // [127, 0, 0, 0]

 

const buf = new ArrayBuffer(4) // 4 bytes

// buf의 첫번째에 127을 할당하라. => 0000 0000 0111 1111
new DataView(buf).setInt16(0, 127, false) // big endian. 

// 그러면 big endian에 의해 0000 0000이 앞에, 0111 1111이 뒤에 할당되어
// [0000 0000 0111 1111]이 된다.
// 결과적으로 [0000 0000 / 0111 1111 / 0000 0000 / 0000 0000]
// 8비트로 쪼개서 4조각을 내서 출력해보면 다음과 같다.
console.log(new Uint8Array(buf)) // [0, 127, 0, 0]

 

 

 

* 왜 일반 배열을 사용하지 않고 TypedArray와 ArrayBuffer를 조합해서 사용하는가?

 

ArrayBuffer와 TypedArray는 중복되는 데이터 복사를 피함으로서 메모리를 효율적으로 관리할 수 있다.

앞서 ArrayBuffer를 공유하고 있는 TypedArray가 어느 한 쪽을 변형하자 다른 한 쪽에서도 변형이 되는 것은 이 때문이다.

 

 

* 주의. javascript에서 제공하는 ArrayBuffer과 Node에서 제공해준 Buffer 클래스간 변환할 때 신경 좀 쓸 것

 

node의 공식 문서에 따르면 노드 클래스 Buffer는 Uint8Array의 하위클래스입니다.

(The Buffer class is a subclass of JavaScript's Uint8Array class.)

 

node 환경에서 Buffer를 직접 만들고 싶다면 아래와 같은 메서드들이 있습니다.

구체적으로 convert하는 방법은 아래 so 글을 참고 합시다.

https://stackoverflow.com/questions/8609289/convert-a-binary-nodejs-buffer-to-javascript-arraybuffer

 

 

ref)

developer.mozilla.org/ko/docs/Web/JavaScript/Typed_arrays

developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/TypedArray

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer

blog.bitsrc.io/javascript-typed-arrays-ccfa5ae8838d

mohwa.github.io/blog/javascript/2015/08/31/binary-inJS/

libsora.so/posts/i-hate-unsigned/

 

 


darren, dev blog
블로그 이미지 DarrenKwonDev 님의 블로그
VISITOR 오늘 / 전체