Photo credit: Siliconangle.com

Author: HSY

preface

This article will give you a brief look at how objects are handled internally in V8, as well as the details of some of the optimizations v8 has made to speed up access to object properties. In addition to combining the existing information, this article also links to the corresponding source location of some implementations, to save you later need to combine the source code to spend time

The purpose of this article is to understand the internal implementation details of V8, so you can decide if you want to read the following materials first:

  • A tour of V8: object representation
  • Fast properties in V8

TaggedImpl

In the V8 internal implementation, all objects are derived from TaggedImpl

The following diagram shows the inheritance relationship of some classes in V8 that are involved in Object implementation:

The logic that TaggedImpl abstracts is “tagging,” so we need to learn more about what “tagging” means

V8 GC is a “Precise GC”, a Precise GC, as opposed to a “Conservative GC”

The GC’s job is to help us automatically manage memory on the heap. When an object is recognized as garbage by the GC, it needs to reclaim its memory. The question then arises as to how the GC determines whether it is a pointer or a non-pointer, since we know that the object’s properties may be value properties or refer to something else on the heap (Pointers) :

type Object = Record<string.number>;
const obj = { field1: 1 };
Copy the code

In the above code we simulate the data structure of the object through Record, which is a simple key-value pair. But we define values as numbers, because for value types, we just store their values, and for reference types, we store their memory addresses, which are also values, so we use number

Conservative GC has the advantage of low coupling with applications. In order to achieve this goal, the GC needs to rely as little as possible on information provided by applications. As a result, the GC cannot accurately determine whether a value represents a pointer or a non-pointer. For example, in the example above, the conservative GC does not know exactly whether the value 1 of field1 represents a number or a pointer

Of course, conservative GC does not completely fail to recognize Pointers; it can make guesses about Pointers and non-pointers based on the behavior of the application’s specific memory usage (and therefore not completely decoupled). In simple terms, we hardcode some guessing logic, for example, we know some deterministic behavior in the application, so we don’t have to interact with the application and hardcode this logic directly into the GC implementation. We know id code format, for example, if you want to verify whether a string of Numbers id, we can according to the encoding formats to verify, also can call the police API (if any), the former is conservative type GC works, you can verify the part, but for those who conform to the format, but there is no number, It will also be recognized as an ID card

We know that if a memory address is accidentally released, it can cause the application to go into the wrong state or even crash later on. The conservative GC deals with this problem by marking any address that looks like a pointer as active when it marks an active object, so that it doesn’t accidentally free memory, hence the name. However, some objects that might already be garbage survive, so conservative GC runs the risk of crushing the heap

V8’s GC is the exact GC, and the exact GC needs to work closely with the application. The TaggedImpl is defined to help the GC recognize Pointers and non-pointers. TaggedImpl uses a technique called Pointer Tagging (mentioned in Pointer Compression in V8)

Pointer tagging simply takes advantage of the fact that addresses are aligned by word length (integer multiples of word length). This feature comes from:

  1. First, CPU word lengths are all even for hardware design reasons
  2. However, due to the internal design of the early CPU, the efficiency of addressing even addresses was higher than that of addressing cardinal addresses (although due to the hardware design upgrade, it is not absolute at present).
  3. So everyone (the compiler, the memory allocation at runtime) makes sure the address is aligned to the word length

This continues as a default rule until now. Based on this rule, since the lowest binary bit of an even number is 0, in V8:

  • Move the value one bit to the left so that the lowest binary bit of the value is0
  • For Pointers, the lowest binary position is1

For example, for GC, 0b110 represents the value 0b11 (moved right one bit when used), and for 0b111 it represents the pointer 0b110 (subtracted 1 when addressing).

By labeling, the GC considers an address to be a SMI-Small INTEGER if its lowest binary bit is 0, or a HeapObject if it is not

You can refer to garbage collection Algorithms and Implementations for a more systematic overview of GC implementation details

Object

Object is used within V8 to represent all objects managed by GC

The figure above illustrates the memory layout of the V8 runtime, where:

  • Stack represents the stack used by native code (CPP or ASM)
  • Heap represents the heap managed by GC
  • Native code passesptr_To refer to objects on the heap, without accessing GC’s heap if it is SMI
  • If you want to manipulate the fields of an object on the heap, you do this further by hard-coding offsets in the definition of the class to which the object belongs

Offsets for fields in each class are defined in field-offsets-tq.h. The reason for manual hardcoding is that the instance memory of these classes is allocated through GC, rather than using native heap directly, so you can’t take advantage of the offsets automatically generated by the CPP compiler

