代码重用

想象一下,将用高级语言编写的非Web应用程序转换为可用于Web的二进制模块。 无需对非Web应用程序的源代码进行任何更改,即可完成此转换。 浏览器可以有效地下载新翻译的模块,并在沙箱中执行该模块。 执行中的Web模块可以与其他Web技术(尤其是JavaScript(JS))无缝交互。 欢迎使用WebAssembly 。

由于名称适合使用汇编语言,因此WebAssembly是低级的。 但是,这种低级特征鼓励优化:浏览器虚拟机的即时(JIT)编译器可以将可移植WebAssembly代码转换为快速的,特定于平台的机器代码。 WebAssembly模块因此成为适用于计算绑定任务(例如数字运算)的可执行文件。

哪些高级语言可以编译成WebAssembly? 这个列表正在增长,但是最初的候选者是C,C ++和Rust。 我们将这三种称为系统语言 ,因为它们是用于系统编程和高性能应用程序编程的。 系统语言共享两个适合它们编译成WebAssembly的功能。 下一部分将进行详细介绍,其中将设置完整的代码示例(使用C和TypeScript)以及WebAssembly自己的文本格式语言的示例。

显式数据输入和垃圾回收

这三种系统语言要求变量声明和从函数返回的值的显式数据类型,例如intdouble 。 例如,以下代码段说明了C中的64位加法:

 long n1 = random ( ) ; 
long n2 = random ( ) ;
long sum = n1 + n2 ;

库函数random声明为带有返回类型的型:

 long random(); /* returns a long */ 

在编译过程中,将C源代码翻译为汇编语言,然后将其翻译为机器代码。 在英特尔汇编语言(AT&T风格)中,上面的最后一个C语句类似于以下内容(带有##引入注释):

 addq %rax, %rdx ## %rax = %rax + %rdx (64-bit addition) 

%rax%rdx是64位寄存器,而addq指令表示add quadwords ,其中四字长为64位,这是C long的标准大小。 汇编语言强调了可执行的机器代码涉及类型,该类型通过指令和参数(如果有)的某种混合给出。 在这种情况下, add指令是addq (64位加法),而不是addl ,例如addl ,它将C int典型的32位值相加。 正在使用的寄存器是完整的64位寄存器( %rax%rdx中r ),而不是其32位块(例如, %eax%rax的低32位, %edx是低32位)的%rdx )。

三种强调显式类型的系统语言都是编译成WebAssembly的良好候选者,因为该语言也具有显式数据类型: i32(用于32位整数值), f64(用于64位浮点值),等等。

显式数据类型也鼓励对函数调用进行优化。 具有显式数据类型的函数具有签名 ,该签名指定参数的数据类型以及从函数返回的值(如果有)。 以下是名为$ add的WebAssembly函数的签名,该函数是用下面讨论的WebAssembly文本格式语言编写的。 该函数将两个32位整数作为参数,并返回一个64位整数:

 (func $add (param $lhs i32) (param $rhs i32) (result i64)) 

浏览器的JIT编译器应将32位整数参数和返回的64位值存储在适当大小的寄存器中。

当涉及到高性能的Web代码时,WebAssembly并不是唯一的游戏。 例如, asm.js是一种像WebAssembly一样设计的JS方言,可以接近本机速度。 asm.js语言要求优化,因为代码模仿了上述三种语言中的显式数据类型。 这是一个使用C然后是asm.js的示例。 C中的示例函数为:

 int f ( int n ) {       /** C **/ 
return n + 1 ;
}

参数n和返回值都明确地键入为int 。 等效功能为asm.js将是:

 function f ( n ) {      /** asm.js **/ 
n = n | 0 ;
return ( n + 1 ) | 0 ;
}

通常,JS没有显式的数据类型,但是JS中的按位或运算会产生一个整数值。 这解释了否则没有意义的按位或运算:

 n = n | 0;  /* bitwise-OR of n and zero */ 

n与零的按位或运算的结果为n ,但此处的目的是表示n保持一个数值。 return语句重复了此优化技巧。

在JS方言中,TypeScript在采用显式数据类型方面脱颖而出,这使得该语言对于编译成WebAssembly具有吸引力。 (下面的代码示例对此进行了说明。)

三种系统语言共有的第二个功能是它们在没有垃圾收集器(GC)的情况下执行。 对于动态分配的内存,Rust编译器会自动写入分配和释放代码; 在其他两种系统语言中,动态分配内存的程序员负责显式取消分配相同的内存。 系统语言避免了自动GC的开销和复杂性。

