如何使用WebAssembly构建高计算量web应用(上)

image.png

目录

📦 JavaScript语言设计缺陷和性能短板
📦 什么是WebAssembly?
📦 WebAssembly是如何工作的?
📦 为什么WebAssembly更快?
📦 WebAssembly实战演练


1. JavaScript语言存在的缺陷和性能短板

1.1 JS 语言设计缺陷

  • JavaScript 最初是一种简单的脚本语言,旨在为充满轻量级超文本文档的 Web 应用带来一些交互性。它的设计易于学习和编写,并不追求运行速度。多年来,浏览器在 JavaScript 解析上的重大性能改进的众多浏览器引入了即时(JIT)编译使得JavaScript运行速度快了一个量级。使得JavaScript运行速度快了一个量级。(大拐点)


    image.png

但是对于 JavaScript 这种弱数据类型的语言来说,要实现一个完美的 JIT 非常难。因为Javascript 是一个没有类型的语言,而且像+这样的符号又能够重载,譬如这样的代码:
const sum = (a, b, c) => a + b + c;
这是 一个求和函数,可以直接放在浏览器的控制台下运行,如果传参都是整数时,结果是整数相加的结果:如,答案是 6。但是,如果至少有一个是字符串,则结果是按照字符串拼接出的结果,如 console.log(sum('1',2,3)),答案是 "123"。也就是说,JIT在遇到sum第一个参数时会编译成字符串的机器码;但是在碰到第二个sum调用时,不得不重新编译一遍。这样一来,JIT带来的效率提升便被抵消了。

1.2 JavaScript性能问题

  • 我们对性能的期望是无尽的,JIT带来的性能提升也早已因为JavaScript的动态特性达到了性能天花板。特别在当前Web端视频音频、图形图像处理、VR、AR游戏的日益增多的情况下,问题显得日益突出。无法满足一些大型web项目开发,于是三大浏览器巨头分别提出了自己的解决方案:


    image.png

    image.png

    我们熟知的四大主流浏览器厂商 Google Chrome、Apple Safari、Microsoft Edge 和 Mozilla FireFox ,觉得Mozilla FireFox所推出的 asm.js 很有前景,为了让大家都能使用,于是他们就共同参与开发,基于asm.js制定一个标准,也就是WebAssembly。

  • 2015年, WebAssembly首次发布,并可直接在浏览器中运行

  • 2017 年 3 月份, 四大厂商均宣布已经于最新版本的浏览器中支持了 WebAssembly 的初始版本,这意味着 - WebAssembly 技术已经实际落地

  • 2019年,被正式加入Web的标准大家庭中

浏览器支持

image.png

由图可见,无论是PC、移动端还是服务器,都已经开始支持WebAssembly了,这也说明WebAssembly已经开始普及~

2.什么是WebAssembly?

2.1 概念理解

在了解 WebAssembly 之前,让我们先了解一下机器语言汇编语言高级语言

计算机语言分为三类

  • 第一代 机器语言 (相当于人类的原始阶段)
  • 第二代 汇编语言 (相当于人类的手工业阶段)
  • 第三代 高级语言(相当于人类的工业阶段)
2.1.1 机器语言

机器语言(machine code)本质上是由“0”和“1”组成的二进制数。计算机发明之初,人们只能计算机的语言去命令计算机干这干那。向计算机每发出一条指令,就要写出一串串由“0”和“1”组成的指令序列。


image.png

因此,使用机器语言是十分痛苦的,特别是在程序有错需要修改时,更是如此。而且,由于每台计算机的指令系统往往各不相同,所以,在一台计算机上执行的程序,要想在另一台计算机上执行,必须另编程序,需要进行大量重复繁琐的工作。

但在当时,由于使用的是针对特定型号计算机的语言,故而运算效率是所有语言中最高的。机器语言,是第一代计算机语言。

  • 机器语言的优点: 直接执行,速度快,资源占用少
  • 机器语言的缺点:难读、难编、难记、可移植性差和易出错
2.1.2 汇编语言

