并行化密集型计算

稍早前对 terser 压缩代码进行优化时将执行过程的代码从 Python 迁移到了 Node.js,准确地说是试图用 gulp stream 来批量压缩,用了一个比较老的包(gulp-terser),已经很久不维护了。与原先相比,底层都是 terser,只是为了在 Node.js 的镜像下使用而统一了文件的提取(glob)和批处理过程(Python Pool & subprocess -> gulp stream)。

但这样处理了以后发现压缩处理时长变长了很多,差不多慢了 1 分钟,研究了一下 gulp 的处理机制,发现虽然 gulp 号称流式处理,但文件依然是按个通过 pipe 的,前一个处理完下一个才能进入,但通过 parallel-transform 也能实现同时处理多个文件,可是实际操作中以 parallel-transform 替代 through2 并不能提升处理效率。笔者并未实际深究其原因,大致读了下 parallel-transform 的实现,它依然是围绕着 event-loop 做文章,设想成一个水池,干涸了就加水,满了就停止并等待处理完毕抬高基准线。

笔者甚至使用了 RxJS 来控制并发 …… 但事实证明还是自己太年轻了,这种密集型计算根本无法通过 event-loop 来优化,它和 ajax 或者 IO 操作不一样,处理数据的仍是这个进程,怎么办?那就多进程/多线程!

问题就变得简单了起来,第一步把文件列表拿到,第二步把它们分发到其他进程/线程处理。

首先想到的是 gulp.src 接受的是数组,而 glob 接收的是字符串,实现肯定不一样,事实证明果然相差甚远。想想看还是需要先拿到本着能不造轮子就不造轮子的思路,去网上搜搜代码,首先想到的是已废弃的 gulp-util。一看里面有个 buffer 符合我们的需要,直接抄来,只不过需要注意一下这里有个 cb 需要塞到 fn 的回调里去,这样才能准确得到 task 的总共执行时间。

1
2
3
4
5
6
7
8
9
var end = function(cb) {
this.push(buf);
--- cb();
--- if(fn) {fn(null, buf);
+++ if (fn) {
+++ fn(null, buf).then(cb)
+++ }
}
};

fn 是个耗时的操作,必须再它完成后再 cb

然后按照 terser 和 child_process 的关键词找到了 @bazel/rules_nodejs - terser 这个包,简直是宝藏 …… 一顿大抄特抄,写死了 terser 执行参数,加了个 Promise 的返回值。上 pre 跑了一下,功德圆满!

快了整整 4.5 倍!

为啥这么快?看了下 CPU 数量,我惊呆了!

48 核???

顺便看了下 issue 里也有试图引入 works_thread,不过 child_process 已经足够香了!

不过 web 上毕竟一个 page 只有一个进程,所以要有 Web Worker 来让密集计算在主线程之外运行,而可预计到的是 Web Worker 和 work_thread 的关系就好比 wasm 和 wasi,激动人心的进展!

并行计算大势所趋

评论