Let’s use a legend to illustrate the encoding mode (64-bit system) :

  • Different colors represent regions defined by the object itself and regions inherited from it
  • Object has no fields, soObject::kHeaderSize0
  • HeapObject is a subclass of Object, so its field offset starts atObject::kHeaderSize(Reference code), the HeapObject has only one field offsetkMapOffsetValue is equal to theObject::kHeaderSize0Because the field size iskTaggedSize(On 64-bit systems the value is 8), soHeapObject:kHeaderSizeIs 8 bytes
  • JSReceiver is a subclass of the HeapObject class, so its field offset starts atHeapObject:kHeaderSize(Reference codeJSReceiver also has only one field offsetkPropertiesOrHashOffset, its value isHeapObject:kHeaderSizeThat is, 8bytes, because the field size iskTaggedSize, soJSReceiver::kHeaderSize16bytes (plus inherited 8bytes)
  • JSObject is a subclass of JSReceiver, so its field offset starts atJSReceiver::kHeaderSize(Reference code), JSObject also has only one field offsetkElementsOffsetAnd has a value ofJSReceiver::kHeaderSizeThat is 16bytes, and finallyJSObject::kHeaderSizeIs 24 bytes

Based on the above analysis, there are three offsets in JSObject when inheritance is finally implemented through manual coding:

  • kMapOffset
  • kPropertiesOrHashOffset
  • kElementsOffset

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

  • map
  • propertiesOrHash
  • elements

map

A map, also known as a HiddenClass, describes an object’s meta-information, such as its size (instance_size), and so on. Map also inherits from HeapObject and is itself a GC-managed object, and the Map field in JSObject is a pointer to the Map object on the heap

We can use the map layout annotation in map source code to understand the topology of map memory.

PropertiesOrHash elements,

There is no significant difference in the use of arrays and dictionaries in JS, but from an engine implementation point of view, choosing different data structures internally for arrays and dictionaries can optimize their access speed, so using propertiesOrHash and Elements respectively is the purpose

Named properties are associated with propertiesOrHash and indexed properties are associated with Elements. The word “association” is used because propertiesOrHash and Elements are just Pointers that the engine connects to different data structures on the heap based on runtime optimization strategies

We can demonstrate the possible topology of JSObject on the heap by looking at the following diagram:

It should be noted that v8’s generational GC divides the heap by object activity and use, so the map objects are actually placed in a dedicated heap space (and therefore more organized than the map above), but this does not affect the illustration above

Inobject, fast

The named Properties pointer will be associated with the data structure pointed to by the propertiesOrHash pointer of the object. V8 does not directly select the common hash map for the data structure used to store properties. Instead, v8 has built in three forms of associated properties:

  • inobject
  • fast
  • slow

Let’s first look at the forms of inObject and FAST. Here is their overall representation:

Inobject, as its name suggests, means that Pointers to property values are stored directly in successive addresses at the beginning of the object, and is the fastest of the three (as described in fast-properties).

Note that inobject_ptr_x in the figure above is only a pointer to the attribute value, so to find the corresponding attribute by name, use a struct called DescriptorArray. This struct contains the following items:

  • Key: indicates the field name
  • PropertyDetails, which represents the meta information of the field, for exampleIsReadOnly,IsEnumerable
  • Value, only if it’s constant, if it’s1Indicates that the location is not in use.

In order to access inobject or fast properties (related implementation in LookupIterator: : LookupInRegularHolder) :

  1. V8 first needs to search for the index of the property value in either the InObject array (inobjects can be regarded as arrays because they are contiguous memory addresses) or the property array (leftmost in the figure) in the DescriptorArray, based on the property name

  2. And then combined with the first address with the pointer array pointer offset, get the value of an attribute, then through the pointer attribute value, access to specific attribute values (the relevant implementation JSObject: : FastPropertyAtPut)

Inobject is faster than FAST because the fast attribute has one more indirection:

  1. When an inobject property knows the index of its property value, it can be offset by the first address of the object (map_ptr, propertiesOrHash_ptr, elementS_ptr are fixed sizes).

  2. In the case of fast, the first address of the object must be offset by kPropertiesOrHashOffset to obtain the first address of the PropertyArray, and then offset the index based on the first address

Since inObject is the fastest access form, it is set to the default form in V8. However, it is important to note that fast and InObject are complementary, except that by default, added attributes take precedence over inObject. Properties are added to Fast’s PropertyArray:

  • When the total number of InObject attributes exceeds a certain limit
  • The number of dynamically added attributes exceeds the reserved number of inObject attributes
  • When Slack Tracking is done

When v8 creates an object, it dynamically selects an InObject number, which is called expected_NOF_Properties (described below), and then creates the object with that number in combination with the number of the object’s internal fields, such as map_ptr

The initial number of InObjects is always larger than the actual size needed. The purpose is to serve as a buffer for potential attributes that can be added dynamically later on. If there is no subsequent action to add attributes dynamically, space will be wasted

Such as:

class A {
  b = 1;
}

const a = new A();
a.c = 2;
Copy the code

When allocating space for A, v8 selects the expected_NOF_properties value larger than the required 1, even though A has only one attribute B. Due to the dynamic nature of the JS language, multi-allocated space allows properties that are added dynamically later to enjoy the efficiency of InObject, such as a.c = 2 in the example, where C is also an InObject property, although it was added dynamically later

slow

Slow is slower than fast and InObject because slow attributes cannot be optimized using inline cache technology. For more details on inline cache, see:

  • Inline caching
  • Explaining JavaScript VMs in JavaScript – Inline Caches

Slow is mutually exclusive with InObject and Fast. When the slow mode is entered, the attribute structure of the object is as follows:

Slow mode no longer requires the above mentioned DescriptorArray; all the field information is stored in one dictionary

Inobject ceiling

The number of inObject properties mentioned above is limited, and the calculation process is roughly as follows:

// In order to facilitate calculation, the involved constant definitions are extracted from the source files and grouped together
#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;
Copy the code

As defined above, on a 64-bit system without pointer compression enabled, the maximum number is 252 = (255 * 8-3 * 8) / 8

allow-natives-syntax

In order to demonstrate this in code, the –allow-natives-syntax option is a NATIVES option in V8. With this option enabled, we can use some proprietary apis to learn the internal details of the engine runtime. It was originally used to write test cases in V8 source code

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

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

More apis can be found in Runtime.h, and how to use them can be found by searching test cases in the V8 source code. In addition, the implementation of DebugPrint is in objects-printer.cc

The code above runs something like this:

DebugPrint: Smi: 0x1 (1) # Smi we've covered this above
Copy the code

Constructor creation

When v8 creates the object, it dynamically selects the expected value, which is the initial number of inObject properties, and is referred to as expected_NOF_properties

There are two main ways to create objects in JS:

  • Created from the constructor
  • Object literals

Let’s look at the creation from the constructor first

The technique of using fields as inObject properties was not invented in V8 and is a common property handling scheme in static language compilations. V8 just introduced it into the DESIGN of the JS engine and made some tweaks to it

For objects created from constructors, since the number of attributes is roughly available at compile time, the number of inObject attributes can be allocated using information gathered at compile time:

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);
Copy the code

