Nodejs在实现中用了哪些奇技淫巧?

1,070 阅读4分钟
原文链接: www.zhihu.com

谢邀,好问题。(最近正在给公司进行容器化,累死我了还要邀请我讲那么烧脑的问题哈哈哈...

奇巧淫技这种东西嘛,我也不知道算不算,但是最近在commit中看到一个,nextTick队列被重新设计。

(文末有彩蛋)

实际上还有挺多的:

  • async hook的设计
  • timer时间轮优化,http date缓存获取
  • 包机制(我觉得挺巧的
  • stream的设计(back press,hwm)等等...

戏说nextTick队列

熟悉 node 事件循环的同学应该知道,我们在一个循环中设置nextTick回调的时候,会在本轮循环的末尾,mirco task前面执行。

事件循环其实要分,你要分为除去libuv提供的 6大阶段之外,还要加上 node.js 本身实现的两个阶段,才构成了完整的事件循环。

那么本轮的最后会执行所有的nexttick,在实际上,我们会大量使用nexttick这个回调(无论是node开发者,还是node使用者)。在最初的版本里,nextick的队列是这样的:

var nexTickCallback = [];

简单粗暴。早期实现中,这并没有什么问题,直到后来这个pr的出现:#13446,这个哥们发现了一个神奇的现象:使用es6构造一个数组,手动添加clear,push,shift等方法,比原生的[]要快接近20%.以下是结果:

improvement confidence      p.value
 process/next-tick-breadth-args.js millions=2     27.75 %        *** 1.271176e-20
 process/next-tick-breadth.js millions=2           7.71 %        *** 4.155765e-13
 process/next-tick-depth-args.js millions=12      47.78 %        *** 4.150674e-52
 process/next-tick-depth.js millions=12           47.32 %        *** 7.742778e-31

那么他给nextTick添加了一个什么代码呢?没有错,他只是简单的使用es6的class自己写了一个function:

class NextTickQueue {
    constructor() {
        this.head = null
        this.tail = null
        this.length = 0
    }

    push(v) {
        const entry = { data: v, next: null }
        if (this.length > 0) this.tail.next = entry
        else this.head = entry
        this.tail = entry
        ++this.length
    }

    shift() {
        if (this.length === 0) return
        const ret = this.head.data
        if (this.length === 1) this.head = this.tail = null
        else this.head = this.head.next
        --this.length
        return ret
    }

    clear() {
        this.head = null
        this.tail = null
        this.length = 0
    }
}

就这样一个修改,性能提升20%!

这个函数是有通用性的,也就是说我们可以运用到现实生活中去优化我们队列的大量操作。为此,我特地写了一份测试函数。得到的结果真是让人兴奋。

使用一个特殊的可重用单向链表去优化速度

又过了一段时间,nextTick的实现再次被踢翻,具体的pr再这里:pr:#18617,这位哥们的做法更加变态:他的思路其实很简单,我们push操作的时候,系统都会申请一块新的空间来存储,清理的时候会将一大块内存都清理掉,那么这样实在是有点浪费,不如一次性申请好一堆内存,push的时候按位置放进去不就完了?于是有了现在的实现:

// 现在的设计变成了这样子:是一个单项链表,每个链表中的元素,都有一个固定为2048长度的数组
  // 如果单次注册回调的次数少于2048次,那么只会一次性分出2048个长度的array提供使用
  //这2048长度的数组中的内存是可以重复使用的
  //
  //  head                                                       tail
  //    |                                                          |
  //    v                                                          v
  // +-----------+ <-----\       +-----------+ <------\         +-----------+
  // |  [null]   |        \----- |   next    |         \------- |   next    |
  // +-----------+               +-----------+                  +-----------+
  // |   tick    | <-- bottom    |   tick    | <-- bottom       |  [empty]  |
  // |   tick    |               |   tick    |                  |  [empty]  |
  // |   tick    |               |   tick    |                  |  [empty]  |
  // |   tick    |               |   tick    |                  |  [empty]  |
  // |   tick    |               |   tick    |       bottom --> |   tick    |
  // |   tick    |               |   tick    |                  |   tick    |
  // |    ...    |               |    ...    |                  |    ...    |
  // |   tick    |               |   tick    |                  |   tick    |
  // |   tick    |               |   tick    |                  |   tick    |
  // |  [empty]  | <-- top       |   tick    |                  |   tick    |
  // |  [empty]  |               |   tick    |                  |   tick    |
  // |  [empty]  |               |   tick    |                  |   tick    |
  // +-----------+               +-----------+ <-- top  top --> +-----------+
  //
  //回调比较少的情况
  //  head   tail                                 head   tail
  //    |     |                                     |     |
  //    v     v                                     v     v
  // +-----------+                               +-----------+
  // |  [null]   |                               |  [null]   |
  // +-----------+                               +-----------+
  // |  [empty]  |                               |   tick    |
  // |  [empty]  |                               |   tick    |
  // |   tick    | <-- bottom            top --> |  [empty]  |
  // |   tick    |                               |  [empty]  |
  // |  [empty]  | <-- top            bottom --> |   tick    |
  // |  [empty]  |                               |   tick    |
  // +-----------+                               +-----------+
  //
  //当往队列中插入一个callback的时候,top就会往下走一个格子
  //当从中取出的时候,bottom也会从中取出一个,如果不为空,则直接返回,
  //调整bottom的位置往下走
  //
  //
  //判断一个表是否满了或者全空非常简单(2048),当top===bottom的时候,
  //list[top] !== undefine 那就是满了
  //会重新生成一个表
  //如果top===bottom && list[top] === void 666
  //那就证明,这个表已经空了

经过这个commits的测试,性能提高40%

confidence improvement accuracy (*)   (**)  (***)
 process/next-tick-breadth-args.js millions=4        ***     40.11 %       ±1.23% ±1.64% ±2.14%
 process/next-tick-breadth.js millions=4             ***      7.16 %       ±3.50% ±4.67% ±6.11%
 process/next-tick-depth-args.js millions=12         ***      5.46 %       ±0.91% ±1.22% ±1.59%
 process/next-tick-depth.js millions=12              ***     23.26 %       ±2.51% ±3.36% ±4.40%
 process/next-tick-exec-args.js millions=5           ***     38.64 %       ±1.16% ±1.55% ±2.01%
 process/next-tick-exec.js millions=5                ***     77.20 %       ±1.63% ±2.18% ±2.88%

Be aware that when doing many comparisions the risk of a false-positive
result increases. In this case there are 6 comparisions, you can thus
expect the following amount of false-positive results:
  0.30 false positives, when considering a   5% risk acceptance (*, **, ***),
  0.06 false positives, when considering a   1% risk acceptance (**, ***),
  0.01 false positives, when considering a 0.1% risk acceptance (***)

总结一下nexttick

其实也不算是什么奇巧淫技,也就是业界中常用的「空间换时间」的做法了。

这个「空间换时间」的做法总结成:

当构建一个复杂javascript对象时,我们可以使用对象池的方式进行对对象的重用,能够大量减少系统压力,虽然说多费一点内存,但是现阶段来说,内存几乎是不值钱的。

出题:

通过这种思想,我们在设计web框架的时候,有一个东西大量创建,这个东西具体是什么呢?嘿嘿嘿嘿嘿....

(我才不说,免得抢我pr)