WebAssembly的快速概述可以总结如下。 WebAssembly语言上的几乎所有文章都将近乎本机的速度提到为该语言的主要目标之一。 本机速度是编译系统语言的速度。 因此,这三种语言也是最初被编译为WebAssembly的候选语言。

WebAssembly,JavaScript和关注点分离

完全相反的是,WebAssembly语言并非旨在取代JS,而是通过在计算绑定任务上提供更好的性能来补充JS。 WebAssembly在下载方面也有优势。 浏览器将JS模块作为文本获取,这是WebAssembly解决的效率低下问题。 WebAssembly中的模块具有紧凑的二进制格式,可加快下载速度。

同样令人关注的是JS和WebAssembly如何一起工作。 JS旨在读取和编写文档对象模型(DOM),即网页的树表示形式。 相比之下,WebAssembly没有DOM的任何内置功能。 但是WebAssembly可以导出JS然后可以根据需要调用的函数。 关注点的分离意味着分工的清晰划分:

 DOM<----->JS<----->WebAssembly 

不管使用哪种方言,JS仍应管理DOM,但JS也可以使用通过WebAssembly模块提供的通用功能。 一个代码示例有助于说明分工。 (本文中的代码示例可在我的网站上的ZIP文件中找到。)

冰雹序列和Collat​​z猜想

一个生产级示例将使WebAssembly代码执行繁重的计算绑定任务,例如生成大型加密密钥对或使用此类对进行加密和解密。 一个更简单的示例适合作为简单易行的替代方案。 有数字运算,但是JS可以轻松处理的常规形式。

考虑功能会斯通 (对于冰雹 ),它的正整数作为参数。 该函数定义如下:

3N + 1 if N is odd
hstone(N) =
N/2 if N is even

例如, hstone(12)返回6,而hstone(11)返回34。如果N为奇数,则3N + 1为偶数; 但是如果N是偶数,则N / 2可以是偶数(例如4/2 = 2)或奇数(例如6/2 = 3)。

通过将返回值作为下一个参数传递,可以迭代使用hstone函数。 结果是这样的冰雹序列 ,该序列以24作为原始参数开头,返回值12作为下一个参数,依此类推:

 24,12,6,3,10,5,16,8,4,2,1,4,2,1,... 

它需要10次调用才能使序列收敛到1,此时4,2,1的序列将无限期地重复:(3×1)+1为4,将其减半以产生2,将其减半以产生1,依此类推上。 加号杂志解释了为什么冰雹似乎是此类序列的合适名称 。

请注意,2的幂快速收敛,仅需N除以2即可达到1。 例如,32 = 2 5的会聚长度为5,而64 = 2 6的会聚长度为6。 这里有趣的是从初始参数到第一次出现一个的序列长度。 我在C和TypeScript中的代码示例计算冰雹序列的长度。

Collat​​z猜想是,无论初始参数N> 0是什么,冰雹序列都收敛到一个。 没有人找到Collat​​z猜想的反例,也没有人找到将猜想提升为定理的证明。 这个猜想,尽管很容易通过程序进行测试,却仍然是数学中一个极具挑战性的问题。

从C到WebAssembly的一步

下面的hstoneCL程序是一个非Web应用程序,可以使用常规C编译器(例如GNU或Clang)进行编译。 该程序将生成一个大于8的N> 0的随机整数值,并从N开始计算冰雹序列的长度。稍后将应用程序编译为WebAssembly时,将需要两个程序员定义的函数mainhstone

例子1. C语言中的hstone函数

 #include <stdio.h> 
#include <stdlib.h>
#include <time.h>

int hstone ( int n ) {
int len = 0 ;
while ( 1 ) {
if ( 1 == n ) break ;           /* halt on 1 */
if ( 0 == ( n & 1 ) ) n = n / 2 ; /* if n is even */
else n = ( 3 * n ) + 1 ;        /* if n is odd  */
len ++;                       /* increment counter */
}
return len ;
}

#define HowMany 8

int main ( ) {
srand ( time ( NULL ) ) ;  /* seed random number generator */
int i ;
puts ( "  Num  Steps to 1" ) ;
for ( i = 0 ; i < HowMany ; i ++ ) {
int num = rand ( ) % 100 + 1 ; /* + 1 to avoid zero */
printf ( "%4i %7i \n " , num , hstone ( num ) ) ;
}
return 0 ;
}