What “rough” means is that Ctor2 above is considered to have four attributes, regardless of the condition case

We can test this by running the code above:

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)>
Copy the code

The code above prints two DebugPrint sections, the first of which is shown above:

  • thenDebugPrint:It prints the object that we passed ino1
  • The subsequent0x954a8d7a921: [Map]Is the map information of the object
  • We’ve already shown that a map is the meta-information of an object, so things likeinobject propertiesIt’s all in there
  • The aboveinobject properties10 is 2 plus 8, where 2 is the number of attributes collected during compilation and 8 is the number of additional pre-allocated attributes
  • Because there’s always a pointer in the object headermap,propertiesOrHash,elements, so the entire object’s instance size isheaderSize + inobject_properties_size104 is 3 plus 2 plus 8 times 8

You can verify the output of %DebugPrint(O2) according to the procedure above

Empty constructor

To avoid confusion, let’s explain the size of an empty constructor:

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

The number of inObject properties should be 8 = 0 + 8 because the constructor has no properties

The reason 10 is displayed is that if no attributes are found during compilation, the default value 2 is given as the number of attributes, assuming that most constructors will have attributes and that there is no possibility of them being added dynamically at this time

The above calculation process can be further explored by using shared-function-info.cc

Class

In ES6, Class is already supported. Let’s look at instantiating an object using Class

Actually the Class is just a syntactic sugar, JS language standard run-time semantics of a Class definition in ClassDefinitionEvaluation section. In short, we also create a FunctionObject (and set the name of the function to the Class name), so that our new Class actually has the same semantics as our new FunctionObject

