8

Image source: siliconangle.com

Author: HSY

Preface

The text will give you a brief understanding of how v8 internally handles objects, as well as the details of some optimizations made by v8 to speed up the access of object properties. In addition to combining the existing information, this article also links some source code locations corresponding to the implementation to save time when you need to integrate the source code to go deeper.

The purpose of this article is to understand the internal implementation details of v8. You can decide whether you need to read the following information first according to your own situation:

TaggedImpl

In the internal implementation of v8, all objects are derived TaggedImpl

The following figure is an illustration of the inheritance relationship of some classes involving Object implementation in v8:

The abstract logic of TaggedImpl is "tagging", so we need to further understand the meaning of "tag"

The GC of v8 is "Accurate GC, Precise GC", as opposed to "Conservative GC, Conservative GC"

The task of GC is to help us automatically manage the memory on the heap. When an object is recognized by the GC as a garbage object, the GC needs to reclaim the memory it occupies. The question that follows is how the GC judges pointers and non-pointers, because we know that the attributes of the object may be value attributes or references Other contents on the heap (pointers):

type Object = Record<string, number>;
const obj = { field1: 1 };

In the above code, we use Record to simulate the data structure of the object, which is actually a simple key-value pair. However, we define the value as a number type. This is because for value types, we can store their values directly, while for reference types, we store their memory addresses, and memory addresses are also values, so they are all Expressed by number

The advantage of conservative GC is that the coupling with the application is very low. In order to achieve this design purpose, it is necessary to make the GC rely as little as possible on the information provided by the application. As a result, the GC cannot accurately determine that a certain value represents a pointer. Still non-pointer. For example, in the above example, conservative GC cannot accurately know field1 the value of 1 represents a value or a pointer

Of course, conservative GC is not completely incapable of identifying pointers. It can guess pointers and non-pointers based on the specific behavior of the application when using memory (so it is not completely decoupled). To put it simply, it is to hard-code some guessing logic. For example, if we know some certain behaviors in the application, then we don't need to interact with the application, just hard-code this part of the logic into the GC implementation. For example, we know the encoding format of the ID card. If we want to verify whether a string of numbers is an ID card, we can verify it according to the encoding format or call the public security API (if available). The former is the conservative GC working method. Part of it can be verified, but those numbers that conform to the format but do not exist will also be recognized as ID cards

We know that if a memory address is accidentally released, it will definitely cause the application to enter the wrong state or even crash. In order to cope with this problem, conservative GC will mark all addresses that look like pointers as active when it is marking active objects, so that the problem of accidental release of memory will not occur. It is called "conservative". And because of this. But what follows is that some objects that may have been garbage survived, so conservative GC has the risk of compressing the heap