可以在任何类似Unix的系统上从命令行(以作为命令行提示符)编译并运行代码:

 % gcc -o hstoneCL hstoneCL.c  ## compile into executable hstoneCL 
% . / hstoneCL                  ## execute

这是示例运行的输出:

Num  Steps to 1
88      17
1       0
20       7
41     109
80       9
84       9
94     105
34      13

包括C在内的系统语言都需要专门的工具链,才能将源代码转换为WebAssembly模块。 对于C / C ++语言, Emscripten是一种开创性且仍被广泛使用的选项,它是基于著名的LLVM (低级虚拟机)编译器基础结构构建的。 我在C语言中使用的示例使用Emscripten,您可以通过本指南进行安装 )。

可以使用Emscription将代码编译成WebAssembly模块,而无需进行任何更改,从而使hstoneCL程序网络化。 Emscription工具链还创建了一个HTML页面以及JS 胶水 (在asm.js中),该胶水在DOM和计算hstone函数的WebAssembly模块之间进行中介 。 步骤如下:

  1. 将非Web程序hstoneCL编译为WebAssembly:

     % emcc hstoneCL.c -o hstone.html  ## generates hstone.js and hstone.wasm as well 
    

    文件hstoneCL.c包含上面显示的源代码,并且-o for output标志指定HTML文件的名称。 任何名称都可以,但是生成的JS代码和WebAssembly二进制文件则具有相同的名称(在本例中,分别为hstone.jshstone.wasm )。 较早版本的Emscription( 低于 13的版本)可能要求标志-s WASM = 1包含在编译命令中。

  2. 使用Emscription开发Web服务器(或等效服务器)托管网络化的应用程序:

     % emrun --no_browser --port 9876 .   ## . is current working directory, any port number you like 
    

    要禁止显示警告消息,可以包括标志–no_emrun_detect 。 此命令将启动Web服务器,该Web服务器将托管当前工作目录中的所有资源。 特别是hstone.htmlhstone.jshstone.webasm

  3. 打开支持WebAssembly的浏览器(例如Chrome或Firefox),访问URL http:// localhost:9876 / hstone.html

此屏幕快照显示了我在Firefox上运行的示例的输出。

代码重用_WebAssembly的速度和代码重用-编程知识网

图1.网络化的hstone程序

结果是惊人的,因为整个编译过程只需要一个命令,而无需对原始C程序进行任何更改。

微调hstone程序进行网络化

Emscription工具链很好地将C程序编译为WebAssembly模块并生成所需的JS胶水,但是这些工件对于机器生成的代码来说是典型的。 例如,生成的asm.js文件大小几乎为100KB。 JS代码处理多种情况,并且不使用最新的WebAssembly API。 在Web化会斯通程序的简化版本会更容易把重点放在如何WebAssembly模块(装在hstone.wasm文件)与JS胶(装在hstone.js文件)相互作用。

还有另一个问题:WebAssembly代码不需要镜像诸如C之类的源程序中的功能边界。例如,C程序hstoneCL具有两个用户定义的函数mainhstone 。 生成的WebAssembly模块将导出名为_main的函数,但不会导出名为_hstone的函数。 (值得注意的是,函数main是C程序中的入口点。)C hstone函数的主体可能在某些未导出的函数中,或者只是包装在_main中 。 导出的WebAssembly函数正是JS胶可以通过名称调用的函数。 但是,有一条指令可以指定应在WebAssembly代码中按名称导出哪些源语言功能。

例子2.修改后的hstone程序

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <emscripten/emscripten.h>

int EMSCRIPTEN_KEEPALIVE hstone(int n) {
int len = 0;
while (1) {
if (1 == n) break;           /* halt on 1 */
if (0 == (n & 1)) n = n / 2; /* if n is even */
else n = (3 * n) + 1;        /* if n is odd  */
len++;                       /* increment counter */
}
return len;
}

上面显示的修订后的hstoneWA程序没有主要功能。 不再需要它,因为该程序并非旨在作为独立的应用程序运行,而是专门作为具有单个导出功能的WebAssembly模块运行。 指令EMSCRIPTEN_KEEPALIVE (在头文件emscripten.h中定义)指示编译器在WebAssembly模块中导出_hstone函数。 命名约定很简单:诸如hstone之类的 AC函数保留其名称,但在WebAssembly中使用单个下划线作为其第一个字符(在本例中为_hstone )。 WebAssembly中的其他编译器遵循不同的命名约定。

为了确认该方法是否有效,可以简化编译步骤,以仅生成WebAssembly模块和JS胶水,而不生成HTML:

 % emcc hstoneWA.c -o hstone2.js  ## we'll provide our own HTML file 