为了减轻使用机器语言编程的痛苦,人们进行了一种有益的改进:用一些简洁的英文字母、符号串来替代一个特定的指令的二进制串。比如,用“ADD”代表加法。这样我们很容易读懂并理解程序在干什么,纠错及维护都变得方便了,这种程序设计语言就称为汇编语言,即第二代计算机语言(Assembly)。

Assembly是一种低级编程语言,使用汇编器(Assembler)可以在一个进程内很方便地转换成机器码。


18616547-b9cd26cf7b617635.png
2.1.3 高级语言

不论是机器语言还是汇编语言都是面向硬件的具体操作的,语言对机器的过分依赖,要求使用者必须对硬件结构及其工作原理都十分熟悉,这对非计算机专业人员是难以做到的,对于计算机的推广应用是不利的。人们意识到,应该设计一种这样的语言,这种语言接近于数学语言或人的自然语言,同时又不依赖于计算机硬件,编出的程序能在所有机器上通用。经过努力,1954年,第一个完全脱离机器硬件的高级语言—FORTRAN问世了,40多年来,共有几百种高级语言出现,有重要意义的有几十种。其中就包括C++、java等。

高级语言、汇编语言、机器语言之间的关系如下:


image.png

所有高级编程语言都会被自己的编译器编译,比如C++ 被编译器转换为汇编语言再被汇编器汇编成机器码,以便在特定处理器,比如x86\x64\arm上运行。不同的处理器架构需要不同的机器代码和不同的汇编语言(assembly)。


image.png

2.2 什么是 webAssembly

WebAssembly(缩写为 Wasm)是一种基于栈式虚拟机的二进制指令格式。Wasm 是一种底层类汇编语言,能在 Web 平台上以趋近原生应用的速度运行。C/C++/Rust 等语言将 Wasm 作为编译目标语言,可以将已有的代码移植到 Web 平台中运行,以提升代码复用度。

咱们能够从字面上理解,WebAssembly的名字带个Assembly(汇编),因此咱们从其名字上就能知道其意思是给Web使用汇编语言,让Web执行低级二进制语法。而是如下图,通过编译器把高级别的语言(C,C++和Rust)编译为WebAssembly,以便有机会在浏览器中运行。能够看出来它实际上是一种运行机制,一种新的字节码格式(.wasm),而不是新的语言。


18616547-32bfe3fe31a6dca1.png

WebAssembly 的特点:

  • 层次低,尽量接近机器语言,这样解释器才更容易进行 AOT/JIT 编译,以趋近原生应用的速度运行 Wasm 程序;
  • 作为目标代码,由其他高级语言编译器生成;
  • 代码安全可控,不能像真正的汇编语言那样可以执行任意操作;
  • 代码是平台无关的(不能是平台相关的机器码),可以跨平台执行,采用了虚拟机/字节码技术。

WebAssembly 目前已经在浏览器端的图像处理、音视频处理、游戏、IDE、可视化、科学计算等

3. webAssembly是如何工作的?

工作原理:WebAssembly的工作原理简要来说是:我们把C,C++, Rust等静态语言的程序编译成浏览器能够运行的wasm二进制文件,当浏览器下载 WebAssembly 代码时,可以快速将其转换为任何本地机器码后运行。
设计WebAssembly的主要目标之一是可移植性。 要在某个设备上运行应用程序,它必须兼容设备的处理器架构和操作系统。这意味着要为支持的操作系统和CPU架构的每个组合编译源代码。 使用 WebAssembly ,只需要一次编译,您的应用程序将可以在每个现代浏览器中运行。

3.1 LLVM(Low-Level-Virtural-Machine)编译模型

我们知道大多数静态高级语言是通过编译器前端编译成为中间代码,然后再由编译器后端把中间代码翻译成目标机器的可执行机器码的。

image.png
3.2 WebAssembly LLVM

webAssembly对应的位置则是生成特定平台机器码之前,类似于一种汇编语言,但是它不对应真实的物理机器,而是一种浏览器抽象成的虚拟处理器,比JavaScript源码更直接地对应机器码。浏览器下载wasm文件后只需要做简单的编译生成特定机器的机器码就能执行。

