记一次向WebAssembly的移植:gl-matrix

少女dtysky

世界Skill

时刻2019.06.23

项目地址,欢迎Star(如果你觉得还行):gl-matrix-wasm

由于某些工作上的原因,以及个人兴趣,配合之前用Rust+WASM写软件渲染器的踩坑,端午花了三天将gl-matrix移植到了WASM(但因为各种奇奇怪怪的事情一直没时间发文)。此移植包含gl-matrix的所有功能,同时具备完整的单元测试。库本身以Rust + wasm-bindgen + wasm-pack + webpack4的形式开发,使用TS + Karma + Mocha来写单元测试(当然前两年还能扯一扯“优雅”啥的,现在这些都常识了也没啥说的)。在使用方面,我提供了两种使用模式,来应对不同的场合。同时还使用了很多Trick来在不破坏工程性的同时优化性能,某种意义上可以当做Rust来写WASM的工程的模板。

当然,新技术(或许也不算新了)总是看似很美好,坑却也无限大。具体详细的经历我就不说了(也没空),下面就大概说说一些坑,以及个人认为的WASM的优缺点吧。

坑(缺点)

坑其实和WASM本身关系不是特别大,主要是工具本身的坑,而有的缺点则是WASM目前切实的缺点了:

  1. 功能限制。如果你以为有了Rust就可以使用Rust所有能力快速开发一个WASM项目就太天真了,如果你的项目完全使用Rust编写或许可以,但一旦要和JS端进行交互,那就只能使用Rust的一个不大的子集(没有生命周期,没有trait等等...),函数无法返回引用(全走裸指针然后unsafe吧www要么就全走移动(其实是拷贝))。
  2. 内存管理。没有GC,需要手动内存管理。其实这确实也不是什么坑,但对于大多数被JS惯坏的前端也确实是个问题。而且如果你完全使用wasm-bindgen来托管内存管理(虽然还是要在js端xx.free()释放),那么你对内存的掌握又会下降,这可能会造成一些性能坑(比如在本库中,由于完全托管给Rust创建结构体,比自己在JS端直接操作WebAssembly.Memory实例进行ArrayBuffer的操作要慢的多,当然这个还有#4的问题)。
  3. 灵活性。这个其实是Rust的问题,其实仍然不算问题,只是JS这边确实有时候有的场景过于灵活,而Rust的借用规则由比较严格。举个例子,gl-matrix中经常会有vec3.add(a, a, b)这种使用方式,而这个是无法通过Rust的借用规则的,但它编译到WASM后又没办法在编译期检查,咋整呢——它搞了个运行时检查,这不但会使得逻辑无法实现还会使得性能有所下降(除此之外还有空指针检查)。当然这正是Rust可靠的表现,所以我选择把它黑掉(划掉)。
  4. 工程灵活性。这个主要是Rust工具链的可定制性问题。目前这套流程可以生成wasm + js + ts头的组合,在够用的情况下很完美。但...如果要做一些魔改(比如修改生成的JS来优化性能或自定义功能),就比较难受了,我这边采用的是超级Hack的方法,详见wasm-opt.js的黑魔法。
  5. 互操作开销。Rust本身通过ABI和JS互调用,但这个开销其实还蛮高的,这个可能会在某些状况打消掉WASM本身计算性能的优势。
  6. 异步。不错,目前WASM本身编译是强行异步的,这个可能会对代码组织有些坑(主要是库),如果是工程代码使用Webpack和异步模块其实也不是啥问题。
  7. 性能。这个其实不是WASM的问题,而是Rust工具链的问题(应该是),导致同样代码编译出来比CPP + EMCC编译出来慢两倍左右,具体见Issue 1585。这个问题我还没查明到底是啥(没空),如果有大佬帮忙再感谢不过。
  8. 体积。不错你没看错,同等功能WASM的体积比JS大两倍左右(至少在这个纯粹的库)。

优点

当然WASM本身优点也是有很多的(要不要它干嘛):

  1. 可控。在WASM中,内存对于开发者是完全透明的,它就是一块线性的ArrayBuffer。Rust在其之上帮我们抽象出了堆和栈,帮我们做了一些事情,但其本质还是非常透明的,而精确的内存控制对于游戏这种复杂应用的好处是巨大的。
  2. 可预测。在WASM中,性能是可以预测的。你不用担心GC(The world!)或是JIT带来的迷失感。同一个函数执行100次和1000次稳稳差10倍左右。
  3. 低开销。GPU和内存开销都比JS更低,这个不用多说。
  4. 性能好。根据不同业务属性,性能提升不太一样。但是纯计算性能毋庸置疑是很高的,当然JIT后的JS也不差,这个下面会详细说。
  5. 深入融合Webpack工作流。这算是一个工程上可用的进步吧(比以前),Webpack4中WASM已经是一等公民了。
  6. 不用再写JS啦!对部分开发者这个可能是最重要的(当然有TS这个属性可能弱了点)。
  7. 完整。这个是说Rust这套流程的,它的web_sysjs_sys实现了WebIDL的完整Port,可用性还是很不错的(如果你不在意性能的话)。
  8. 还有的请补充......

性能

性能方面前面也说了,WASM的运算性能是可预测的,而JS则由于JIT和GC会比较难以预测。对于本应用,由于JS版本使用了TypedArrayBuffer,以及进行的都是非常容易优化的简单纯数学运算,所以JIT后的JS版本性能可以说达到了JS可达到的上限。但即使在这种状况下,对于大多数的测试WASM版本还是要稳稳压1.5倍左右:

详见[Benchmark(Matrix4, 2015 RMBP, Chrome)](https://github.com/dtysky/gl-matrix-wasm/blob/master/Benchmark.md。

而对于真实世界,性能测试就没有Benchmark这么简单了,我写了DEMO来进行这个测试(当然对于Web 1000个物体的场景已经很大了):http://gl-matrix-wasm.dtysky.moe/

可见其实WASM版本在真实场景中还是有优势的(当然,Safari除外,其WASM现在优化得还不行)。

当然,除了这个库本身我也测试过别的应用,比如CRC32、数字图像处理、DOM操作等等,大致结论就是在无法简单优化的密集计算场景下,WASM有显著优势(比如模型压缩解压缩,加密解密),而在其他场合目前看来就差了点意思,投入产出比较低。

未来

未来来看,我觉得WASM有几点很值得关注:

  1. WASI。WebAssembly System Interface,不用多说,这说明我们以前对WASM本身都有很深的误解,它并不是为Web而生的汇编语言,而是个恰好可以跑在Web上的通用虚拟机。这是个好东西,对未来WASM在更多场景可用而言很重要。
  2. SIMD。simd.js提案被废弃后,SIMD on WebAssembly就成了Web使用SIMD唯一可预计的方式了,而SIMD可以为图形应用带来的好处也不用多说。
  3. 多线程。相比于阉割了SharedArrayBufferWorker(虽然有Transferable对象),这东西在某些场合很重要。

结论

一个项目是否要选择WASM,还是要从其适用性出发。如果你的项目是对计算要求较高,而且没有频繁的WASM <-> JS互操作,同时能保证内存都是申请在WASM虚拟机中最好,那么你的项目是比较适合使用WASM的。否则至少在目前时点不太建议。

特别要求性能,目前暂时考虑CPP版本,Rust版本在工程化的角度做的很好,而且团队足够重视,应该是未来的主流。

当然WASM本身目前起步也不是很久,它也在不断变好,相信未来它的能力和性能都会更强,也能适用于更多的领域(虽然大多领域应该还是干不过JS,“我为啥要功夫学这玩意,JS两下撸完回家打游戏不好吗?”)。

踩坑或许还不是特别深入,大佬觉得有问题或者建议请指正。

招人

本来想发招人的但目前也比较难,不过你有P7的实例肯定是没问题的,我们是支付宝互动图形团队,致力于在这个前端迷失的时代,在前端老本行的方向走的更远,给用户带来更好的体验。我们有自研Web3D/2D游戏引擎、巨量用户和业务场景,还和小程序团队直接相邻,有想法请直接联系我。

完。

如果不是自己的创作,少女是会标识出来的,所以要告诉别人是少女写的哦。