The v8 GC is an accurate GC. The accurate GC needs to be closely coordinated with the application. TaggedImpl is defined to cooperate with the GC to identify pointers and non-pointers. TaggedImpl uses a pointer tagging (this technology is mentioned Pointer Compression in V8

Simply put, pointer tagging technology uses the feature that addresses are aligned according to word length (integer multiples of word length). This feature comes like this:

  1. First of all, the word length of the CPU is even due to hardware design considerations.
  2. Then, due to the internal design of the early CPU, the efficiency of addressing even addresses was higher than the efficiency of addressing base addresses (but due to the upgrade of hardware design, it is not absolute at present)
  3. So everyone (compiler, memory allocation at runtime) will ensure that the address is aligned according to the word length

This continues to the present, basically as a default rule. Based on this rule, because the lowest binary bit of an even number is 0 , in v8:

  • For the value to be shifted one bit to the left, the lowest binary digit of such a value is 0
  • For pointers, the lowest binary position is 1

For example, for GC, 0b110 represents the value 0b11 (need to shift one bit to the right when used), and for 0b111 represents the pointer 0b110 (need to subtract 1 when addressing).

Through the labeling operation, the GC can think that if the lowest binary bit of an address is 0 , the position is Smi-small integer , otherwise it is HeapObject

You can refer to the garbage collection algorithm and implementation a more systematic understanding of the details of GC implementation

Object

Object is used in v8 to represent all objects managed by GC

The figure above demonstrates the memory layout of v8 runtime, where:

  • stack represents the stack used by native code (cpp or asm)
  • heap represents the heap managed by GC
  • ptr_ code refers to objects on the heap through 060879a5bd3027, if it is smi, there is no need to access the GC heap
  • If you want to manipulate the fields of the object on the heap, you need to further complete the hard-coded offset in the definition of the class to which the object belongs

The offsets of the fields in each class are defined in field-offsets-tq.h . The reason for manual hard-coding is that the instance memory of these classes needs to be allocated by GC, instead of using the native heap directly, so the offset automatically generated by the cpp compiler cannot be used.

Let's explain the encoding method (64bit system) through a legend:

  • The figure uses different colors to indicate the area defined by the object itself and the inherited area
  • There are no fields in Object, so Object::kHeaderSize is 0
  • HeapObject is a subclass of Object class, so its field offset start value is Object::kHeaderSize ( reference code ), HeapObject has only one field offset kMapOffset value equal to Object::kHeaderSize that is 0 , because the field size is kTaggedSize on the system The value is 8), so HeapObject:kHeaderSize is 8bytes
  • JSReceiver is a subclass of the HeapObject class, so its field offset starting value is HeapObject:kHeaderSize ( reference code ), JSReceiver also has only one field offset kPropertiesOrHashOffset , the value of which is HeapObject:kHeaderSize or 8 bytes, because the field size is kTaggedSize JSReceiver::kHeaderSize is 16bytes (plus the inherited 8bytes)
  • JSObject is a subclass of JSReceiver, so its field offset starting value is JSReceiver::kHeaderSize ( reference code ), JSObject also has only one field offset kElementsOffset , the value JSReceiver::kHeaderSize bytes, and the last JSObject::kHeaderSize is 24 bytes.

According to the above analysis results, after the inheritance is finally realized by manual coding, there are a total of three offsets in JSObject:

  • kMapOffset
  • kPropertiesOrHashOffset
  • kElementsOffset

These three offsets also mean that JSObject has three built-in properties:

  • map
  • propertiesOrHash
  • elements

map

The map is also generally called HiddenClass, which describes the meta-information of the object, such as the size of the object (instance_size) and so on. map is also inherited from HeapObject , so it is also an object managed by GC. The map field in JSObject is a pointer to the map object on the heap

Map layout annotated in the map source code and the following figure to understand the topological form of the map memory:

propertiesOrHash,elements

In JS, there is no significant difference in the use of arrays and dictionaries, but from the perspective of engine implementation, choosing different data structures for arrays and dictionaries internally can optimize their access speed, so use propertiesOrHash and elements respectively. For this purpose

For named properties, it will be associated with propertiesOrHash , and for indexed properties, it will be associated with elements . The word "association" is used because propertiesOrHash and elements are just pointers, and the engine will connect them to different data structures on the heap according to runtime optimization strategies.

We can use the following diagram to demonstrate the possible topological form of JSObject on the heap:

It should be noted that the generational GC of v8 divides the heap according to the activity and purpose of the object, so the map object will actually be placed in a dedicated heap space (so it will actually appear more organized than the above picture), but Does not affect the schematic of the above picture

inobject、fast

Above we introduced that named properties will be associated with propertiesOrHash pointer, and the data structure used to store properties, v8 does not directly select the common hash map , but has built-in 3 types of associated properties. :

  • inobject
  • fast
  • slow

Let's first understand the form of inobject and fast, the following is their overall diagram:

