본문 바로가기
Programming/Javascript

[Javascript] V8 Engine이 string과 number 값을 다루는 방법

by SpiralMoon 2024. 6. 28.
반응형

V8 Engine이 string과 number 값을 다루는 방법

V8 Engine이 자바스크립트의 문자열, 숫자 데이터의 메모리 최적화를 수행하는 방법에 대해 알아보자

사전 지식

V8 Engine

 

Node.js — The V8 JavaScript Engine

Node.js® is a JavaScript runtime built on Chrome's V8 JavaScript engine.

nodejs.org


Constant pool를 통한 메모리 절약

V8 Engine에서는 JS 코드의 런타임 최적화 및 빠른 실행을 위해 다양한 최적화 기법을 사용한다. 그 기능중 하나인 constant pool은 런타임 동안 변하지 않는 상수(bytecode에서 상수로 참조되는 heap 객체)들의 집합을 저장하는 구조이다.

 

V8 Engine은 contant pool을 통해 integer, string값에 대해 중복 제거를 수행함으로서 메모리 사용량을 줄이고 성능을 향상시킨다.


Constant pool을 간단하게 확인하는 방법

Node.js 환경에서는 node 명령어로 js 코드를 실행할 때 bytecode에 대한 출력을 확인할 수 있는 옵션을 제공한다.

 

node --print-bytecode --print-bytecode-filter='test*' index.js > bytecode.txt

 

--print-bytecode : 파일에 대한 bytecode 출력

--print-bytecode-filter : bytecode를 출력할 대상 함수명

 

즉, 위의 명령어는 index.js를 실행하고 index.js에서 이름이 test로 시작하는 함수에 대한 bytecode를 bytecode.txt에 출력하는 동작이 된다.


string

V8 Engine은 문자열에 대한 중복 할당을 최적화한다.

 

// string.js

function stringTest1() {
   let str1 = 'this is string';
   let str2 = 'this is string';
}

function stringTest2() {
    let str1 = 'this is string';
    let str2 = new String('this is string');
}

stringTest1();
stringTest2();

 

위 소스코드에서 두 함수는 동일한 값을 가지는 문자열을 두 번씩 선언하고 있다.

 

node --print-bytecode --print-bytecode-filter='stringTest*' string.js > string_code.txt

 

내부 동작을 확인하기 위해 명령어를 통해 bytecode를 출력한다.

 

[generated bytecode for function: stringTest1 (0x017ba0749479 <SharedFunctionInfo stringTest1>)]
Bytecode length: 8
Parameter count 1
Register count 2
Frame size 16
Bytecode age: 0
   40 S> 0000017BA074A2BE @    0 : 13 00             LdaConstant [0]
         0000017BA074A2C0 @    2 : c4                Star0
   73 S> 0000017BA074A2C1 @    3 : 13 00             LdaConstant [0]
         0000017BA074A2C3 @    5 : c3                Star1
         0000017BA074A2C4 @    6 : 0e                LdaUndefined
   92 S> 0000017BA074A2C5 @    7 : a9                Return
Constant pool (size = 1)
0000017BA074A271: [FixedArray] in OldSpace
 - map: 0x03f8a83c0211 <Map(FIXED_ARRAY_TYPE)>
 - length: 1
           0: 0x017ba074a209 <String[14]: #this is string>
Handler Table (size = 0)
Source Position Table (size = 9)
0x017ba074a2c9 <ByteArray[9]>

[generated bytecode for function: stringTest2 (0x017ba07494c9 <SharedFunctionInfo stringTest2>)]
Bytecode length: 20
Parameter count 1
Register count 4
Frame size 32
Bytecode age: 0
  138 S> 0000017BA074A386 @    0 : 13 00             LdaConstant [0]
         0000017BA074A388 @    2 : c4                Star0
  172 S> 0000017BA074A389 @    3 : 21 01 00          LdaGlobal [1], [0]
         0000017BA074A38C @    6 : c2                Star2
         0000017BA074A38D @    7 : 13 00             LdaConstant [0]
         0000017BA074A38F @    9 : c1                Star3
         0000017BA074A390 @   10 : 0b f8             Ldar r2
  172 E> 0000017BA074A392 @   12 : 69 f8 f7 01 02    Construct r2, r3-r3, [2]
         0000017BA074A397 @   17 : c3                Star1
         0000017BA074A398 @   18 : 0e                LdaUndefined
  203 S> 0000017BA074A399 @   19 : a9                Return
