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가 연속된 이진 데이터임을 고려하면 아래와 같은 동작을 이해할 수 있다.
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은 작은 순으로 기록하는 것이다.
이것이 직관적으로 와닺지 않을텐데 그림을 참고해보면 어느 정도 감을 잡을 수 있다.
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/
'Programming Language > 🟨 Javascript' 카테고리의 다른 글
DOM 구조화 : Range, Parsing, AST 그리고 에디터 (0) | 2021.11.01 |
---|---|
예시로 공부하는 javascript 정규식 (0) | 2021.05.27 |
js의 Number, BigInt 타입과 정밀한 숫자 계산에 대하여 (0) | 2021.04.20 |
throttle과 debounce를 통해 중복된 요청을 줄여보자 (0) | 2021.03.16 |
Date 타입과 ISO, UNIX (0) | 2021.02.17 |