Pointer Tagging

Các số nguyên nhỏ thường xuyên được sử dụng và sẽ rất lãng phí nếu như lưu mỗi số nguyên là một object. Thay vào đó, dựa trên việc dữ liệu trong kiến trúc 32-bit và 64-bit thường được sắp xếp theo cụm 4 byte nên bit cuối cùng luôn là 0, V8 lưu các số nguyên nhỏ trực tiếp ở trên heap mà không cần bọc bên trong một object.

Cụ thể hơn, V8 sẽ sử dụng 2 bit cuối cùng để phân biệt giữa con trỏ trỏ đến object (mọi thứ trong JavaScript đều là object) và số nguyên nhỏ. Cơ chế này được gọi là pointer tagging.

Đối với kiến trúc 32-bit:

            |----- 32 bits -----|
Pointer:    |_____address_____w1|
Smi:        |___int31_value____0|

Có thể thấy:

  • Con trỏ sẽ có bit nhỏ nhất là 1 do được cộng thêm 1. Để lấy ra giá trị thực của con trỏ thì ta cần trừ đi 1.
  • Số nguyên nhỏ sẽ có bit nhỏ nhất là 0 do bị dịch trái sang 1 bit. Để lấy ra giá trị thực của số nguyên nhỏ thì ta cần chia 2.

Bit w được dùng để đánh dấu con trỏ đó là strong pointer hoặc weak pointer. Strong pointer là con trỏ đang trỏ đến một object có tồn tại trong bộ nhớ còn weak pointer sẽ trỏ đến object mà có thể đã bị xóa.

Có thể thấy, số nguyên nhỏ sẽ có 31-bit dùng để lưu giá trị. Trong trường hợp giá trị vượt ra ngoài phạm vi 31-bit, V8 sẽ sử dụng một cơ chế có tên là “boxing”: chuyển giá trị thành số chấm động và tạo ra một đối tượng để lưu giá trị đó.

Đối với kiến trúc 64-bit:

			|----- 32 bits -----|----- 32 bits -------|
Pointer:    |________________address______________(w1)|
Smi:        |____int32_value____|000000000000000000(0)|

Pointer Compression

Dựa trên việc các con trỏ mà được cấp phát trong bộ nhớ thường có 32-bit cao là giống nhau, V8 chỉ lưu 32-bit thấp ở trên heap và lưu 32-bit cao dùng để khôi phục toàn bộ 64-bit địa chỉ vùng nhớ (còn được gọi là isolate root) ở trên một thanh ghi nào đó. Kỹ thuật này được gọi là pointer compression.

Example

Ví dụ, xét đối tượng bên dưới cùng với thông tin debug của nó:

V8 version 11.6.0 (candidate)
d8> const object = {a: 1}
undefined
 
d8> %DebugPrint(object)
DebugPrint: 0x13c0001cc791: [JS_OBJECT_TYPE]
 - map: 0x13c0000db545 <Map[16](HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x13c0000c4b79 <Object map = 0x13c0000c41b5>
 - elements: 0x13c000000219 <FixedArray[0]> [HOLEY_ELEMENTS]
 - properties: 0x13c000000219 <FixedArray[0]>
 - All own properties (excluding elements): {
    0x13c000002a49: [String] in ReadOnlySpace: #a: 1 (const data field 0), location: in-object
 }

Dữ liệu được lưu trên heap là:

gef➤  x/16x 0x13c0001cc791 - 1
0x13c0001cc790:	0x000db545	0x00000219	0x00000219	0x00000002
0x13c0001cc7a0:	0x00000129	0x00010001	0x00000000	0x000dc735
0x13c0001cc7b0:	0x00002a49	0x00000084	0x00000002	0x00000591
0x13c0001cc7c0:	0x919c87e6	0x00000adc	0x7566280a	0x6974636e

Có thể thấy, để xem được chính xác vùng nhớ của object, ta cần phải trừ 0x13c0001cc791 đi 1 giá trị. Bên cạnh đó, giá trị 0x00000002 chính là giá trị của thuộc tính a mà bị nhân đôi.

Ngoài ra, các con trỏ của map, propertieselements đều chỉ có 32-bit thấp được lưu trên heap.

list
from outgoing([[V8 - Tagging]])
sort file.ctime asc

Resources