image.png

如下图,是WebAssembly的代码范例 - 它具有易于阅读的文本格式(.wat),但实际提供给浏览器的内容是二进制格式(.wasm)。


18616547-2c2c84c9501ed423.png

如下图:WebAssembly 允许将 C ,C++ 或 Rust 代码编译成 WebAssembly 模块,可以在Web 应用中加载并通过JavaScript调用。它不是 JavaScript 的替代品,它将与JavaScript一起共存。


18616547-4fc4cb4d9550b2e6.png

4. 为什么WebAssembly更快?

一说到WebAssembly,许多文章都会提及它的快,它的性能优势都是相对于JavaScript来说的,但是他为什么快呢?

4.1 体积小

WebAssembly的二进制文件比 JavaScript 文本文件小得多。因而下载速度更快,这在网速低的时候尤为重要。

4.2 ### JIT原理

为了了解为什么WebAssembly有更好的性能,我们首先需要简单过一下浏览器JIT的工作原理。

首先JavaScript引擎的工作就是把我们看得懂的编程语言转换成机器能看懂的语言,我们与机器的沟通介质就是JavaScript引擎,没有这个翻译官,我们下达的命令机器就没法理解和执行。


image.png

编程语言的翻译有两种方法:

  • 使用解释器
  • 使用编译器

解释器是一部分一部分地边解释边执行。

image.png

编译器是提前把源代码整个编译成目标代码,直接在支持目标代码的平台上运行,执行过程不需要编译器。

image.png

两种方法各有利弊:
解释器好处除了易于实现跨平台外,最直接的就是对于我们前端开发人员来说,调试页面时,修改一行代码可以立即看到结果,不需要等待编译过程。但是对于同样的代码需要执行多次的情况弊端就很明显了,它需要执行多次的解释,就比如说执行循环。

编译器则可以在编译的时候可以对这些重复的代码等进行优化,使得执行得更快。由于花了许多时间在提前优化上,所以相对地需要牺牲的就是编译代码的时间。

在JIT出现以前,JavaScript大多都是从由JavaScript引擎解释执行的,因此效率低下,为了解决性能问题,其中一个办法就是引入编译器,将部分代码优化编译,充分结合编译器的优势,结合解释器与即时编译器(JIT)以提升性能早已在python等语言上得到很好的实践证实,所以浏览器厂商们就纷纷引入了JIT。

JIT的实现方法就是在JavaScript引擎中实现一个监视器,这个监视器监视运行的代码,记录下代码各自的运行次数并标记它们的热点类型。如果发现某一段代码执行了较多次,将标记为’warm’,如果执行了许多次,那么就会被标记为’hot’


image.png

JIT会把标记为warm的代码送到基线编译器(Baseline compiler)中编译,并且存储编译结果,当解释器继续解释时,监视器发现了同样的代码,那么就会把刚才编译好的结果推给浏览器,让浏览器使用较快的编译版本。


image.png

在基线编译器中,代码会进行一定程度的优化,但是由于优化需要时间,代码只是warm状态,所以基线编译器并不会花太长时间去优化。
不过如果标记为warm的代码执行了更多次呢? 代码已经非常的hot了,这时花更多时间去优化它就非常有必要了,所以监视器会将这段代码放到优化编译器(Optimizing compiler)中,生成更加快速高效的代码,这时如果监视器发现了同样的代码,就会返回这个更加快的优化编译版本。


image.png

然而,由于Javascript是弱类型的语言,它的灵活性可能会导致在几百个循环后某一次循环中这个对象少了某个属性,那么JIT检测到后会认为这个优化的编译代码不合理,会将这个编译代码丢弃掉,转而使用基线编译版本或者也可能直接回到解释器。

这个过程叫做去优化(JIT监视到如果某段代码进行了几次优化到去优化的循环后,会终止这段代码的优化编译,防止无限的循环,尽管如此,这里的性能损耗仍不可忽视)