现在可以将HTML文件简化为以下手写文件:

<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<script src="hstone2.js"></script>
</head>
<body/>
</html>

HTML文档将加载JS文件,该JS文件随后将获取并加载WebAssembly二进制文件hstone2.wasm 。 顺便说一下,新的WASM文件的大小约为原始示例的一半。

可以像以前一样编译应用程序代码,然后使用内置的Web服务器启动它:

 % emrun --no_browser --port 7777 .  ## new port number for emphasis 

在浏览器(在本例中为Chrome)中请求修订HTML文档之后,可以使用浏览器的Web控制台来确认hstone函数已导出为_hstone 。 这是我在Web控制台中会话的一部分, ##再次引入了注释:

> _hstone(27)   ## invoke _hstone by name
< 111           ## output
> _hstone(7)    ## again
< 16            ## output

EMSCRIPTEN_KEEPALIVE指令是让Emscripten编译器生成WebAssembly模块的直接方法,该模块将感兴趣的任何功能导出到该编译器同样生成的JS胶。 然后,可以使用具有适当手工制作的JS的自定义HTML文档,来调用从WebAssembly模块导出的函数。 对于这种干净的方法,Emscripten表示欢迎。

将TypeScript编译为WebAssembly

下一个代码示例在TypeScript中,它是具有显式数据类型的JS。 该安装程序需要Node.js及其npm软件包管理器。 以下npm命令将安装AssemblyScript,这是用于TypeScript代码的WebAssembly编译器:

 % npm install -g assemblyscript  ## install the AssemblyScript compiler 

TypeScript程序hstone.ts由一个函数(又称为hstone)组成 。 现在,诸如i32 (32位整数)之类的数据类型将紧随参数名称和局部变量名称之后(而不是参数名称和局部变量名称之前)(在本例中分别为nlen ):

export function hstone(n: i32): i32 { // will be exported in WebAssembly
let len: i32 = 0;
while (true) {
if (1 == n) break;            // halt on 1
if (0 == (n & 1)) n = n / 2;  // if n is even
else n = (3 * n) + 1;         // if n is odd
len++;                        // increment counter
}
return len;
}

功能会斯通采用类型I32中的一个参数,并返回相同的类型的值。 该函数的主体与C示例中的主体基本相同。 可以按以下方式将代码编译到WebAssembly中:

 % asc hstone.ts -o hstone.wasm  ## compile a TypeScript file into WebAssembly 

WASM文件hstone.wasm的大小仅为14KB。

为了突出显示如何加载WebAssembly模块的详细信息,下面的手写HTML文件(我网站上ZIP中的 index.html )包含用于获取和加载WebAssembly模块hstone.wasm并随后实例化此模块的脚本可以在浏览器的控制台中调用导出的hstone函数进行确认。

例子3. TypeScript代码HTML页面

<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<script>
fetch('hstone.wasm').then(response =>            <!-- Line 1 -->
response.arrayBuffer()                           <!-- Line 2 -->
).then(bytes =>                                  <!-- Line 3 -->
WebAssembly.instantiate(bytes, {imports: {}})    <!-- Line 4 -->
).then(results => {                              <!-- Line 5 -->
window.hstone = results.instance.exports.hstone; <!-- Line 6 -->
});
</script>
</head>
<body/>
</html>

可以逐行阐明上面HTML页面中的script元素。 第1行中的fetch调用使用Fetch模块从托管HTML页面的Web服务器获取WebAssembly模块。 当HTTP响应到达时,WebAssembly模块将以字节序列的形式进行操作,这些字节序列存储在脚本的第2行的arrayBuffer中。这些字节构成了WebAssembly模块,它是从TypeScript文件编译的所有代码。 如第4行结尾所示,此模块没有导入。

在第4行的开头,实例化了WebAssembly模块。 WebAssembly模块类似于非静态类,该类具有面向对象的语言(例如Java)中的非静态成员。 该模块包含变量,函数和各种支持工件。 但是必须像在非静态类中一样,将该模块实例化为可用,在这种情况下,必须在Web控制台中使用,但通常应在相应的JS粘合代码中使用。

脚本的第6行以相同的名称导出原始TypeScript函数hstone 。 该WebAssembly函数现在可用于任何JS粘合代码,因为浏览器控制台中的另一个会话将确认。