function Ctor() {}
class Class1 {}

%DebugPrint(Ctor);
%DebugPrint(Class1);
Copy the code

We can run the above code and see that Ctor and Class1 are both JS_FUNCTION_TYPE

As we explained earlier, the initial number of InObject properties is based on information gathered 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);
Copy the code

The number of attributes collected at compile time is called the “estimated number of attributes”, because it only needs to provide the estimated precision, so the logic is simple. When parsing a function or Class definition, send a statement setting the attributes and the “estimated number of attributes” adds up to 1. The following form is equivalent to recognizing the “estimated number of properties” as 0, resulting in an initial inObject properties value of 10. Will set the initial inObject number 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);
Copy the code

The _defineProperty in the Class1 constructor is too complex for the current predictive logic, which is easy to design not because it is technically impossible to analyze the examples above, but because of the dynamic nature of the JS language, In order to maintain startup speed (which is also an advantage of dynamic languages), this is not a good place to use overly static analysis techniques

The _defineProperty form is actually compiled by Babel so far. With slack tracking we’ll be looking at later, it doesn’t matter if the estimated value doesn’t match our expectations. Because our single class with more than 10 attributes is not the majority of cases in the entire application, but if we consider 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);
Copy the code

Because of inheritance, it’s very likely that after multiple inheritances, we’ll have more than 10 attributes. If we print the code above, we will see that the inObject properties are 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);
Copy the code

The number of inobject properties above is only 14. The reason is that the estimated number of Class3 inObject properties and the estimated number of inobject properties of its ancestor class need to be added. Its two ancestor classes are both estimated at 2 (a fixed number of 2 assigned by default because no number was collected at compile time), so the estimate of Class3’s inobject property is 6 = 2 + 2 + 2, plus an additional 8 allocated, and finally 14

Our actual number of attributes is 15, which results in the 15th attribute, P35, being assigned to type FAST. Looking back at code not compiled by Babel, all attributes will be of type InObject

It was initially found that Babel compiled differently from TSC, which did not use the _defineProperty form, believing that the Babel compilation implementation was flawed. The result of Babel is the behavior specified in the standard. See Public Instance Fields – instance fields are added using Object.defineProperty. For TSC, the same compilation results can be achieved by enabling useDefineForClassFields (currently enabled by default in deno-v1.9).

I was going to say TSC is an option, but it seems that avoiding compilation is probably the best way to do it in scenarios where performance is extremely critical

Created from an object literal

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

The number of InObject properties is 1. There is no room for 8 inObject properties, because CreateObjectLiteral is created from the object literal through the CreateObjectLiteral method. There is no internal policy for reservating space, but instead uses information gathered by compilation directly. This is different from the strategy inside the JSObject::New method created from the constructor

Creating from an object literal will use the number of properties in the literal as the number of InObject properties, so subsequent properties will be of type FAST

Empty object literals

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

const a = {};
%DebugPrint(a);
Copy the code

Run the code above and you will see that the number of InObject properties is 4 because:

  • CreateObjectLiteral will call in the Factory: : ObjectLiteralMapFromCache
  • Factory: : ObjectLiteralMapFromCache logic is, its literal, useobject_function().initial_map()To make a template for creating objects
  • object_function()Self creation inGenesis::CreateObjectFunctionIn, amongkInitialGlobalObjectUnusedPropertiesCountIs 4

So 4 is a hard-coded value that is used as the initial number of InObject properties when empty objects are created

Create (null) creates an Object directly in slow mode

Switch between InObject, fast and slow

The existence of inObject, Fast and Slow is based on the concept of divide and conquer. Inobject, fast for static scenarios (such as constructor creation) and Slow for dynamic parts. Let’s take a quick look at the switching conditions among the three

  1. If the InObject quota is sufficient, attributes are preferred as inObject
  2. When inObject is not configured properly, the attribute is considered to be of type FAST
  3. When the fast quota is also insufficient, the object is switched to slow mode
  4. In one of the intermediate steps, it doesdeleteThe operation to delete an attribute (all but the last one) causes the entire object to switch to slow mode
  5. If an object is set to another function objectpropertyProperty, the object will also switch to slow mode, seeJSObject::OptimizeAsPrototype
  6. Once the switch object into a missile model, from the perspective of developers, basic can think the object won’t switch to fast mode (though the engine internal some special circumstances will use JSObject: : MigrateSlowToFast switch back to the fast)

