자바스크립트 Array는 객체다? 희소 배열이 성능과 일관성에 미치는 영향

4 min read
Cover Image for 자바스크립트 Array는 객체다? 희소 배열이 성능과 일관성에 미치는 영향

이론

  • Array의 타입을 찍어보면 object가 출력됩니다.

      const array = [1, 2, 3];  
      console.log(typeof array); // object
    
  • 그렇기 때문에 -논리적으로는 허점이 있지만,- 인덱스 접근으로 데이터 추가가 가능하죠.

      array[6] = '7';
    
  • 그러나 이런 방식은 성능 저하와 메서드의 비일관적 처리를 유발할 수 있어 지양하는 것이 좋습니다.

  • 그 이유를 지금부터 알아보도록 하겠습니다.

배경 설명

  • Javascript에서 Array는 사실 Dictionary(Object)입니다.

  • v8에서는 배열 구현 시 객체와 마찬가지로 Hidden Class를 사용합니다.

예시: 인덱스로 요소 추가

const array = [1, 2, 3];  
array[6] = '7';  
console.log(array); // [ 1, 2, 3, <3 empty items>, '7' ]
  • 이 방식은 동작하지만, 성능과 일관성 측면에서 문제가 될 수 있습니다.

권장

  • 에러가 나지 않는다면 활용해도 될까요?

  • 결론: 사용을 지양해야 합니다. 비일반적인 스타일이며 성능 저하를 유발할 수 있습니다.


실험

퍼포먼스의 저하

  • v8 블로(https://v8.dev/blog/elements-kinds)의 이미지 설명에 따르면, 왼쪽에서 오른쪽, 위에서 아래로 갈수록 속도가 느려질 수 있습니다.

    • Packed > Holey

    • SMI(정수) > DOUBLE(실수) > 나머지

  • v8은 내부적으로 Array를 최적화합니다.

    • 정수로 구성된 경우 SMI_ELEMENTS로 최적화하지만,

    • 크기나 다른 고려사항에 따라 다른 ELEMENTS로 전환될 수 있습니다.

Elements 트랜지션 확인 하는 법

// 플래그 사용 예시 (터미널에서 실행)  
// node --trace-elements-transitions {filename}.js  

const packedArray = [1, 2, 3, 4, 5];  
packedArray[10] = 'test';  

// 출력 예시 (플래그 사용 시)  
// elements transition [PACKED_SMI_ELEMENTS -> HOLEY_ELEMENTS] in (...생략)

Packed vs Holey 성능 비교

// Packed Element 배열 생성  
function createPackedArray(size) {  
  const array = new Array(size);  
  for (let i = 0; i < size; i++) {  
    array[i] = i;  
  }  
  return array;  
}  

// Holey Element 배열 생성  
function createHoleyArray(size) {  
  const array = new Array(size);  
  for (let i = 0; i < size; i++) {  
    if (i % 2 === 0) {  
      array[i] = i;  
    }  
  }  
  return array;  
}  

// 배열 요소에 접근하여 시간을 측정하는 함수  
function measureAccessTime(array) {  
  const start = process.hrtime();  
  for (let i = 0; i < array.length; i++) {  
    const element = array[i];  
  }  
  const end = process.hrtime(start);  
  return end[0] * 1000 + end[1] / 1000000; // 시간(ms)으로 변환  
}  

// 성능 비교  
const size = 999999; // 배열 크기  
const packedArray = createPackedArray(size);  
const holeyArray = createHoleyArray(size);  

console.log("Packed Array Access Time:", measureAccessTime(packedArray), "ms"); // 예: 1.65 ms  
console.log("Holey Array Access Time:", measureAccessTime(holeyArray), "ms");  // 예: 1.83825 ms
  • 결과: 전체가 채워진 Packed Array 대비 Holey Array는 약 0.2ms 더 오래 걸립니다. 처리 로직이 복잡하거나 배열이 커질수록 차이는 더 커질 수 있습니다.

Packed SMI vs Packed Elements 성능 비교

// PACKED_SMI_ELEMENTS 배열 생성  
function createPackedSmiArray(size) {  
  const array = new Array(size);  
  for (let i = 0; i < size; i++) {  
    array[i] = i; // 작은 정수 값(SMI)  
  }  
  return array;  
}  

// PACKED_ELEMENTS 배열 생성  
function createPackedElementsArray(size) {  
  const array = new Array(size);  
  for (let i = 0; i < size; i++) {  
    array[i] = { value: i }; // 객체 저장  
  }  
  return array;  
}  

// 배열 요소에 접근하여 시간을 측정하는 함수  
function measureAccessTime(array) {  
  const start = process.hrtime();  
  for (let i = 0; i < array.length; i++) {  
    const element = array[i];  
  }  
  const end = process.hrtime(start);  
  return end[0] * 1000 + end[1] / 1000000; // 시간(ms)으로 변환  
}  

// 성능 비교  
const size = 1000000; // 배열 크기  
const packedSmiArray = createPackedSmiArray(size);  
const packedElementsArray = createPackedElementsArray(size);  

console.log("PACKED_SMI_ELEMENTS Access Time:", measureAccessTime(packedSmiArray), "ms");   // 예: 1.363167 ms  
console.log("PACKED_ELEMENTS Access Time:", measureAccessTime(packedElementsArray), "ms"); // 예: 1.604834 ms

결과: 작은 정수로 채운 Packed SMI Array가 객체가 저장된 Packed Array보다 약 0.24ms 빠릅니다.

희소 배열에 대한 메서드의 비일관적인 처리

  • Holey Array에서 empty에 대한 처리는 메서드마다 일관적이지 않습니다. 최신 메서드는 undefined로 취급하는 반면, 오래된 메서드는 건너뜁니다.
const sparseArray = ['apple', 'banana', 'orange'];  
sparseArray[10] = 'grapes';  

let olderMethod = [];  
sparseArray.forEach((element, index) => {  
  olderMethod.push(element);  
});  

const newerMethodArray = Array.from(sparseArray);  

console.log('Older Method', olderMethod);  
console.log('Newer Method', newerMethodArray);  

/*  
Older Method  
[ 'apple', 'banana', 'orange', 'grapes' ]  

Newer Method  
[  
  'apple',  
  'banana',  
  'orange', undefined,  
  undefined, undefined,  
  undefined, undefined,  
  undefined, undefined,  
  'grapes'  
]  
*/
  • 이런 비일관성은 사이드 이펙트를 일으키고 디버깅을 어렵게 만들 수 있습니다.

요약

  • 자바스크립트에서 Array는 객체이지만, Key를 통한 요소 추가 등 객체처럼 다루는 것은 지양해야 합니다.

  • 희소 배열이 되면 성능이 저하되고, empty 요소 처리의 비일관성으로 인해 예측 불가능한 동작이 발생할 수 있습니다.

실무 권장 사항

  • 가능한 정수로 구성된 숫자 배열(SMI)을 사용하기

  • 가능한 꽉 찬(Packed) 배열을 유지하기

  • 희소 배열을 만들지 않도록 인덱스 점프나 중간 비우기를 피하기

  • 메서드별 empty 처리 차이를 고려해 API 선택 신중히 하기