Inobject is the same as its name, indicating that the pointer corresponding to the property value is directly stored in the continuous address at the beginning of the object. It is the fastest access among the three forms (according to the description fast-properties

Pay attention to the inobject_ptr_x figure above. They are just pointers to the attribute value. Therefore, in order to find the corresponding attribute by name, you need to use a DescriptorArray . This structure records:

  • key, field name
  • PropertyDetails, which represents the meta information of the field, such as IsReadOnly , IsEnumerable etc.
  • value, it will be stored in it only when it is constant, if it is 1 , it means that the position is not used (can be understood in conjunction with the label above)

In order to access the inobject or fast attribute (the relevant implementation is at LookupIterator::LookupInRegularHolder ):

  1. V8 needs to search for the index of the property value in inobject array (because inobject is a continuous memory address, it can be regarded as an array) or property array (leftmost in the figure) DescriptorArray according to the property name.
  2. Then combine the first address of the array and the pointer offset, get the pointer of the attribute value, and then access the specific attribute value through the pointer of the attribute value (the relevant implementation is JSObject::FastPropertyAtPut )

Inobject is faster than fast, because the fast attribute has one more indirect addressing:

  1. After the inobject attribute knows the index of its attribute value, it can be offset directly according to the first address of the object ( map_ptr , propertiesOrHash_ptr , elements_ptr before inobject array are fixed sizes)
  2. And if it is fast, you need to get the first address of the PropertyArray at the first address of the kPropertiesOrHashOffset

Because inobject is the fastest access form, it is set as the default form in v8, but it should be noted that fast and inobject are complementary, but by default, the added attributes are processed in the form of inobject first, and When the following situation is encountered, the property will be added to the Fast PropertyArray:

  • When the number of overall inobject attributes exceeds a certain upper limit
  • When the dynamically added attributes exceed the reserved number of inobject
  • When slack tracking is completed

When v8 creates an object, it dynamically selects an inobject number, which is recorded as expected_nof_properties (described later), and then combines the number with the number of internal fields of the object (such as map_ptr etc.) to create the object

The initial number of inobjects will always be larger than the actual size currently required. The purpose is to serve as a buffer for attributes that may be dynamically added in the future. If there is no subsequent action to dynamically add attributes, it will inevitably cause a waste of space. This problem is It can be solved by slack tracking described later

such as:

class A {
  b = 1;
}

const a = new A();
a.c = 2;

When allocating space a A has only one attribute b expected_nof_properties selected by v8 will be larger than the actual required 1. Because of the dynamic nature of the JS language, the more allocated space allows subsequent dynamically added properties to also enjoy the efficiency of a.c = 2 . c example, 060879a5bd35b6 and 060879a5bd35b7 in the example are also inobject properties, although they are dynamically added subsequently

slow

Slow is slower than fast and inobject because slow attribute access cannot be optimized using inline cache technology. For more details about inline cache, please refer to:

Slow is mutually exclusive with inobject and fast. When entering slow mode, the property structure in the object is as follows:

The slow mode no longer needs the DescriptorArray mentioned above, and the field information is stored in a dictionary.

inobject upper limit

As mentioned above, the number of inobject properties has an upper limit, and the calculation process is roughly as follows:

// 为了方便计算,这里把涉及到的常量定义从源码各个文件中摘出后放到了一起
#if V8_HOST_ARCH_64_BIT
constexpr int kSystemPointerSizeLog2 = 3;
#endif
constexpr int kTaggedSizeLog2 = kSystemPointerSizeLog2;
constexpr int kSystemPointerSize = sizeof(void*);

static const int kJSObjectHeaderSize = 3 * kApiTaggedSize;
STATIC_ASSERT(kHeaderSize == Internals::kJSObjectHeaderSize);

constexpr int kTaggedSize = kSystemPointerSize;
static const int kMaxInstanceSize = 255 * kTaggedSize;
static const int kMaxInObjectProperties = (kMaxInstanceSize - kHeaderSize) >> kTaggedSizeLog2;

According to the above definition, the maximum number is 252 = (255 * 8 - 3 * 8) / 8

allow-natives-syntax

In order to demonstrate through the code later, here we need to introduce the --allow-natives-syntax option. This option is an option of v8. After turning on this option, we can use some private APIs. These APIs can facilitate the understanding of the internal details of the engine runtime. Used to write test cases in v8 source code

// test.js
const a = 1;
%DebugPrint(a);

The above code can be run by the command node --allow-natives-syntax test.js %DebugPrint is natives-syntax, and DebugPrint is one of the private APIs

More APIs can be found in runtime.h , and their specific usage can be understood by searching the test cases in the v8 source code. In addition, the corresponding implementation of in 160879a5bd370b objects-printer.cc

The content displayed after the above code runs is similar:

DebugPrint: Smi: 0x1 (1) # Smi 我们已经在上文介绍过了

Constructor creation

As mentioned above, when v8 creates an object, it will dynamically select an expected value. This value is used as the initial number of the inobject attribute, which is recorded as expected_nof_properties . Next, let’s see how this value is selected.

There are two main ways to create objects in JS:

  • Create from constructor
  • Object literal

Let's first look at the situation created from the constructor

The technique of using fields as inobject properties is not pioneered by v8. It is a common property processing solution in the compilation of static languages. V8 just introduced it into the design of the JS engine, and made some adjustments to the JS engine

For objects created from the constructor, the number of attributes can be during the compilation phase, so when the object is allocated, the number of inobject attributes can use the information collected during the compilation phase:

function Ctor1() {
  this.p1 = 1;
  this.p2 = 2;
}

function Ctor2(condition) {
  this.p1 = 1;
  this.p2 = 2;
  if (condition) {
    this.p3 = 3;
    this.p4 = 4;
  }
}

const o1 = new Ctor1();
const o2 = new Ctor2();

%DebugPrint(o1);
%DebugPrint(o2);

"Roughly" means that for the above Ctor2 will be considered to have 4 attributes, and the situation of condition

We can test by running the above code:

DebugPrint: 0x954bdc78c61: [JS_OBJECT_TYPE]
 - map: 0x0954a8d7a921 <Map(HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x0954bdc78b91 <Object map = 0x954a8d7a891>
 - elements: 0x095411500b29 <FixedArray[0]> [HOLEY_ELEMENTS]
 - properties: 0x095411500b29 <FixedArray[0]> {
    #p1: 1 (const data field 0)
    #p2: 2 (const data field 1)
 }
0x954a8d7a921: [Map]
 - type: JS_OBJECT_TYPE
 - instance size: 104
 - inobject properties: 10
 - elements kind: HOLEY_ELEMENTS
 - unused property fields: 8
 - enum length: invalid
 - stable_map
 - back pointer: 0x0954a8d7a8d9 <Map(HOLEY_ELEMENTS)>
 - prototype_validity cell: 0x0954ff2b9459 <Cell value= 0>
 - instance descriptors (own) #2: 0x0954bdc78d41 <DescriptorArray[2]>
 - prototype: 0x0954bdc78b91 <Object map = 0x954a8d7a891>
 - constructor: 0x0954bdc78481 <JSFunction Ctor1 (sfi = 0x954ff2b6c49)>
 - dependent code: 0x095411500289 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>

The above code will output two paragraphs DebugPrint , the above is the first paragraph:

  • Immediately after DebugPrint: printed the object we passed in o1
  • The following 0x954a8d7a921: [Map] is the map information of the object
  • We have already introduced that map is the meta-information of an object, so things like inobject properties are recorded in it
  • The above inobject properties is 10 = 2 + 8 , where 2 is the number of attributes collected during compilation, and 8 is the number of additional pre-allocated attributes
  • Because there are always map , propertiesOrHash , elements in the object header, the instance size of the entire object is headerSize + inobject_properties_size , which is 104 = (3 + (2 + 8)) * 8

You can verify the output %DebugPrint(o2) according to the above process

Empty constructor

In order to avoid confusion during the experiment, let's explain the size of the object allocated in the empty constructor:

function Ctor() {}
const o = new Ctor();
%DebugPrint(o);

The above print result shows that the inobject properties is also 10. According to the calculation process above, because the constructor has no attributes found at the compilation stage, the number should be 8 = 0 + 8

The reason why 10 is displayed is because if there are no attributes found at the compilation stage, a value of 2 will be given as the number of attributes by default. This is based on "most constructors will have attributes, and currently there is no possibility that they will be dynamically added later." assumed

Regarding the above calculation process, you can explore further shared-function-info.cc

Class

Above, we used function objects directly as constructors, and ES6 has already supported Class. Next, let’s look at the use of Class to instantiate objects.

In fact, Class is just a syntactic sugar. The runtime semantics of Class in the JS language standard is defined in section ClassDefinitionEvaluation Simply put, it will also create a function object (and set the name of the function to the Class name), so that subsequently our new Class is actually the same as new FunctionObject

function Ctor() {}
class Class1 {}

%DebugPrint(Ctor);
%DebugPrint(Class1);

We can run the above code, we will find that Ctor and Class1 are both JS_FUNCTION_TYPE

As we have mentioned before, the initial number of inobject properties will use the information collected at compile time, so the following forms are equivalent, and the number of inobject properties is 11 (3 + 8):

function Ctor() {
  this.p1 = 1;
  this.p2 = 2;
  this.p3 = 3;
}
class Class1 {
  p1 = 1;
  p2 = 2;
  p3 = 3;
}
class Class2 {
  constructor() {
    this.p1 = 1;
    this.p2 = 2;
    this.p3 = 3;
  }
}
const o1 = new Ctor();
const o2 = new Class1();
const o3 = new Class2();
%DebugPrint(o1);
%DebugPrint(o2);
%DebugPrint(o3);

The number of attributes collected during the compilation phase is called the "estimated number of attributes", because it only needs to provide the estimated accuracy, so the logic is very simple. When solving the analytic function or Class definition, a statement to set the attribute is issued. Add 1 to the "estimated number of attributes". The following forms are equivalent, they will recognize the "estimated number of properties" as 0 and cause the initial value of inobject properties to be set to 10 (As I said above, when estimated is 0, it will always be assigned a fixed The number of 2, plus the pre-allocation of 8, will make the initial number of inobjects set to 10):

function Ctor() {}

// babel runtime patch
function _defineProperty(obj, key, value) {
  if (key in obj) {
    Object.defineProperty(obj, key, {
      value: value,
      enumerable: true,
      configurable: true,
      writable: true,
    });
  } else {
    obj[key] = value;
  }
  return obj;
}

class Class1 {
  constructor() {
    _defineProperty(this, "p1", 1);
    _defineProperty(this, "p2", 2);
    _defineProperty(this, "p3", 3);
  }
}

const o1 = new Ctor();
const o2 = new Class1();
%DebugPrint(o1);
%DebugPrint(o2);

Class1 constructor _defineProperty for the current estimate is too complicated logic, logic design simple estimate is not because they can not analyze the above example, technically, but because of the dynamic nature of JS language, and in order to keep start-up speed (It is also the advantage of dynamic language) It is not suitable for the use of heavy static analysis techniques here.

_defineProperty is actually the result of Babel's current compilation. Combined with the slack tracking described later, even if the estimated number here does not meet our expectations, it will not have much impact, because the attributes of our single class The number of cases exceeding 10 will not be the majority in the entire application, but if we consider the case of inheritance:

class Class1 {
  p11 = 1;
  p12 = 1;
  p13 = 1;
  p14 = 1;
  p15 = 1;
}

class Class2 extends Class1 {
  p21 = 1;
  p22 = 1;
  p23 = 1;
  p24 = 1;
  p25 = 1;
}

class Class3 extends Class2 {
  p31 = 1;
  p32 = 1;
  p33 = 1;
  p34 = 1;
  p35 = 1;
}

const o1 = new Class3();
%DebugPrint(o1);

Because of the existence of inheritance, it is likely that after multiple inheritance, our number of attributes will exceed 10. If we print the above code, we will find that the inobject properties is 23 (15 + 8). If compiled by babel, the code will become:

"use strict";

function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }

class Class1 {
  constructor() {
    _defineProperty(this, "p11", 1);
    _defineProperty(this, "p12", 1);
    _defineProperty(this, "p13", 1);
    _defineProperty(this, "p14", 1);
    _defineProperty(this, "p15", 1);
  }
}

class Class2 extends Class1 {
  constructor(...args) {
    super(...args);

    _defineProperty(this, "p21", 1);
    _defineProperty(this, "p22", 1);
    _defineProperty(this, "p23", 1);
    _defineProperty(this, "p24", 1);
    _defineProperty(this, "p25", 1);
  }
}

class Class3 extends Class2 {
  constructor(...args) {
    super(...args);

    _defineProperty(this, "p31", 1);
    _defineProperty(this, "p32", 1);
    _defineProperty(this, "p33", 1);
    _defineProperty(this, "p34", 1);
    _defineProperty(this, "p35", 1);
  }
}

const o1 = new Class3();
%DebugPrint(o1);

The number of inobject properties above is only 14, because the estimated value of the number of inobject properties of Class3, plus the estimated value of the number of inobject properties of its ancestor class, the estimated value of its two ancestor classes is 2 (because The number is not collected at compile time and the fixed number is allocated by default 2), so the estimated value of the inobject attribute of Class3 is 6 = 2 + 2 + 2 , plus 8 additional allocations, and finally 14

Our actual number of attributes is 15, which results in the 15th attribute p35 being assigned to fast type. Looking back at the code that has not been compiled by babel, all attributes will be inobject type

Initially, it was found that the compilation results of babel and tsc were different. The latter did not use _defineProperty , thinking that the compilation of babel was flawed. Later found that the result of babel is actually the behavior specified in the standard, see Public instance fields -the instance fields are added Object.defineProperty For tsc, the useDefineForClassFields (this option is enabled by default in the current deno-v1.9)

I wanted to say that you can choose tsc, but now it seems that in some scenarios with extreme performance requirements, avoiding the introduction of the compilation link may be the best way

Create from object literal

const a = { p1: 1 };
%DebugPrint(a);

Running the above code, you will find that the inobject properties is 1. There is no reserved space for 8 here, because the 160879a5bd3b44 CreateObjectLiteral create the object literal. There is no strategy to reserve space inside, but is used directly compiles the collected information, which is different from the JSObject::New method inside the method created from the constructor

Creating from the object literal will use the number of attributes in the literal as the number of inobject properties , so the attributes added later will be of the fast type

Empty object literal

Similar to the case of the empty constructor, the size of the empty object literal also needs to be discussed separately:

const a = {};
%DebugPrint(a);

Run the above code, you will find that the inobject properties is 4, this is because:

So 4 is a hard-coded value. When creating an empty object, use this value as the initial number of inobject properties

also mentions in the CreateObjectLiteral source code. If Object.create(null) is used, it is directly in the slow mode.

Switch between inobject, fast, slow

The existence of the three modes of inobject, fast, and slow is based on the concept of divide and conquer. For static scenes (such as constructor creation), inobject and fast are applicable, and for dynamic parts, slow is applicable. Let's take a brief look at the switching conditions between the three

  1. In the case of sufficient inobject quota, attributes are first treated as inobject type
  2. When the inobject configuration is insufficient, the attribute is regarded as fast
  3. When the fast quota is insufficient, the object is switched to slow mode.
  4. In a certain step in the middle, the operation of delete executed to delete the attributes (except for deleting the last order attribute, the other order attributes will be deleted) so that the entire object is switched to slow mode
  5. If an object is set as the property property of another function object, the object will also switch to slow mode, see JSObject::OptimizeAsPrototype
  6. Once the object is switched to slow mode, from the developer's point of view, it can basically be considered that the object will no longer switch to fast mode (although JSObject::MigrateSlowToFast will be used to switch back to fast in some special cases inside the engine)

The above switching rules seem to be very cumbersome (and may not be all the cases), but in fact, the idea behind it is very simple. Inobject and fast are both "partially static" optimization methods, while slow is a completely dynamic form. If the object frequently dynamically adds attributes or performs the delete operation, it is predicted that it is likely to change frequently in the future, so it may be better to use a purely dynamic form, so switch to slow mode

We can learn a little about the quota of fast type. The fast type is stored in PropertyArray. This array kFieldsAdded (the current version is 3). There is currently a kFastPropertiesSoftLimit (currently 12) As its limit, Map::TooManyFastProperties uses > , so the current quota of fast type is 15

You can use the following code to test:

const obj = {};
const cnt = 19;
for (let i = 0; i < cnt; i++) {
  obj["p" + i] = 1;
}
%DebugPrint(obj);

Set cnt to 4 , 19 and 20 respectively, and you will get output similar to the following:

# 4
DebugPrint: 0x3de5e3537989: [JS_OBJECT_TYPE]
 #...
 - properties: 0x3de5de480b29 <FixedArray[0]> {

#19
DebugPrint: 0x3f0726bbde89: [JS_OBJECT_TYPE]
 #...
 - properties: 0x3f0726bbeb31 <PropertyArray[15]> {

# 20
DebugPrint: 0x1a98617377e1: [JS_OBJECT_TYPE]
 #...
 - properties: 0x1a9861738781 <NameDictionary[101]>
  • In the above output, when 4 attributes are used, they are all inobject type FixedArray[0]
  • When 19 attributes are used, 15 attributes are already fast PropertyArray[15]
  • When 20 attributes are used, because the upper limit is exceeded, the entire object is switched to slow type NameDictionary[101]

As for why inobject shows FixedArray , it’s just because when the fast type is not used, propertiesOrHash_ptr points to a empty_fixed_array default. Students who are interested can confirm property_array

slack tracking

As we mentioned earlier, the number of initial inobject attributes in v8 will always be allocated more. The purpose is to allow subsequent attributes that may be dynamically added to become inobject attributes, so as to enjoy the fast access efficiency it brings. However, if the over-allocated space is not used, it will be wasted. In v8, a technology called slack tracking is used to improve the space utilization.

This technique is simply implemented like this:

  • initial_map() attribute in the map of the constructor object, which is the template created by the constructor object, that is, their map
  • initial_map() tracking will modify instance_size attribute value in the 060879a5bd3ef6 attribute, which is used when the GC allocates memory space
  • When an object is created with a constructor C for the first time, its initial_map() is not set, so the value will be set for the first time. Simply put, it is to create a new map object and set the object's construction_counter property, see Map::StartInobjectSlackTracking
  • construction_counter is actually a decreasing counter, the initial value is kSlackTrackingCounterStart which is 7
  • Each subsequent time (including the current time) use the constructor to create an object, the construction_counter will be decremented by , when the count is 0, the current number of attributes (including dynamically added ones) will be summarized, and the final instance_size will be obtained.
  • After slack tracking is completed, the subsequent dynamically added attributes are all of the fast type

The form of construction_counter count is similar to the following figure:

Slack tracking is based on the number of constructor calls, so objects created using object literals cannot use it to improve space utilization. This also explains the creation of empty literals mentioned above. The default pre-allocation is 4. Instead of reserving 8 as in the creation of the constructor (because slack tracking cannot be used to improve the space utilization in the future, so it can only be throttled at the beginning)

You can more about its implementation details Slack tracking in V8 160879a5bd3fe6

summary

We can summarize the key parts of the above as follows:

In actual use, we don't have to consider the above details, just make sure that under the conditions:

This article briefly combined the source code to introduce how objects are handled in v8. I hope I can be fortunate to be the initial reading for everyone to learn more about v8 memory management.

Reference

This article was published from , the big front-end team of NetEase Cloud Music. Any unauthorized reprinting of the article is prohibited. We recruit front-end, iOS, and Android all year round. If you are ready to change jobs and you happen to like cloud music, then join us at grp.music-fe (at) corp.netease.com!

云音乐技术团队
3.6k 声望3.5k 粉丝

网易云音乐技术团队