Constant pool (size = 2)
0000017BA074A331: [FixedArray] in OldSpace
 - map: 0x03f8a83c0211 <Map(FIXED_ARRAY_TYPE)>
 - length: 2
           0: 0x017ba074a209 <String[14]: #this is string>
           1: 0x03f8a83c73b9 <String[6]: #String>
Handler Table (size = 0)
Source Position Table (size = 12)
0x017ba074a3a1 <ByteArray[12]>

 

bytecode를 살펴보면 두 함수에서 모두 Constant pool에 this is string이라는 문자열을 1회씩 할당한 것을 볼 수 있다.

해당 문자열은 런타임 동안 변경되지 않는 리터럴 상수이므로 constant pool에 할당되었으며, 동일한 값을 갖기 때문에 중복 선언되지 않고 constant pool에 1개만 할당되었다.

 

stringTest1()의 메모리 구조

stringTest1()에서는 변수 str1, str2는 동일한 constant pool의 메모리 공간을 참조하게 된다.

stringTest2()의 메모리 구조

stringTest2()에서는 객체 str2의 heap 영역과 변수 str1에서 constant pool의 메모리 공간을 참조하게 된다.

 

이러한 현상은 메모리 스냅샷을 통해서도 쉽게 확인할 수 있다.

 

// case 1
const arr = [];

for (let i = 0; i < 10; i++) {
    arr[i] = 'this is string';
}


// case 2
const arr = [];

for (let i = 0; i < 10; i++) {
    arr[i] = new String('this is string');
}

 

동일한 문자열을 10번씩 선언하는 두 케이스를 작성하고 케이스별로 메모리 스냅샷을 기록해보았다.

 

case 1의 snapshot

case 1에서는 arr의 모든 요소가 constant pool에 할당된 문자열의 메모리(@190309)를 동일하게 참조하는 것을 볼 수 있다.

 

case 2의 snapshot

case 2에서는 arr의 모든 String 객체들은 heap 메모리에 각각 할당되지만 내부적으로는 constant pool에 할당된 문자열의 메모리(@190613)를 참조하는 것을 볼 수 있다.


number

V8 Engine은 숫자에 대해서도 중복 할당을 최적화한다. 다만, 숫자 데이터는 SMI 여부에 따라 두 종류로 나뉘며 최적화 방법이 다르다.


SMI

 

  • SMI(Small Integer)에 해당되는 정수 데이터는 Stack pointer에 저장된다.
  • SMI에 해당되지 않는 나머지 모든 숫자 데이터는 Heap에 저장된다.

 

SMI 여부에 따라 나누어 최적화하는 이유는 SMI 범위의 정수는 사용빈도가 너무 높기 때문에 동일한 방식으로 관리가 이루어진다면 값이 변경되거나 계산이 수행될 때마다 매번 새로운 개체를 할당해야하는 것이 큰 부담이기 때문이다.

 

SMI는 32bit 환경에서 31bit 부호있는 정수값을 의미하고, 64bit 환경에서는 32bit 부호있는 정수값을 의미한다.

 

 

32bit SMI
64bit SMI

위 두 사진은 SMI의 메모리 구조를 나타내며 MSB는 +,- 부호를 나타내고 LSB는 SMI를 나타내는 태깅값으로 0고정됨을 알 수 있다.


// number.js

function numberTest1() {
    let num1 = 111111111111111111;
    let num2 = 111111111111111111;
}

function numberTest2() {
    let num1 = 12345;
    let num2 = 12345;
}

function numberTest3() {
    let num1 = 1.1;
    let num2 = 1.1;
}

numberTest1();
numberTest2();
numberTest3();

 

위 소스코드에서 세 함수는 동일한 값을 가지는 숫자를 두 번씩 선언하고 있다.

 

 node --print-bytecode --print-bytecode-filter='numberTest*' number.js > number_code.txt

 

