原文地址:https://medium.com/web-on-the-edge/exploring-compilation-from-typescript-to-webassembly-f846d6befc12
三个月前,我们三个人 - Web平台团队的实习生 - 着手构建一个从TypeScript到WebAssembly的编译器。WebAssembly是一个适合编译到web的体积和加载性能更优的二进制格式,在提升Web应用程序的性能方面有这很大的潜能。 (我们发现这个WebAssembly的解释很有用)
在此之前,我们都没有编写过TypeScript,甚至没有听说过WebAssembly,我们也不太了解编译器。 考虑到这一点,说实现项目的目标是具有挑战性的一点都不假。
我们从基础开始:构建一个对编译器的认知的模型,并抽取出我们并不真正需要知道的任何细节。 我们阅读了WebAssembly文档,尝试编译并使用wasmFiddle运行WebAssembly,并编写了一些简单的TypeScript程序。
起初,我们在官方TypeScript编译器的一个分支上开发了一个原型编译器。这是我们遇到第一个重大挑战:一旦我们实现了一些基本功能(位操作,二进制表达式),我们意识到TypeScript中的类型系统与WebAssembly中的四种本地数值类型基本不兼容。我们决定假设Number数字类型总是指的是float64类型,因为我们不想偏离TypeScript太多。但是,这意味着我们无法处理只能使用int类型才能完成的情况(例如余数)而不损失效率。我们将这个问题放在了一边,最后通过If语句判断生成不同的wasm字节码来解决。
在实现If语句时,我们发现了一系列新的挑战,需要我们更多地了解wasm字节码本身的结构。我们首先查看由TypeScript编译器生成的抽象语法树,然后将其映射为分发if和else指令,但我们无法弄清楚如何评估条件并如何正确处理操作码立即数。我们查看了wasmFiddle的输出以查看if语句,并注意到他们正在使用块和break-if操作码来创建结构。我们意识到这两种方法基本上是等价的,但是块更容易跟踪。
我们仍然无法弄清楚要分发什么,所以我们试着手动修改wasm字节码,看看我们是否可以得到一个if语句的例子来产生我们期望的结果。这导致了一系列额外的问题,即我们还需要修改每个段的字节码中的长度参数。我们无法立即确定这是个问题,但是追踪它意味着我们单独查看操作码并学习它们属于哪个部分,哪些操作需要操作码立即数,哪些部分实际上只是ASCII编码的字符。一旦我们对这个主题有了这种基本的理解,就很容易找出我们之前就如何分发操作码和它们的立即数而犯的错误。
在用if语句取得进展之后不久,我们发现有两个开源项目试图将TypeScript的子集或变体编译为WebAssembly:AssemblyScript和TurboScript。鉴于我们从原型编译器中学到的,我们更喜欢AssemblyScript是新的,并且还有空间让我们贡献自己的力量。它还利用了我们熟悉的TypeScript编译器。因此,我们联系了AssemblyScript。 主要贡献者Daniel Wirtz非常乐于合作并接受来自我们的请求。
我们花了一段时间试验了AssemblyScript,遇到了相当多的构建问题,并发现了我们没有意识到的功能已经实现(字符串或类)和我们预期能够工作的功能,但没有(数组和浮点数)。我们一路上开了几个issue,这对我们来说是新的,因为这是我们贡献的第一个大型开源项目。我们很快就可以提供一项功能:将TypeScript中的数组列表初始化编译为WebAssembly。AssemblyScript编译器需要使用malloc和memset为数组设置WebAssembly的线性内存部分,然后发出操作码以设置内存中数组的容量和大小,然后根据类型的大小设置4 或8的间隔。
一旦我们开始为AssemblyScript和做编译器功能贡献代码,前端和测试基础架构开始成熟,我们就需要确定哪些工作正常,哪些可以改进。然后,我们编写了一些演示程序,并使用AssemblyScript将它们编译为wasm,但令我们沮丧的是发现wasm比JavaScript慢。
此外,我们发现使用基本的编译器编写一个引人注目的演示非常复杂,而且我们缺少很多功能,例如库函数。 在JS和WebAssembly之间进行公平比较尤其具有挑战性,每个程序更新演示的UI和完成计算,都是单线程的。我们使用WebWorker来解决这个问题,这是我们以前从未做过的事情。 然后,我们开始做一些侦探工作,找出为什么WebAssembly会比较慢。
我们采取的第一步是查看我们正在编译的代码,并将其分解成几部分。我们已经实现了一个数独解算器,它执行了大量的计算,但它也在创建和遍历大数组并进行大量的函数调用。我们开始编译小的程序,这些小的程序分别执行了这些任务,并发现虽然计算速度更快(正如我们所预期的那样),但数组访问,尾递归和函数调用非常缓慢。例如,Fibonacci的迭代计算运行速度提高了5倍,但4000个元素的数组遍历运行速度降低了4倍。我们意识到我们丢掉了像Emscripten + binaryen这样的管道优化的一些优化。然后,我们创建了一组基于Sunspider基准的测试,分别使用AssemblyScript和Emscripten + binaryen编译,用JavaScript,用兼容AssemblyScript的TypeScript和C语言做测试。我们可以编写的基准非常有限,因为它们需要与两个编译器兼容,目前仍然没有取得显着成果。然后,我们比较了性能,以确定AssemblyScript在哪些任务中仍然很慢。
在这个项目的过程中,我们学会了尽早并持续地进行测试,并且永远不要将某个功能视为理所当然,即使基础的if语句在WebAssembly中也是比较难于进行调试。我们还了解到,如果使用现存的功能强大工具去做一个编译器会轻松得多。我们有幸利用Daniel Wirtz在编译AssemblyScript时所做的令人难以置信的工作,以及Microsoft的TypeScript团队所做的工作以及所有从事二进制工作的人员的工作。如果我们没有采用TypeScript和二进制编译器基础结构中的现有开源工具,那么维护后端或构建解析器的开销会阻止我们完成尽可能多的工作。
阿普尔瓦拉曼,伊格纳西奥拉米雷斯皮里兹,萨纳特夏尔马