WebAssembly具有更简洁的API,用于获取和实例化模块。 新的API将上面的脚本简化为仅获取实例化操作。 此处显示的较长版本具有展示细节的好处; 特别是将WebAssembly模块表示为字节数组,并将其实例化为具有导出函数的对象。

计划是让网页以与JS ES2015模块相同的方式加载WebAssembly模块:

 <script type='module'>...</script> 

然后,JS将获取,编译和以其他方式处理WebAssembly模块,就好像它只是另一个JS模块一样。

文字格式语言

WebAssembly二进制文件可以与等效的文本格式相互转换。 二进制文件通常驻留在带有WASM扩展名的文件中,而它们相对应的文本副本驻留在具有WAT扩展名的文件中。 WABT是用于处理WebAssembly的近十二种工具的集合,其中包括用于与WASM和WAT等格式进行相互转换的工具。 转换工具包括wasm2watwasm2cwat2wasm实用程序。

文本格式语言采用Lisp流行的S表达式( S表示符号 )语法。 S表达式(简称sexpr )将树表示为具有任意多个子列表的列表。 例如,对于TypeScript示例,此sexpr发生在WAT文件的末尾:

 (export "hstone" (func $hstone)) ## export function $hstone by the name "hstone" 

树表示为:

export        ## root
|
+----+----+
|         |
"hstone"    func    ## left and right children
|
$hstone   ## single child

以文本格式,WebAssembly模块是一个sexpr,其第一个术语是module ,这是树的根。 这是定义和导出单个函数的模块的基本示例,该函数不带参数,但返回常数9876:

(module
(func (result i32)
(i32.const 9876)
)
(export "simpleFunc" (func 0)) // 0 is the unnamed function's index
)

该函数没有名称定义(即作为lambda),并通过引用其索引0(该索引是模块中第一个嵌套sexpr的索引)导出的。 导出名称以字符串形式给出; 在这种情况下,为“ simpleFunc”。

文本格式的函数具有标准模式,可以如下所示:

 (func <signature> <local vars> <body>) 

签名指定参数(如果有)和返回值(如果有)。 例如,这是一个未命名函数的签名,该函数采用两个32位整数参数,但返回一个64位整数值:

 (func (param i32) (param i32) (result i64)...) 

可以为函数,参数和局部变量指定名称。 名称以美元符号开头:

 (func $foo (param $a1 i32) (param $a2 f32) (local $n1 f64)...) 

WebAssembly函数的主体反映了该语言的基础堆栈计算机体系结构。 堆栈存储用于暂存器。 考虑以下函数示例,该函数将其整数参数加倍并返回值:

(func $doubleit (param $p i32) (result i32)
get_local $p
get_local $p
i32.add)

每个get_local操作都可以对局部变量和参数进行处理,它们将32位整数参数压入堆栈。 然后, i32.add操作会从堆栈中弹出前两个(且当前仅)值,以执行加法。 这样,加法运算的总和就是堆栈上的唯一值,从而成为$ doubleit函数返回的值。

将WebAssembly代码转换为机器代码时,应尽可能用通用寄存器替换作为暂存器的WebAssembly堆栈。 这是JIT编译器的工作,它将WebAssembly虚拟堆栈计算机代码转换为真实计算机代码。

Web程序员不太可能以文本格式编写WebAssembly,因为从某些高级语言进行编译是一种非常诱人的选择。 相比之下,编译器作者可能会发现在这种细粒度的级别上工作会很有成效。

结语

WebAssembly达到接近自然速度的目标已经做了很多工作。 但是随着JS的JIT编译器的不断改进,以及非常适合优化的方言(例如TypeScript)的出现和发展,可能是JS的速度也接近原生。 这是否意味着WebAssembly浪费了精力? 我觉得不是。

WebAssembly解决了计算中的另一个传统目标:有意义的代码重用。 就像本文中的简短示例所说明的那样,使用合适的语言(例如C或TypeScript)编写的代码可以轻松地转换为WebAssembly模块,该模块可以与JS代码完美地结合在一起,而JS代码是连接Web中使用的多种技术的粘合剂。 因此,WebAssembly是一种重用旧代码并扩展新代码使用范围的诱人方法。 例如,最初作为桌面应用程序编写的,用于图像处理的高性能程序也可能在Web应用程序中有用。 然后,WebAssembly成为有吸引力的重用途径。 (对于受计算限制的新Web模块,WebAssembly是一个不错的选择。)我的直觉是WebAssembly会在重用和性能方面蓬勃发展。

翻译自: https://opensource.com/article/19/8/webassembly-speed-code-reuse

代码重用