V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
James369
V2EX  ›  程序员

请教一个 CPU 密集计算多线程优化办法?

  •  
  •   James369 · 2021-09-28 10:43:57 +08:00 · 3539 次点击
    这是一个创建于 1160 天前的主题,其中的信息可能已经有所发展或是发生改变。

    我在写一个 js 前端项目的时候,遇到一个比较耗时的操作 task,它基本结构是一个 2 层的 for 循环计算,task 大体长这样:

    function task(x: Int) {
      ... 
      for(var i = 1...10000) {
        for(var j = 1...10000) {
          ...计算数据(与参数 x 相关)
        }
      }
      ...
    }
    

    这个 task 在点击按钮时触发,要耗时大概 3 秒左右,为了不阻塞 UI 界面。我想开子线程去优化。那么开子线程能否做到以下几点:

    1. 不管用户如何快速的点击按钮,这个 task 子线程始终保持在 1 ~ 2 个(避免开启过多线程)
    2. 当用户点击按钮时,如果已有 task 未执行完毕,可以先终止或跳出原 task,再执行新 task 。
    3. 假如在开启子线程执行 task 时,需要再传入一个大的 Object (例如 task(x: Int, big: Object)),那么这个 Object 要不要做多线程同步,如何做?

    第 3 点如果做不到,那就放弃第 3 点。

    第 1 条附言  ·  2021-09-28 11:17:33 +08:00
    关于第 3 点传入 bigObject 参数的问题,做个补充说明:可以降级不做多线程同步了。
    因为经过分析,这个 task 的计算只用到 bigObject 中的若干个方法,并不影响到 bigObject 的内部状态,也不受 bigObject 内部状态的影响。
    43 条回复    2021-10-16 21:34:45 +08:00
    wellsc
        1
    wellsc  
       2021-09-28 10:46:22 +08:00   ❤️ 3
    js 有多线程吗
    James369
        2
    James369  
    OP
       2021-09-28 10:50:06 +08:00
    @wellsc 所以我来问一下 js 的实现技术栈,其实这 3 点在其它语言中都很好实现,但是不知道 js 怎么做?
    typetraits
        3
    typetraits  
       2021-09-28 10:50:39 +08:00   ❤️ 1
    启动 Service Worker,把任务放进 worker 执行,然后发送消息回主线程进行确认
    wellsc
        4
    wellsc  
       2021-09-28 10:52:20 +08:00 via iPhone
    @James369 js 就不太适合计算密集型,它的适用场景更偏向 io 密集
    James369
        5
    James369  
    OP
       2021-09-28 10:55:09 +08:00
    @wellsc 现在就有这方面的需求,并且还不方便放到后端去计算。
    Huelse
        6
    Huelse  
       2021-09-28 11:07:57 +08:00
    这需求就不适合 js 做,js 也没真多线程,一定要做的话考虑下 WebAssembly 吧
    James369
        7
    James369  
    OP
       2021-09-28 11:12:57 +08:00
    @Huelse 我不信,js 都要一统江湖了
    Building
        8
    Building  
       2021-09-28 11:19:53 +08:00 via iPhone
    js 没有多线程,可以试试开一个 iframe 专门帮你做计算。
    zhangxh1023
        9
    zhangxh1023  
       2021-09-28 11:21:27 +08:00
    前端遇到这种问题,大多数情况下都是用一个 loading 遮罩层,遮盖掉整个页面的。
    或者看看能否从需求上砍砍,利用类似于懒加载的方式,按需求计算。
    James369
        10
    James369  
    OP
       2021-09-28 11:24:13 +08:00
    @Building 奇怪,前端怎么这么弱 了? 那上面有 v 友提到的 service worker 或者 web worker 能否实现这个需求呢?
    fengjianxinghun
        11
    fengjianxinghun  
       2021-09-28 11:27:17 +08:00
    试试 webgpu, 如果只是数值计算转到 webgpu 太合适了。
    fengjianxinghun
        12
    fengjianxinghun  
       2021-09-28 11:28:16 +08:00
    webgpu compute shader
    libook
        13
    libook  
       2021-09-28 11:29:18 +08:00   ❤️ 3
    @wellsc #1 搜一下 Web Workers,Web API 现在有挺多花活,以前很多 JS 干不了的事情现在都能干了,当然不同引擎的具体实现会有差异。

    @James369 我没怎么用过 Workers,提供几个点子看看是否可行。
    1. 主线程创建 worker 可以把 worker 引用放到一个数组里,然后每次用户点击按钮都检测这个数组有几个元素,销毁 worker 就减少一个元素,创建 worker 就增加一个元素。
    2. 主线程每次需要终止一个 worker 的时候就向这个 worker 发送消息,worker 可以监听 message 事件,收到终止消息就改一个局部变量,然后每次循环的时候都检查这个变量,检查到终止信号就退出循环并做善后处理,直至整个 worker 执行完退出。不需要善后处理的话可以直接用 Worker.terminate()强杀。
    3. Object 是静态的话可以写死在源代码里,如果是动态的,貌似现成之间只能通过 message 来传递数据。如果你需要多个线程同步数据变化,就是分布式一致性的问题了,这个问题可能也能解决,只不过比较复杂,而且如果经由主线程的交换多了,主线程就会成为瓶颈,反而没必要用多线程了。

    你可以去 MDN 上看看 Worker API 的文档,没准能提供些灵感。

    另外可以跳出现在的需求,从架构和算法上看看有没有其他优化方法,比如是不是每次都要处理全量数据。
    cheng6563
        14
    cheng6563  
       2021-09-28 11:31:37 +08:00
    就算是 nodejs 做后台服务器也是用多进程解决问题的
    wellsc
        15
    wellsc  
       2021-09-28 11:31:49 +08:00 via iPhone
    @libook 是可以用,客户端的东西,就是怕用户体验不好🐶
    FrameJack
        16
    FrameJack  
       2021-09-28 11:34:44 +08:00
    GPGPU 了解一下
    lisongeee
        17
    lisongeee  
       2021-09-28 11:35:05 +08:00
    不阻塞 UI 界面,你可以将任务分片,类似 react fiber
    libook
        18
    libook  
       2021-09-28 11:40:32 +08:00
    CPU 密集型也不一定 WebAssembly 就能解决,目前 V8 的很多计算效率已经被优化很好了(乌龟绑火箭,不是说着玩的),有时候上了 Wasm 也没有明显的性能提升,反而加大了开发成本。

    具体要看你的计算是不是 JS 所不擅长的,比如通过一些数据结构和内存上的奇淫技巧来大幅优化计算效率,这种用 Wasm 可能会有明显的性能提升。
    crackhopper
        19
    crackhopper  
       2021-09-28 11:45:09 +08:00
    1. 不适合在前端做,为啥不放后端。
    2. 不适合用 js 做,在后端了,反正怎么搞都有办法。
    James369
        20
    James369  
    OP
       2021-09-28 11:50:11 +08:00
    @fengjianxinghun 应该还用不到 GPU 这么重型的武器吧,并且也不是所有的客户机都有 GPU
    James369
        21
    James369  
    OP
       2021-09-28 11:50:34 +08:00
    @libook 你说的方法可行性很高,我下午仔细研究一下
    oldshensheep
        22
    oldshensheep  
       2021-09-28 12:17:41 +08:00   ❤️ 1
    @James369 客户机没有 GPU ?手机电脑哪个没有 GPU 。
    解决方案

    1 、Worker
    Worker 我用过,写了一个数字图像处理的东西 https://blog.oldshensheep.com/toyweb/dip,之前是没有用 Worker 的,因为我这个一打开网页就要做大量运算所以就非常卡,后来用 Worke 用了多线程就不卡了。

    2 、GPGPU https://github.com/gpujs/gpu.js
    GPGPU 这个没用过但是这个计算速度非常快,具体原因大家都懂。但是我没有用过。不过我还又写了一个 WebGL 版本的数字图像处理的网页,和 GPGPU 差不多都是通过 gpu 运算。

    总之追求极致速度如果能用 GPGPU 的化可以试试,对算法有要求。要不用 Worker 吧。

    Wasm 不太了解就不多说了。
    duan602728596
        23
    duan602728596  
       2021-09-28 12:21:50 +08:00
    2021 年了,js 没有多线程?
    hronro
        24
    hronro  
       2021-09-28 12:44:58 +08:00   ❤️ 3
    楼上太多误导信息了,还什么 JS 没有多线程,这都 2021 年了,对 JS 还有这么多偏见。

    JS 的多线程就是 Web Workers,这个东西 10 年前的 IE 10 都支持,压根就不是什么新东西。

    多线程之间的数据共享可以用 SharedArrayBuffer 和 Atomics 来实现。

    具体可以看看 MDN 文档:

    https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer
    https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Atomics
    mtdhllf
        25
    mtdhllf  
       2021-09-28 14:59:44 +08:00
    就这点需求,搞个队列控制一下并发就行了
    fortunezhang
        26
    fortunezhang  
       2021-09-28 15:17:40 +08:00
    之前做红绿灯数据的时候,nodejs 后端,也遇到了 cpu 密集型计算。用的 redis 缓存+文件缓存。
    xiaojie668329
        27
    xiaojie668329  
       2021-09-28 18:19:43 +08:00 via iPhone
    1. wasm 大概可以加快计算速度 2. web worker 3. 只是不想卡 UI 而不在意什么时候完成的话可以进行任务分片,类似楼上说到的 react fiber 。
    mxT52CRuqR6o5
        28
    mxT52CRuqR6o5  
       2021-09-28 18:40:06 +08:00
    具体怎么优化得结合具体业务的,也就是你代码里的...计算数据到底计算了些啥
    wasm 其实是最没用的,因为 chrome 里 wasm 的和 js jit 用的同一个后端,也就是你 js 写的好一点和 wasm 的运行效率是一个级别的
    webworker 主要就是要注意主线程和 worker 线程之间传输数据是有序列化和反序列化的消耗的,用不好可能会更慢
    gpu 得是那种能并行的特殊计算
    甚至于 webworker/gpu 都可以不用说不定也有办法解决,你可以再计算一定量数据后让浏览器休息一会,让浏览器响应用户操作,就像 react 的 concurrent mode 那样
    secondwtq
        29
    secondwtq  
       2021-09-28 22:40:09 +08:00
    @mxT52CRuqR6o5
    > 因为 chrome 里 wasm 的和 js jit 用的同一个后端
    来源请求
    mxT52CRuqR6o5
        30
    mxT52CRuqR6o5  
       2021-09-29 10:23:39 +08:00
    @secondwtq
    https://www.zhihu.com/question/442315024/answer/1709977919
    第四段
    大家有点太瞧不起 v8 引擎的 jit 优化了
    secondwtq
        31
    secondwtq  
       2021-10-10 05:51:19 +08:00
    @mxT52CRuqR6o5
    这个答案说的是“如果你的应用不是非常计算密集”,“加上 js 和 wasm 之间通信的额外消耗”,并且不“使用其他的高级 的 wasm 特性”,#28 直接就说“wasm 其实是最没用的”是不是太快了点?

    另外,“chrome 里 wasm 的和 js jit 用的同一个后端”其实是很正常并且意料之中的事情,但是“因为‘chrome 里 wasm 的和 js jit 用的同一个后端’,所以‘js 写的好一点和 wasm 的运行效率是一个级别的’”这个逻辑是不通的,这一点原来的答案犯了错误。我理解原答主是想 debunk WASM 的 myth,但是这个部分是纯粹的 misinformation 。总体来说答案本身质量一般(不仅存在逻辑错误,也缺乏足够的数据和细节),但是里面的链接好东西不少,这个等会细嗦。

    首先,性能是个非常复杂的问题,而编译器只是其中的一环,不能简单以粗暴的编译器比较来确定性能。几个例子:
    * Rust,Julia 的官方实现都使用 LLVM 作为后端,而 Haskell 也有个 LLVM 后端,那么能说 Rust,Julia 和 Haskell (使用 LLVM 后端)的性能是一样的么?更直接的一个例子是,Safari 的 JS 引擎 JavaScriptCore ( JSC ),以前有个 FTL 模块,就是把 JSC 接入 LLVM 后端,那么能说 JavaScript (在 JSC 引擎上)和 Rust 的性能是一样的么?
    * GCC 和 Clang 使用了完全不同的后端实现( GCC 是自己写的后端,Clang 也是 LLVM ),然而总体性能却差不多。
    * 编译器本身就是个非常复杂的问题,就算限定到 WebAssembly + LLVM 的组合,https://00f.net/2021/02/22/webassembly-runtimes-benchmarks#battle-of-the-llvms 这里有一个不同 WASM runtime 的 benchmark,其中的“Battle of the LLVMs”一节展示了若干个不同的基于 LLVM 的 runtime 的测试,可见同样是 LLVM 后端,性能也存在不小的差异。

    “因为编译器后端一样,所以性能一样”这个逻辑的问题是,如果从编译器的角度考虑性能问题的话,简单来说分三个部分,第一个是给编译器的输入,第二个是编译器本身,在传统编译器中,这两个唯一确定了编译器的输出( JIT 编译器需要加上一些运行时数据,不过简单起见可以认为已经跑到了最优化的程度),第三个是编译器的目标(目标硬件,目标语言+编译器的组合,或目标 VM )。
    对于 V8 引擎这个问题来说,编译器本身不变,编译器目标无法控制,所以主要就在输入上。也就是要想做到性能一样,就得做到给编译器的输入一样。而做到这一点很难,最根本原因是 JavaScript 本身并不是适合写高性能应用的语言,WASM 则更多地考虑了性能。
    举个例子:很多编程语言的 primitive value 涉及到了 boxing,而 boxing 会影响性能。编译器通过优化,可能能把很多 boxing 优化掉,从而在这一方面做到和没有 boxing 的语言类似的性能,但是第一编译器不保证一定有 boxing 的优化,第二就算编译器有 boxing 优化,也不保证一定能把每个 boxing 优化掉。而如果一开始就使用没有 boxing 的语言,就根本不存在这一问题,一定是保证没有 boxing 发生的。
    这只是一个简单的例子。很多语言,特别是 Tracing GC 语言中,类似的情况很多。而我刚才说过编译器是个非常复杂的东西,有可能有哪个条件没有满足,优化就不会发生。现在人类的高质量 JIT 优化编译器,比如 V8,可能确实能优化掉大部分的 boxing,但是只要 hot code 中恰好有一个地方没有优化掉,性能就下去了。而就算简单的情况能够优化,复杂的情况可能也难以优化。(另外,编译器优化复杂性的来源之一是,一个优化经常会依赖于之前的优化,也就是“前面的优化为后面的优化创造更多的机会”,而一步错了就意味着后面的相关优化都会受到影响)
    V8 在优化 JS 方面确实很强,但这也意味着它优化 WASM 并不会更差,也就是 WASM 至少能提供更强的保证。WASM 给编译器的输入通常是比 JS 更优质的输入。更多需要考虑的是 JS 和 WASM 交互的 overhead 之类的问题。

    说白了,没人瞧不起 V8,瞧不起的是,并且只是 JavaScript 罢了。
    这也是编译器开发者的无奈——编译器开发者对自己的产品有完全的自由,然而对其输入和输出却缺乏控制力,输入是语言标准定好的,输出是硬件 ISA 标准定好的,夹在中间两头受罪。
    不过现在问题并不这么简单,因为很多编程语言标准委员会的成员,自己就是该语言编译器的开发者,也就是说*某些*编译器开发者是有能力影响编程语言标准的。同样地,通过某些渠道(比如处于同一公司,或者通过各种公开或私下的反馈),编译器开发者也可以影响硬件,虽然流程可能会复杂很多?这一定程度上改变了编译器尴尬的定位,但是没有改变编译器本身的性能高度依赖于输入和输出语言的表达能力的根本逻辑——编译器开发者为了提高编译器性能而试图改变编程语言和硬件的设计这一现象,反而恰恰证明了该逻辑的正确性。
    一个典型例子是 ES5 的 use strict,这里 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Strict_mode 提到了该功能“Fixes mistakes that make it difficult for JavaScript engines to perform optimizations”,ES5 fix 了一些,不代表 ES5 没 fix 掉的就不需要 fix 了。
    更具体的我在 https://v2ex.com/t/632869#r_8401400 讨论过一些。

    当然,上面只是纸上谈兵,没有实际的数据,这里有几个例子:

    首先是 WASM 的前身,asm.js 和 WASM 的对比:
    www.usenix.org/system/files/atc19-jangda.pdf Not So Fast: Analyzing the Performance of WebAssembly vs. Native Code 这篇文章把 SPEC CPU benchmark,使用 Emscripten 分别编译到 asm.js 和 WASM,在 Chrome 和 Firefox 两个浏览器上跑,同时结合 native 运行测试进行对比,结论是 WASM 版本的运行时间平均是 native 的 1.5-2 倍,而 asm.js 版本的运行时间平均是 WASM 的 1.3-1.5 倍。
    pspdfkit.com/blog/2018/a-real-world-webassembly-benchmark A Real-World WebAssembly Benchmark 这个则是把 PSPDFKit 这个 native 库,编译到 WebAssembly 和 asm.js (这个貌似有一定 impact,各大浏览器后来都针对这玩意做了一定优化,V8 更是直接在 blog 中引用了这个 benchmark ),然后再做 benchmark,这个可以直接在线跑。这个 workload 的结果貌似是,直到目前为止,Chrome 上 WebAssembly 和 asm.js 之间的性能并没有拉开明显差距。

    有意思的也就在这:注意这两个例子都是 asm.js 和 WASM 的对比,这两者本质上其实都是差不多的东西(就是作为其他语言的目标 IR )。这并非我们想要看到的,WASM 与所谓“写的好一点”的 JS (即大多数 JavaScript 原生程序所实际使用的语言)的对比——asm.js 虽然看上去是 JS,但是压根就不是人写的 JS,而是精简掉 JS 中大量对性能不友好的特性之后的子集。特别是 PSPDFKit 这个狗东西,它占了 “iswebassemblyfastyet.com”,并且 claim 自己是在 WebAssembly,和 “JavaScript version of PSPDFKit”之间做对比。这对于我们要讨论的问题来说是有些 misleading 的。
    但是,从 SPEC CPU 的结果中可以发现,哪怕是类似的两个东西(有理由相信 V8 中 WebAssembly 和 asm.js 用的也是“同一个后端”),性能同样可以有不小的差别,这就是因为编译器的输入不一样。
    secondwtq
        32
    secondwtq  
       2021-10-10 05:51:35 +08:00
    那么有没有正经的,“WASM 与所谓‘写的好一点’的 js”之间的对比呢?
    www.sable.mcgill.ca/publications/techreports/2018-2/techrep.pdf WebAssembly and JavaScript Challenge: Numerical program performance using modern browser technologies and devices 这篇文章对比了同一套数值计算 benchmark 在 C 、WebAssembly 和 JavaScript 等不同平台下的性能,结果性能提升普遍在 1.5 到 2 倍之间。
    当然,可以 argue 这个 benchmark 里面的 JS 并不是“最优化”的 JS (这个 JS 代码是他们重写过的,用了 Typed Array 之类的优化),真正“最优化”能做到什么程度呢?

    这就要说回刚才那个答案了,答案回复里引用了一篇文章,干货比答案本身那是多得多: https://zhuanlan.zhihu.com/p/102692865 一个白学家眼里的 WebAssembly (以下称为“白学文章”,因为实在是浓度过高 ...)
    这个文章声称:“只要你懂得如何让 JS 引擎走在 Happy Path 上,那么在浏览器里,JS 就敢和 Rust 五五开。”
    这里有个前提“JS 引擎走在 Happy Path 上”,这个的难度我前面说过。另外,在这篇文章的上下文中,Rust to WebAssembly 貌似无论是在文件大小、工具链完善程度和运行时性能还是上都没有 C to WebAssembly 更好,也就是换 C 可能就是四六开了 :)
    精彩的是这个 claim 后面的证据,这是来自 Firefox 和 V8 两家浏览器引擎官方的内卷成果:

    https://hacks.mozilla.org/2018/01/oxidizing-source-maps-with-rust-and-webassembly Oxidizing Source Maps with Rust and WebAssembly 这个作者 Nick Fitzgerald 应该是专门搞 Rust 和 WASM 的,立场应该有一定偏向。这篇文章是讲通过把 source-map 这个包里的 hot code 改成 使用 Rust 编译成的 WebAssembly 实现,在作者定义的一组 benchmark 上实现了成倍的性能提升。文章不短,不过主要是介绍 source-map 这个库,介绍 WASM 以及怎么用,怎么做 WASM 和 JS 之间的 binding,以及 benchmark 的结果。
    前 V8 开发者 Vyacheslav Egorov 看了之后就写了一篇 https://mrale.ph/blog/2018/02/03/maybe-you-dont-need-rust-to-speed-up-your-js.html Maybe you don't need Rust and WASM to speed up your JS,他声称自己不站队,只是对优化感兴趣。这篇文章同样不短,全文大部分都是关于他怎么做的优化,包括:
    1. 通过使用一个“*almost* default x64.release build of the V8”——也就是他重新 build 了一个 V8 来方便自己做分析,对 V8 引擎进行 profile,发现函数调用的时候 arity 不匹配或导致性能损失,改掉之后提升了一些性能。
    2. 通过手动进行 monomorphisation,提升了更多的性能。
    3. 通过分析发现原来包里中加入的 cache 并没有必要,删掉之后又提升了一截性能。
    4. 把原来包里的内置排序算法改成桶排序,又是一截。类似的还有去除不必要的排序,并把快排换成插入排序。
    5. 通过手动模拟静态类型语言的内存布局,优化 GC 。
    6. 使用 Typed Array 继续优化(这个貌似需要重写整个逻辑,作者没有真的去重写只是预测了一下性能)。
    做完这一切之后——V8 下性能差不多是刚才的 WASM 版本的 1.3-1.6 倍,而 SpiderMonkey ( Firefox 的 JS 引擎)下面的性能差距要小一点。
    注意这六个主要优化中,1 是 V8-specific 的,这需要你了解 V8 的实现细节,3 和 4 是对程序算法本身的优化,和使用什么语言没有关系。2,5,6 则是 C/C++/Rust 等静态类型语言的语言特性自身就有的,而为了在 JS 中“模拟”这些特性,你需要这么写代码: https://mrale.ph/blog/2018/02/03/maybe-you-dont-need-rust-to-speed-up-your-js.html#optimizing-parsing---reducing-gc-pressure
    之后 Nick Fitzgerald 又单独发了一篇文章: https://fitzgeraldnick.com/2018/02/26/speed-without-wizardry.html Speed Without Wizardry 先吹了一下 Vyacheslav 的 expertise 和文章本身的质量,然后解释了下啥叫“wizardry”,最后把上面的 3 和 4 也在 WASM 版里面实现了,最后结果就是那个白学文章里面那个图:JS 和 WASM 在 V8 上完全打平,而在 SpiderMonkey 上,WASM 依然有明显的性能优势。

    Nick 最后那篇文章有点阴阳怪气的意思,简单来说:但是,古尔丹,代价是什么呢?
    标题的意思就是 Vyacheslav 的优化有一半属于“wizardry”,而使用 WASM 则完全不需要。他还特地摘出了 GC 优化那段,用 Vyacheslav 自己的话(“verbose and error prone code”)嘲讽了一番,然后大大的字体把 Vyacheslav 的代码 “memory[mappingA + 3] - memory[mappingB + 3]” “memory[mappingA + 4] - memory[mappingB + 4]” ... 贴出来。
    (当然,他没有强调这代码之所以写成这德行,部分原因是照顾 SpiderMonkey 优化的一些短板)
    他最后说:
    > But a distinction between JavaScript and Rust+WebAssembly emerges when we consider the effort required to *attain* inlining and monomorphization, or to avoid allocations. Rust lets us explicitly state our desires to the compiler, we can rely on the optimizations occurring, and Rust’s natural idioms guide us towards fast code, so we don’t have to be performance wizards to get fast code. In JavaScript, on the other hand, we must communicate with oblique incantations to match each JavaScript engine’s JIT’s heuristics.
    并且他同意 Vyacheslav 说的:
    > Obviously each developer and each team are free to choose between spending N rigorous hours profiling and reading and thinking about their JavaScript code, or to spend M hours rewriting their stuff in a language X.
    实际上,Vyacheslav 所做的优化,已经接近于重写代码了,我不认为这是“写的好一点”的问题。
    这些具体的问题,那篇白学文章并没有细讲,白学文章自己推导出一个结论,就是“WASM 主要是方便把更复杂的原生应用直接搬进 Web”。其实完全不需要,WASM 自己的官方 paper (来自各大厂商的一线实现者) dl.acm.org/doi/pdf/10.1145/3062341.3062363 Bringing the Web up to Speed with WebAssembly 就明确指出了
    > The initial version of WebAssembly presented here focuses on supporting low-level code, specifically compiled from C/C++.
    也就是设计上就不是让你从 JS port 过去的。之后才是
    > A highly important goal is to provide access to the advanced and highly tuned garbage collectors that are built into all Web browsers, thus eliminating the main shortcoming relative to JavaScript when compiling to the Web.

    而在折腾了这么半天之后,JS 版在 SpiderMonkey 上终究还是没打过 WASM 。吊诡的是哪怕 JS 版的性能,貌似 V8 也没打过 SpiderMonkey,证明 V8 并非是上天入地无所不能天下无敌的神器。
    Vyacheslav 还有一些观点 Nick 并没有提:
    * 他认为像上述 1 一样的 V8 的坑会越来越少。我个人对此不是很乐观,主要是在工具层面,而不是语言层面解决,依然是治标不治本的“wizardry”。
    * 当然他也提到了从“language features”和“tools”层面一起解决。因为他也强调“Language and Optimizations Must Be Friends”,不是什么语言都可以随便优化的。
    * 而其他的优化是“universal knowledge that spans across implementations and languages”,其中算法优化自不必说,而至于 JS 和 WASM 的对比,他没有给结论,只是用“N”和“M”一笔带过。并且强调“你都可以选择”,以及“language designers and implementors”都要加油奥利给。
    * 以及“Clever Optimizations Must be Diagnosable"——对于 JS 引擎自身的 magic,浏览器并没有提供任何易用的接口,开发者基本没法知道我这个代码是不是什么地方惹 JS 引擎不爽了——应该不是每个前端开发者都会自己 build 一个 V8 的吧。这方面传统编译器反而做得要好得多,以 Clang 为例,最简单地,用户可以生成 optimization report 查看重要的优化有哪些做了哪些没做,再进一步可以查看生成的 LLVM IR 和 汇编,还可以看 IR 在每一个 pass 中的变化。GCC 和 Clang 传统编译器通常还会加入大量的 option 和 extension 方便性能优化。

    这个涉及另外一个问题,就是通常 WASM 就是由某种形式的“传统编译器”生成的,这个编译器一般自身就是一个优化编译器。也就是说某种程度上讲,WASM 其实在编译器层面比 JS 更优越——它既有 V8 的婆罗门优化,又有传统编译器的刹帝利优化。之前说到 JIT 优化的不确定性,类似的问题在传统编译器中同样存在,区别是总的来说传统编译器做优化一般更容易,开发者可以访问更多优化的信息,并且实际性能受运行时情况的影响更小。总的来说 WASM 依然能够提供更好的 predictability 。

    最后,在 V8 中,JS 和 WASM 都是通过 TurboFan 后端编译,但是在 SpiderMonkey 中,情况也是一样的。SpiderMonkey 的资料更少,但是其官方文档 https://firefox-source-docs.mozilla.org/js/index.html 说明,SpiderMonkey 最高级的 JS 后端,原来叫 IonMonkey,后来叫 WarpMonkey (最近改的名,其实 SpiderMonkey 这么多年已经养了一堆的猴儿了),但是 IR 还是叫 Ion MIR 和 Ion LIR 。而其最高级的 WASM 后端叫 BaldrMonkey,或 WASM-Ion,是“translates the WASM input into same MIR form that WarpMonkey uses and uses the IonBackend to optimize”。“IonBackend”具体指代什么不明,但是 IR 是一样的 IR 就基本是一样的后端。在 SpiderMonkey 的 WASM 实现源码中( https://searchfox.org/mozilla-central/source/js/src/wasm/WasmIonCompile.cpp#5824 )一些关键模块,如 js::jit::OptimizeMIR 同样在 JS 编译时使用( https://searchfox.org/mozilla-central/source/js/src/jit/Ion.cpp#1577 )。https://github.com/bytecodealliance/wasmtime/blob/main/cranelift/spidermonkey.md 这里明确说 SpiderMonkey 中 WebAssembly 使用“IonMonkey-based tier 2 compiler”。前面提到的 WASM 的 paper,也包含了实现部分,是这么说的:
    > V8 (the JavaScript engine in Google’s Chrome), SpiderMonkey (the JavaScript engine in Mozilla’s Firefox) and JavaScriptCore (the JavaScript engine in WebKit) reuse their optimizing JIT compilers to compile modules ahead-of-time before instantiation.
    > The baseline JIT is designed only for fast startup while the Ion optimizing JIT is compiling the module in parallel in the background. The Ion JIT is also used by SpiderMonkey as its top tier for JavaScript.
    > V8, SpiderMonkey, JavaScriptCore, and Chakra all include optimizing JITs for their top tier execution of JavaScript and reuse them for maximum peak performance of WebAssembly.
    > Our experience reusing the advanced JITs from 4 different JavaScript engines has been a resounding success, allowing all engines to achieve high performance in a short time.
    其实并不难理解,编译器本身差别并不大,既然已经有了一个非常不错的编译器,就没必要再写一个了。

    最有意思的是把这个和之前的测试结果结合起来看:SpiderMonkey 同样给 JS 和 WASM 使用了一样的后端,然而这俩的性能确实差出了一截,可见一个语言的性能和编译器后端是啥根本就没有必然联系。
    mxT52CRuqR6o5
        33
    mxT52CRuqR6o5  
       2021-10-10 17:11:13 +08:00 via Android
    @secondwtq 我就算 wasm 能有 js 两倍的效率,对于解决楼主问题也是远远不够的
    Fiber 、web worker 、wasm 里,wasm 是离解决楼主问题目标最远的那个
    mxT52CRuqR6o5
        34
    mxT52CRuqR6o5  
       2021-10-10 17:15:38 +08:00 via Android
    @secondwtq 当 js 出性能问题的时候,我认为基本不用太去往 wasm 方向考虑,wasm 更多的意义在于能使用其他需要的生态甚至以后出裸 wasm 的 runtime 自成一个生态对标 jvm
    secondwtq
        35
    secondwtq  
       2021-10-10 17:40:51 +08:00
    @mxT52CRuqR6o5
    第一我针对的是”因为后端一样,所以性能一样”的逻辑
    第二,现在 Chrome 和 Firefox 都支持了 WASM 的多线程和 SIMD,楼主想做的不一定不能做。白学文章的结论是“多了一种在性能、开发成本和效果之间权衡的选择”,你这“基本不用”,我也不好说😅
    mxT52CRuqR6o5
        36
    mxT52CRuqR6o5  
       2021-10-10 23:09:24 +08:00 via Android
    @secondwtq 我用的措辞不是"一样",而是"一个级别"
    也就多线程和 simd 且可以保证用户一定能使用支持新特性的浏览器的场景下 wasm 相比 js 能有超出一个级别性能的可能性,这种场景肯定是占少数,"基本不用"我不认为有什么问题,相比 wasm 多线程和 simd 应优先考虑 webworker 和 gpu.js 这种东西
    mxT52CRuqR6o5
        37
    mxT52CRuqR6o5  
       2021-10-10 23:10:28 +08:00 via Android
    @secondwtq 你的观点就属于手里有个锤子。看什么都像钉子想拿锤子来敲
    secondwtq
        38
    secondwtq  
       2021-10-12 01:08:53 +08:00
    @mxT52CRuqR6o5 “少数“不等于“基本不用“。
    secondwtq
        39
    secondwtq  
       2021-10-12 01:14:29 +08:00
    @mxT52CRuqR6o5 另外我很好奇你说的“一个级别”是什么定义。
    我个人对”一个级别”的定义是 2 倍。但是隔壁组的工作是用几个月甚至半年做几个百分点的提升。这远小于我定义的“一个级别“的差距,然而人家还是在做。
    mxT52CRuqR6o5
        40
    mxT52CRuqR6o5  
       2021-10-12 09:02:27 +08:00 via Android
    @secondwtq 起码 5 倍差距以上吧,不然 O(n)和 O(2n)都不算一个级别了
    底层的东西性能提升几个百分点是很有价值的,但业务层的东西就不是了
    至于少到多少用“基本不用”才算准确,你这纯属抠字眼,我不想跟你讨论这个问题
    mxT52CRuqR6o5
        41
    mxT52CRuqR6o5  
       2021-10-12 11:36:53 +08:00
    @secondwtq
    我的论点重点在于解决 js 性能问题时,相比 fiber 、webworker 、gpu.js 相比,wasm 考虑优先级要低
    你搁这使劲跟我掰扯“一个级别”“基本不用”的用词含义我也不知道是几个意思
    secondwtq
        42
    secondwtq  
       2021-10-16 20:53:50 +08:00
    @mxT52CRuqR6o5

    > 起码 5 倍差距以上吧,不然 O(n)和 O(2n)都不算一个级别了
    你都加 大 O 了常数项还有意义么 ...

    > “一个级别”“基本不用”的用词含义我也不知道是几个意思

    我的意思是
    1. “因为后端一样,所以性能一样(或者一个级别)”的逻辑是错的
    2.
    > 相比 fiber 、webworker 、gpu.js 相比,wasm 考虑优先级要低
    这个比“基本不用”看上去合理一些,我也觉得 WASM 一般相对于这些不是很合适。
    但是我不觉得到了“基本不用”的程度。

    > 底层的东西性能提升几个百分点是很有价值的,但业务层的东西就不是了

    所以
    3. 那也要看什么业务啊。你直接说
    > 相比 fiber 、webworker 、gpu.js 相比,wasm 考虑优先级要低
    不就得了,为啥非要直接一个“基本不用”扣上以至于引起
    > 纯属抠字眼
    > 用词含义
    的误会呢。
    secondwtq
        43
    secondwtq  
       2021-10-16 21:34:45 +08:00
    ***本回复不是给 #28 #30 #33 #34 #36 #37 #40 #41 看的,而是给通过 V 站站内通知、Google 等不同渠道来的有缘人看的***

    不是刻意要“抠字眼”,解释下我为啥以 2 作为普遍的“级别”的粗略定义:

    考虑工资 6k 和工资 8k 的两个人,可以勉强说这两个人的工资是一个级别。工资 6k 和工资 60k 就明显不是了。10 是一般意义上的“数量级”单位。但是在很多情况下这个单位都太大了。工资 12k 相对于工资 6k,已经是“一个级别”的边缘了。同样的规律可以应用在价格上。
    孙子兵法更有意思“十则围之,五则攻之,倍则分之,敌则能战之,少则能逃之,不若则能避之”,,看起来其对于军队数量级差距的定义在 2-5 倍之间,而 10x 在这套标准中已经是顶级的碾压了。
    2 对于程序员来说是比较习惯的选择,我认为也是比较合适的选择。某些时候可能显得有点小,不过无非就是原来的一个数量级变成了现在的 3.3 个数量级。
    意大利有 60M 人口,尼德兰有 17M,这俩也就 3.5 倍的关系,但无论如何不能说是在一个级别上。
    我现在用的显示器是 2560 × 1440 的分辨率,相对于 1920 × 1080 的屏幕是 1.78 倍像素点,而 1920 × 1080 相对于 1280 × 720 是 2.25 倍的像素点,两个比率都在 2 倍“左右”。实际用过的大概知道,这三种分辨率用起来完全不是一个级别的体验。
    一般做性能优化,除去明显的低级错误后,各种 trick 提升的天花板也就是 2x,再往上就要靠并行和程序本身算法优化。但是不考虑实现方式和 KPI 好不好看,作为用户来讲,大量数据占用存储减半,复杂页面加载时间减半,能耗减半,模拟迭代速度速度翻倍,多媒体处理或游戏帧数翻倍,数据传输速率翻倍,这些都是能明显感觉到的东西——实际上对于很多比较成熟的系统做到这些都非常不容易,但是如果做到了,使用体验是超出一个级别的提升。所以我对性能问题的“数量级”定义是低于 2x 的。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2660 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 32ms · UTC 09:58 · PVG 17:58 · LAX 01:58 · JFK 04:58
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.