일반적으로 배열은 어떻게 동작하는지 다른 언어를 통해 익숙히 알고 있다.
배열에서 필요한 만큼 메모리 공간을 확보한 다음 데이터를 저장한다. 특히, 연속적인 데이터 접근에 굉장히 용이하다.
그러나 JS의 배열은 뭔가 좀 특이하다.
같은 자료형이 아닌데도 배열이 잘 유지된다. 심지어 Object를 넣어도 잘 동작한다.
배열의 메모리 저장 방식
사실 JS는 배열을 다른언어에서 배열을 사용하여 메모리에 순차적으로 저장하는 방식이 아닌 Linked List의 방식( sparse distributed memory )과 동일하게 메모리에 저장한다. 동작하는 방식은 또 약간 다른가 보다....
무튼, 성능 상 바로 접근해서 사용하는 배열을 능가할 수는 없다. 다만 요소의 삽입 및 삭제는 배열보다 빠르다. 웹사이트의 빈번한 요소의 변화를 처리하는데는 이게 더 좋지않을까 싶기도 하다.
JS 배열의 성능 개선 #1
그래도 일단 JS 배열에 대해 조금 더 깊게 알아보고자 한다.
시간이 흐름에 따라 램도 증가, V8 엔진의 성능 개선 등의 성능 개선도 JS 에서 이뤄졌다.
일반적인 배열의 사용 방식의 성능도 극복하고자 했는지 JS 에서 만든 배열에 모든 요소가 같은 타입을 가진경우 연속적으로 메모리를 할당한다. C 컴파일러와 동일한 배열 연산을 수행한다.
하지만 모든 요소가 같은 타입의 배열에서 다른 타입의 요소가 들어오면 JIT(Just In Time) 컴파일러는 전체 배열의 구조를 해제하고 비연속 메모리를 할당한다. JS에서 다른 언어처럼 배열을 쓰고 싶으면 한가지 타입으로 통합을 해야 한다.
JS 배열의 성능 개선 #2
ES6에서 조금 더 발전했다. Typed Array 라는 것을 추가 했는데 이게 좀 특이하다.
ArrayBuffer 을 사용해서 인접한 메모리 블록을 만든다. 다만, 메모리를 직접 다루는 행위는 사용자에게 Low Level 이고 너무 복잡하다고 생각했는지 View 를 통해서만 접근하도록 만들었다.
const buffer = new ArrayBuffer(8);
const view = new Int32Array(buffer);
view[0] = 100;
위의 코드는 8byte의 공간을 메모리에 할당한다. 그리고 그 메모리에 접근은 View를 통해서 접근하도록 한다.
Int32Array는 4byte의 공간을 차지하는 Integer을 메모리에 차례대로 저장하겠다는것이다. 총 8byte의 공간의 첫부분인 4byte 공간에 100을 할당했다. 나머지는 빈 상태를 유지한다.
자세한 사항은 아래 웹 페이지를 참조
https://developer.mozilla.org/ko/docs/Web/JavaScript/Typed_arrays
JavaScript 형식화 배열 - JavaScript | MDN
JavaScript 형식화 배열(typed array)은 배열같은 객체이고 원시(raw) 이진 데이터에 액세스하기 위한 메커니즘을 제공합니다. 이미 아시다시피, Array 객체는 동적으로 늘었다 줄고 어떤 JavaScript 값이든
developer.mozilla.org
Unsigned와 Signed 값의 차이?
간단하게 1byte에 숫자를 할당할 수있다고 가정하면 총 8bit, 8칸이 생성된다.
Unsigned, 부호를 표시할 필요가 없어서 양수만 표기하면 된다. 8bit 공간을 꽉 채워본다. 0 ~ 2^8 까지 표현할 수 있다.
Signed, 부호를 표시해야 한다. 첫번재 공간을 부호 표기용으로 할당한다. 남은 7bit 공간에 수를 표현한다. -2^7 ~ 2^7 까지 표현 할 수 있다. 조금 더 자세한 것은 "보수" 로 검색해서 찾아보시길...
위의 페이지에 View 테이블을 보면 Uint 에는 unsigned 값을, Int 에는 signed 값을 읽을 수 있도록 만들었다고 한다.
Typed Array (형식화 배열)
대략적으로 Typed Array를 정리해보자면
Typed Array : 원시 이진 데이터에 접근하기 위한 메커니즘을 제공
Typed Array 의 .isArray() 메서드는 false 를 반환함. 배열이 아니라서 push, pop 의 함수를 지원하지 않는다.
버퍼 및 뷰
Typed Array 은 구현을 버퍼와 뷰로 나눈다
버퍼 : 데이터 덩어리를 나타내는 객체 (ArrayBuffer 객체에 의해 구현) 버퍼의 데이터에 접근하기 위해서 뷰가 필요하다.
뷰 : 문맥 (데이터 타입, 오프셋 및 요소 수)을 제공해 데이터를 실제 형식화 배열로 바꾼다.
일반적인 배열처럼 사용하고자 하면 버퍼와 뷰가 필요하다. 실제로 사용하고자 하면 짧은 코드를 몇 줄 더 써야 한다.
버퍼와 뷰의 예를 봅시다.
- ArrayBuffer
일반 고정길이 이진 데이터 버퍼를 나타내는데 사용
직접 조작 불가
형식화 배열 뷰 또는 특정형식으로 버퍼를 나타내는 DataView 를 만들어 버퍼의 컨텐츠를 읽고 씀
( 위 제공된 페이지의 테이블을 참고하시길 )
- DataView
버퍼의 임의 데이터를 읽고 쓰기 위해 Getter/Setter API를 제공하는 Low-level 인터페이스
서로 달느 유형의 데이터 처리에 유용
바이트 순서를 제어할 수 있음
기본적으로 Big-edian 방식, getter/setter 메서드로 little-edian으로 설정 가능
코드 예제를 봅시다,
let buffer = new ArrayBuffer(16); // 16바이트 고정길이 버퍼 생성
if (buffer.byteLength === 16) {
console.log("Yes. it's 16bytes.");
} else {
console.log("Nope. it's the wrong size.");
}
생성한 버퍼로 작업을 하기 위해서 뷰를 만들어야 함.
32bit signed 정수를 버퍼로 다룰거임, 4byte씩 먹는거임
let int32View = new Int32Array(buffer); // View 까지 생성하면 일반 배열처럼 필드값에 접근 가능
for (let i = 0; i < int32View.length; i++) {
int32View[i] = i * 2; // 16바이트 모두 사용, 한 요소당 4바이트씩 처묵
};
// int32View : [ 0, 2, 4, 6 ]
Ex) 같은 데이터에 여러 뷰 적용
좀 희한한 일이지만 재미있음 ㅎㅎ, 위의 코드를 계속해서 쓸거임
let int16View = new Int16Array(buffer); // 2 byte 사용
for (let i = 0; i < int16View.length; i++) {
console.log("Entry " + i + " : " + int16View[i]);
}; // int32View : [0, 0, 2, 0, 4, 0, 6, 0] // 4 byte단위를 2 byte 단위로 쪼갬
int16View[0] = 32;
console.log("Entry 0 in the 32-bit array is now" + int32View[0]); // int32View[0] :32
버퍼는 하나로 일정한데 서로 다른 뷰들을 만들어 사용할 수있음. 근데 실제로 쓸 일이 있을까??....
Ex) 복잡한 구조체와 작업하기
단일 버퍼를 서로 다른형인 여러 뷰와 결합, 여러 데이터 형을 포함하는 데이터 객체와도 상호 작용이 가능하답니다.
WebGL, js-ctypes 이것들 쓰는데 도움이 된답니다... 근데 그건 잘 모름
일단 C 구조체를 만들어봅시다.
struct someStruct {
unsigned long id; // 4 byte
char username[16]; // 16 byte
float amountDue; // 4 byte
};
위의 형식대로 버퍼에 접근이 가능하다!
let buffer = new ArrayBuffer(24);
// 버퍼 내의 데이터를 차례대로 읽음
const idView = new Uint32Array(buffer, 0, 1);
const usernameView = new Uint8Array(buffer, 4, 16);
const amountDueView = new Float32Array(buffer, 20, 1);
Type Array를 Array로 반환하기
Typed Array을 처리한 뒤 Array 프로토타입 도움을 받기 위해 배열로 다시 변환하는게 유용할 때가 있다.
const typedArray = new Uint8Array([1, 2, 3, 4]),
const normalArray = Array.prototype.slice.call(typedArray);
normalArray.length === 4;
normalArray.constructor === Array;
이제 일반 배열처럼 사용 할 수 있다 ㅎㅎ
Typed Array 는 굉장히 효율적인 시스템이랍니다. 일반 배열로는 바이너리 데이터를 효율적으로 처리하기 어렵기 때문에 WebGL에서 잘 쓸 수 있습니다. 또한 Web Worker 간 메모리를 공유할 수 있는 SharedArrayBuffer도 있으니 찾아봐요!
JS 배열 vs Typed Array 비교
같은 타입의 데이터가 들어가면 두개의 성능은 비슷함
동일하지 않은 타입에서 삽입은 Typed Array가 훠어얼씬 빠르다.
Int8Array, Uint8Array, Uint8ClampedArray, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array와 같은 다양한 뷰를 제공함. 위의 뷰를 통해 네이티브 바이트 순서로 데이터에 접근 할 수있다.
JS 가 사용하는 메모리를 더 효율적으로 사용해 봅시다.
본 내용은 https://evan-moon.github.io/2019/06/15/diving-into-js-array/ 을 보고 정리했습니다.
'Javascript' 카테고리의 다른 글
String 값은 어디에 저장이 될까? (0) | 2022.12.01 |
---|---|
변수의 유효범위(Scope)와 클로저, 가비지 컬렉션 (1) | 2022.11.30 |
함수 정리 (0) | 2022.11.29 |
Closer (0) | 2021.11.14 |
버블링과 캡처링 (0) | 2021.11.12 |
댓글