image.png

举个简单的例子,对于以下这段循环代码,在基线编译器阶段,每一行代码会被基线编译器编译成代码桩

function arraySum (arr) {
  let sum = 0
  for (let i = 0; i < arr.length; i++) {
    sum += arr[i]
  }
}

但是对于累加sum += arr[i]这一句代码,我们并没有确定sum和arr是什么类型,如果是数字,基线编译器会生成一个桩(stub),如果是字符串,它也会为它生成一个桩,也就是说在调用过程中,可能有一个以上的桩,这时浏览器再次执行这段代码时,需要进行多次分支选择,进行类型检查。

image.png

所以,如果类型不固定,使用的是基线编译版本,每次循环都要进行一次类型检查。


image.png

如果类型固定,使用的是优化编译器后的版本,在循环之前就进行一次类型检查就可以了,速度提升是相当显著的。


image.png

因此,使用JavaScript在灵活与兴能上存在一定的折中关系,享受灵活的同时必然有一定的性能损耗。

总的来说通过JIT编译器,JavaScript的性能有了很大的提升,通过使用基线编译版本或者优化编译版本,能够大大减少解释器的时间损耗。但是JIT也有一定的瓶颈,主要体现在:

JIT 编译器花了很多时间在猜测 Javascript 中的类型。

  • 在优化和去优化过程中造成了很大开销
  • 而使用WebAssembly的出现的原因之一,就是为了消除这些开销。
3.3 JIT与WebAssembly的时间耗时对比
javascript

我们来看看运行一段JavaScript代码时,JavaScript引擎所花的时间的大概分布

  • parse : 解释。对JavaScript源码进行解释,生成抽象语法树或者字节码,传递给解释器。
  • compile + optimize : 编译+优化。解释器生成字节码,并通过编译器(JIT)编译优化部分字节码,生成机器码。
  • re-optimize : 发生去优化时,重新优化所花的时间。
  • execute : 执行代码的过程。
  • garbage collection: 清理内存的时间。

需要注意的是,这几个部分的工作在线程中是交替进行的,一段代码中某一部分可能在解释、然后某一部分可能在去优化、然后某一部分可能在执行。这里的图的顺序只是为了方便描述。

而需要提及的是,过去没有JIT时,JavaScript的执行时间需要更多的时间,就如同下图所示:

image.png

那么对于执行一段相同功能的WebAssembly代码的时间分布大概是怎样的呢?让我们逐步比对一下。

WebAssembly本身就以二进制形式提供,解析速度更快。它是静态类型的,因此与JavaScript不同,引擎在编译期间不需要类型推断。大多数优化都是在编译源代码期间,在浏览器执行之前进行的。内存是手动管理的,就像 C 和 C++ 这样的语言一样,所以也没有垃圾收集。所有这些都是为了提供了更好,更可靠的性能。

正是因为Wasm的大部分优化工作已经在LLVM的前端部分完成了,所以编译优化的工作很少,这便是其高性能的主要体现。
WebAssembly代码在浏览器中的执行过程:

  1. 解码
  2. 编译
  3. 执行
15825758-e24e8a8c4a817dcd.png

WebAssembly就只有解码、编译优化和执行这三部分开销,对比原生性能开销减少了许多

v2-1d11ad00b54f72d726983ba44201a3c4_720w.jpg

5. WebAssembly实战演练

WebAssembly是一个具有WASM扩展名的文件,可以把它看作一个可以导入JavaScript程序的模块。那么如何生成WASM文件呢?

5.1 emscripten 安装与使用, 让C语言出现在前端

官方推荐方式,先下载 emsdk:

git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
# 下载并安装最新的 SDK 工具.
./emsdk install latest

# 为当前用户激活最新的 SDK. (写入 .emscripten 配置文件)
./emsdk activate latest

# 激活当前 PATH 环境变量
source ./emsdk_env.sh

验证
emcc -v 不报错就成功了

