典型的秘密,如何更快地是20,000倍 - v8引擎的隐藏类优化
#javascript #开源 #typescript #电脑科学

前言

// RUNTIME VALIDATORS
export function is<T>(input: unknown | T): input is T; // returns boolean
export function assert<T>(input: unknown | T): T; // throws TypeGuardError
export function validate<T>(input: unknown | T): IValidation<T>; // detailed
export const customValidators: CustomValidatorMap; // customizable

// ENHANCED JSON
export function application<...Args>(): IJsonApplication; // JSON schema
export function assertParse<T>(input: string): T; // type safe parser
export function assertStringify<T>(input: T): string; // safe and faster
    // +) isParse, validateParse 
    // +) stringify, isStringify, validateStringify

// RANDOM DATA GENERATOR
export function random<T>(g?: Partial<IRandomGenerator>): Primitive<T>;

让我们研究为什么typiaclass-validator快得多。

我撰写了一些文章,介绍了我的打字稿运行时验证器库typia在此处的dev.to社区中。在这些先前的文章中,我经常解释说,typia提高了通过AOT(提前)补充的验证,并且比class-validator快20,000倍。

顺便说一句,描述了AOT汇编,我只是告诉typia为Target T类型生成静态验证代码,而此类静态代码使程序更快。我从来没有解释过这种静态验证代码比class-validator的动态验证代码快得多的原因。

今天,我将描述原因。

// TYPESCRIPT SOURCE CODE
typia.is<number>(3);
typia.createIs<Date>();

// COMPILED JAVASCRIPT CODE
((input) => "number" === typeof input)(3);
(input) => input instanceof Date;

 raw `assert()` endraw  function benchmark

Measured on Surface Pro 8

每个对象都是HashMap

为了帮助您的理解,我将使用许多形象表达式和伪代码。但是,这些形象表达式和伪代码可能与实际的计算机工程理论有所不同。请阅读本文考虑此类方面。

您是否听说过动态语言中的每个对象都是HashMap

要知道typia如何优化性能,您可以理解这个概念。为什么动态语言使用HashMap,以及HashMap如何使程序较慢。

// in dynamic language like JavaScript,
const obj = {};

// you can any properties at any time
obj.a = "A";
obj.wafdfaf = 3;
obj[RandomGenerator.string()] = true;

// removing properties are also same
delete obj.a;
delete obj.wafdfaf;

起初,JavaScript是一种动态语言。

因此,可以将任何类型的值分配给变量。在对象情况下,甚至可以随时添加或删除属性。
为了支持动态属性的插入和删除,必须将对象实现为HashMap类型。

这就是动态语言将HashMap用于对象的原因,而动态语言中没有任何例外情况。 python,ruby,php等,所有这些都将HashMap用于对象。

HashMap

export class HashMap<Key, T> {
    private hasher_: (key: Key) => number;
    private equal_: (x: Key, y: Key) => number;

    // HashMap stores its key-value pairs into a Linked List
    private data_: List<Entry<Key, T>>;

    // Instead, have bucket array of linked list nodes, for fast key finding
    private buckets_: List.Node<Entry<Key, T>>[][];

    private find(key: Key): List.Node<Entry<Key, T>> {
        const hash: number = this.hasher_(key);
        const index: number = hash % this.buckets_.length;
        const bucket: List.Node<Entry<Key, T>>[] = this.buckets_[index] ?? [];

        return bucket.find(
            (node) => this.equal_(node.value.key, key)
        ) ?? this.data_.end();
    }

    // Therefore, single key access could be faster
    public has(key: Key): boolean {
        this.find(key) !== this.data_.end();
    }

    public get(key: Key): T | undefined {
        const it: List.Node<Entry<Key, T>> = this.find(key);
        return it !== this.data_.end() 
            ? it.value.value 
            : undefined;
    }

    // However, full iteration is slower as well as linked list
    public entries(callback: (value: [Key, T]) => void): void {
        let it: List.Node<Entry<Key, T>> = this.data_.begin();
        while (it !== this.data_.end()) {
            callback([it.value.key, it.value.value]);
            it = it.next();
        }
    }

    public size(): number { return this.data_.size(); }
}

export class List<T> {
    private begin_: List<T>; 
    private end_: List<T>;
    private size_: number;

    public size(): number;
    public begin(): List.Node<T>;
    public end(): List.Node<T>;
}
export namespace List {
    export class Node<T> {
        public next: Node<T> | null;
        public prev: Node<T> | null;
        public value: T;
    }
}

顺便说一句,您知道如何实现HashMap吗? HashMap将其键值对存储到Linked List(C ++ STL Case中的双连接列表)中。另外,对于快速钥匙发现,它将Linked List的节点存储到Bucket数组中。

这就是为什么HashMap使动态语言较慢的原因。如您所知,静态语言将对象属性包含在固定和测序的内存空间中,例如Array。比较Linked ListArray的迭代速度,哪个更快?当然,Array要快得多。这是动态语言比静态语言慢的主要原因之一。

  • 动态语言将属性存储到Linked List
  • 静态语言将属性存储到Array(测序的内存空间)

如果我的解释不容易理解,请阅读上面的伪代码。这不是HashMapLinked List的实际实现,而是了解该概念的帮助。

隐藏的类优化

Hidden Class Optimization

现在,我们已经了解到动态语言正在利用HashMap进行动态密钥组成,但是由于其存储Linked List,它使程序较慢。顺便说一句,V8引擎具有针对此问题的特殊优化技术。

当V8引擎检测具有固定属性组成的对象类型时,它为对象类型提供了隐藏的类定义。而且,隐藏的类定义不是使用HashMap,而是将属性安排到固定和测序的内存空间(如静态语言)中。

这称为“隐藏类优化”。如果您听说Google Chrome或nodejs比其他动态或Java(例如Java)快得多,可以假设隐藏的类优化是主要原因之一。

结论

让我们总结一下我们在此处学到的文章中所学的内容:

  1. 动态语言正在使用HashMap来支持动态属性的插入和删除。
  2. HashMap将其键值对存储到Linked List容器中,并使rpgoram慢慢
  3. 此外,静态语言将属性存储到像Array一样的固定和测序的内存空间中,它使程序更快
  4. 在V8引擎案例中,它具有一种称为“隐藏类优化”的特殊优化技术,可以从HashMap逃脱并将属性存储到固定和测序的内存空间(如静态语言)

typiaclass-validator快的原因与上面总结的理论内容也没有太大不同。我已经设计了typia来生成有利于V8引擎的代码,实际上,由typia生成的代码受益于隐藏的类优化。

此外,class-validatorclass-transform始终通过动态属性的访问来构成其序列化和验证逻辑,尤其是由Reflect表示。因此,class-validatorclass-transform永远无法从隐藏的类优化中受益,并且始终迭代慢速Linked List节点。

20,000x速度差距也来自这种差异。通常,数组和链接列表的遍历速度大约相差10。因为class-validatorclass-transformer具有双重到四倍的嵌套到动态密钥访问的迭代,因此10x间隙可为最大值(10 4 = 10,000)x理论上。