내부 동작을 확인하기 위해 명령어를 통해 bytecode를 출력한다.

 

[generated bytecode for function: numberTest1 (0x0165bca494a1 <SharedFunctionInfo numberTest1>)]
Bytecode length: 8
Parameter count 1
Register count 2
Frame size 16
Bytecode age: 0
   41 S> 00000165BCA4A356 @    0 : 13 00             LdaConstant [0]
         00000165BCA4A358 @    2 : c4                Star0
   77 S> 00000165BCA4A359 @    3 : 13 00             LdaConstant [0]
         00000165BCA4A35B @    5 : c3                Star1
         00000165BCA4A35C @    6 : 0e                LdaUndefined
   98 S> 00000165BCA4A35D @    7 : a9                Return
Constant pool (size = 1)
00000165BCA4A2F9: [FixedArray] in OldSpace
 - map: 0x027f2ef00211 <Map(FIXED_ARRAY_TYPE)>
 - length: 1
           0: 0x0165bca4a311 <HeapNumber 1.11111e+17>
Handler Table (size = 0)
Source Position Table (size = 9)
0x0165bca4a361 <ByteArray[9]>

[generated bytecode for function: numberTest2 (0x0165bca494f1 <SharedFunctionInfo numberTest2>)]
Bytecode length: 12
Parameter count 1
Register count 2
Frame size 16
Bytecode age: 0
  144 S> 00000165BCA4A3FE @    0 : 00 0d 39 30       LdaSmi.Wide [12345]
         00000165BCA4A402 @    4 : c4                Star0
  167 S> 00000165BCA4A403 @    5 : 00 0d 39 30       LdaSmi.Wide [12345]
         00000165BCA4A407 @    9 : c3                Star1
         00000165BCA4A408 @   10 : 0e                LdaUndefined
  175 S> 00000165BCA4A409 @   11 : a9                Return
Constant pool (size = 0)
Handler Table (size = 0)
Source Position Table (size = 9)
0x0165bca4a411 <ByteArray[9]>

[generated bytecode for function: numberTest3 (0x0165bca49541 <SharedFunctionInfo numberTest3>)]
Bytecode length: 8
Parameter count 1
Register count 2
Frame size 16
Bytecode age: 0
  221 S> 00000165BCA4A4D6 @    0 : 13 00             LdaConstant [0]
         00000165BCA4A4D8 @    2 : c4                Star0
  242 S> 00000165BCA4A4D9 @    3 : 13 00             LdaConstant [0]
         00000165BCA4A4DB @    5 : c3                Star1
         00000165BCA4A4DC @    6 : 0e                LdaUndefined
  248 S> 00000165BCA4A4DD @    7 : a9                Return
Constant pool (size = 1)
00000165BCA4A479: [FixedArray] in OldSpace
 - map: 0x027f2ef00211 <Map(FIXED_ARRAY_TYPE)>
 - length: 1
           0: 0x0165bca4a491 <HeapNumber 1.1>
Handler Table (size = 0)
Source Position Table (size = 9)
0x0165bca4a4e1 <ByteArray[9]>

 

bytecode를 살펴보면 numberTest1()과 numberTest3()에서는 선언된 값이 SMI에 해당하지 않으므로 Constant pool에 HeapNumber를 1회씩 할당한 것을 볼 수 있다.

해당 숫자는 런타임 동안 변경되지 않는 리터럴 상수이므로 constant pool에 할당되었으며, 동일한 값을 갖기 때문에 중복 선언되지 않고 constant pool에 1개만 할당되었다.

 

그러나 numberTest2()에서는 선언된 값이 SMI에 해당하므로 stack pointer에 저장된다. 따라서 Heap과 constant pool에 값이 할당되지 않았다.


참조

 

Chromium Edge의 메모리 힙 스냅샷 분석을 위한 V8 엔진의 이해 2.

본문에서는 개체의 히든 클래스를 동일하게 유지하기 위해 웹 클라이언트 개발자가 알고 있어야 할 사항들을 메모리 힙 스냅샷의 관점에서 조금 더 구체적인 사례 위주로 살펴본 다음, 한 단계

www.egocube.pe.kr

 

SMI
 
반응형

댓글