编译
接下来就可以编译代码啦。
来个万年不变的Hello world试试:

  1. 在emsdk文件夹下创建test.c文件
#include<stdio.h>

void main(){
  printf("Hello world!");
}
  1. 使用刚才已经配置过的终端,找到test.c文件,执行以下命令
emcc ./test.c -s WASM=1 -o ./test.html
  • emcc 是Emscripten编译器行命令

  • test.c 是咱们的输入文件

  • -s WASM=1 指定咱们想要的wasm输出形式。若是咱们不指定这个选项,Emscripten默认将只会生成asm.js。(可参考 emcc --help 参数说明)

  • -o ./test.html 指定这个选项将会生成HTML页面来运行咱们的代码,而且会生成wasm模块,以及编译和实例化wasm模块所须要的“胶水”js代码,这样咱们就能够直接在web环境中使用了。
    执行后会产生三个新文件:

  • test.wasm 二进制的wasm模块代码,虽然本地打不开,可是浏览器能够帮忙翻译。

  • test.js 一个包含了用来在原生C函数和JavaScript/wasm之间转换的胶水代码的JavaScript文件

  • test.html 一个用来加载,编译,实例化你的wasm代码而且将它输出在浏览器显示上的一个HTML文件
    启动http服务命令,查看运行结果

emrun --no_browser --port 8080 ./test.html
5.2 webAssembly 参与页面中大计算量功能demo
  1. 修改test.c中的C语言代码为斐波那契数列:
#include <stdio.h>

int fib(int n)
{
   if (n <= 1)
      return n;
   return fib(n-1) + fib(n-2);
}
  1. 编译生成wasm文件
 emcc ./test.c -Os -s WASM=1 -s SIDE_MODULE=1 -o ./test.wasm
  • emcc就是Emscripten编译器,
  • test.c是咱们的输入文件
  • -Os表示此次编译须要优化(能够指定优化策略。emcc --help)
  • -s 后紧跟编译的配置(setting)WASM=1表示输出wasm的文件,由于默认的是输出asm.js
  • -s 后紧跟编译的配置(setting)SIDE_MODULE=1表示就只要这一个模块,不要给我其余乱七八糟的代码
  • -o 表示输出的文件,test.wasm是咱们的输出文件。
    所有配置项可以在这里查看。
  1. 书写页面demo
<!DOCTYPE html>
<html>

<head>
    <meta charset='utf-8'>
    <meta http-equiv='X-UA-Compatible' content='IE=edge'>
    <title>Page Title</title>
    <meta name='viewport' content='width=device-width, initial-scale=1'>
</head>

<body>
    <script>
        // 斐波那契数列
        let fib;
        function fibjs(n) {
            if (n <= 1)
                return n;
            return fibjs(n - 1) + fibjs(n - 2);
        }
        function loadWebAssembly(path, imports = {}) {
            return fetch(path) // 加载文件
                .then(response => response.arrayBuffer()) // 转成 ArrayBuffer
                .then(buffer => WebAssembly.compile(buffer))
                .then(module => {
                    return new WebAssembly.Instance(module);
                })
        }
        loadWebAssembly('./test.wasm')
            .then(instance => {
                fib = instance.exports.fib;
            });
        
        function perforweb(n){
            console.log('webAssembly demo 开始');
            let startTime = performance.now();
            let c = fib(n);
            let endTime = performance.now();
            console.log(`webAssembly耗时 ${endTime - startTime} 毫秒,最终结果为 ${c}`);
            return true;
        }
        function perforjs(n){
            console.log('javascript demo 开始');
            let startTime = performance.now();
            let j= fibjs(n);
            let endTime = performance.now();
            console.log(`javascript耗时 ${endTime - startTime} 毫秒, 最终结果为 ${j}`);
        }
    </script>
</body>

</html>
  1. 安装live-server,本地起服务
npm install -g live-server
  1. 浏览器控制台中分别执行2个函数


    image.png

可以看到执行时间的对比,webAssembly在计算大数据量,存在大量递归,遍历的操作时,性能提升非常明显。


image.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容