The above switching rules may seem tedious (and may not be the whole case), but the idea behind them is very simple. Inobject and Fast are both “static” optimization methods, while slow is completely dynamic. When an object frequently adds attributes dynamically or performs a 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 bit about the quota of type fast. Type fast is stored in PropertyArray, which expands its length by kFieldsAdded each time (current version is 3). Is there a kFastPropertiesSoftLimit (current is 12) as its limit, and the Map: : TooManyFastProperties >, is used in type so fast is now the biggest is 15 quota

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);
Copy the code

Setting CNT to 4,19, and 20, respectively, produces 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]>
Copy the code
  • In the output above, when four attributes are used, they are of type InObjectFixedArray[0]
  • When 19 attributes are used, 15 of them are already fastPropertyArray[15]
  • When 20 attributes are used, the object switches to slow as a whole because the upper limit is exceededNameDictionary[101]

The reason why inObject displays FixedArray is because propertiesOrHash_ptr points to an empty_fixed_array by default when the fast type is not used. Those who are interested can confirm this by reading property_array

slack tracking

As mentioned earlier, the initial number of InObject attributes in V8 is always allocated a bit more, so that later attributes that might be added dynamically can also become InObject attributes to benefit from their fast access efficiency. But extra allocated space can be wasted if it’s not used, and v8 uses a technique called Slack tracking to improve utilization

The technique is implemented simply as follows:

  • The constructor object has one in its mapinitial_map()Property, which is the template created by the constructor object, their map
  • Slack Tracking will changeinitial_map()Properties of theinstance_sizeProperty value, which is used when GC allocates memory space
  • When a constructor C is first used to create an object, itsinitial_map()Is not set, so it is set for the first time, simply creating a new map object and setting the object’sconstruction_counterProperties, seeMap::StartInobjectSlackTracking
  • Diminishing construction_counter is actually a counter, the initial value is kSlackTrackingCounterStart or 7
  • Each subsequent object created with this constructor, including any time, is decrement of construction_counter, and when the count reaches zero, the current number of attributes (including those added dynamically) is summarized to get the final instance_size
  • After Slack Tracking is done, subsequent dynamically added attributes are of the fast type

Construction_counter counts look like the following:

Slack tracking is based on the number of constructor calls, so objects created using object literals cannot be used to improve space utilization, which explains why empty literals are created. The default is 4 preallocated instead of 8 reserved for constructor creation (throttling at the beginning because slack Tracking can’t be used to improve space utilization)

You can learn more about the implementation details by looking at Slack Tracking in V8

summary

We can summarize the important points above as follows:

  • There are three modes for object properties: InObject, fast, and slow
  • The efficiency of attribute access decreases from left to right
  • The default attribute type is InObject. After the reserved quota is exceeded, the added attribute belongs to fast
  • When the fast quota continues to be exceeded, the object is switched to slow altogether
  • The initial quota for InObject will vary depending on whether “constructor creation” or “object literal” creation is used, based on the information collected by the compiler (approximate number of attributes + 8 with a maximum of 252), and the latter is fixed at 4
  • useObject.create(null)The object created is directly slow
  • For any object A, during its declaration period, usedeleteRemoves attributes of all but the last alignment, or sets A to another constructorprototypeProperty, all of which switches object A to slow
  • Currently, switching to slow will no longer allow you to switch back to fast

In practical use, we don’t have to worry about the above details, just make sure that when conditions are available:

  • Create objects as constructors as possible; in other words, create attributes as dynamically as possible. In fact, making JS code as static as possible like this is a core principle that caters to internal engine optimizations for better performance, such as keeping variable types as unique as possible to avoid JIT failures
  • If you need to add a lot of attributes dynamically, or if you need to delete attributes, it is better to use the Map object directly (although the engine will automatically switch, but using Map is more suitable for such scenarios, and saves the consumption of internal switching).

This article briefly introduces how objects are handled in V8 with the source code. Hopefully, it will be an initial reading for you to learn more about V8 memory management

The resources

  • Garbage collection algorithm and implementation
  • A tour of V8: object representation
  • V8 engine JSObject structure analysis and memory optimization ideas
  • Fast properties in V8
  • Pointer Compression in V8
  • Slack tracking in V8

This article is published from netease Cloud Music big front end team, the article is prohibited to be reproduced in any form without authorization. Grp.music – Fe (at) Corp.Netease.com We recruit front-end, iOS and Android all year long. If you are ready to change your job and you like cloud music, join us!