浅谈WebAssmebly,在浏览器上进行图像处理

浅谈WebAssmebly,在浏览器上进行图像处理

浅谈WebAssmebly,在浏览器上进行图像处理_第1张图片

自从2008年各大浏览器为JavaScript执行引擎加入JIT(Just in Time, 即时编译器)以来,JavaScript在浏览器的执行性能有了一个量级的飞跃提升,促进了Web领域的飞速发展,一大波大型Web应用丰富了我们的网络世界。但是,JIT带来的性能提升也有天花板,在处理复杂运算的情况下表现仍有不足,为了更进一步解决JavaScript的性能问题,WebAssembly就应运而生了。
(本文非例子部分的图片出自文章1. https://hacks.mozilla.org/2017/02/a-cartoon-intro-to-webassembly/,文字部分基本参考该文章)

1. WebAssembly简介

1.1 什么是WebAssembly?

1.2 谁做的?

1.3 JavaScript性能历史

2. WebAssembly如何工作

3. 为什么WebAssembly更快?

3.1 JIT原理

3.2 JIT与WebAssembly的时间耗时对比

4. WebAssembly初体验

4.1 调用C函数

4.2 在浏览器上进行图像处理

5. 总结

Refrence

WebAssembly简介

什么是WebAssembly?

首先我们先简单了解一下什么是WebAssembly。

  • 它是一种新的、跨平台的web编译技术,可以让众多静态语言编译出浏览器可以运行的代码文件,文件格式为wasm,是一种相当接近机器语言的二进制格式文件;
  • 相比于文本格式的JavaScript文件来说有着体积小、下载速度快的特点;
  • 因为是从强类型语言编译的,所以JavaScript引擎不用猜测变量类型,也不用经历弃优化、重优化步骤,所以代码运行得更快;
  • 这种技术出现,不是为了替代JavaScript,而是作为JavaScript的一个良好补充

谁做的?

WebAssembly是我们熟知的四大主流浏览器厂商 Google Chrome、Apple Safari、Microsoft Edge 和 Mozilla FireFox 共同推动的,他们觉得Mozilla FireFox所推出的asm.js很有前景,为了让大家都能使用,于是他们就共同参与开发,基于asm.js制定一个标准,也就是WebAssembly。
浅谈WebAssmebly,在浏览器上进行图像处理_第2张图片

2017 年 3 月份, 四大厂商均宣布已经于最新版本的浏览器中支持了 WebAssembly 的初始版本,这意味着 WebAssembly 技术已经实际落地,可以在特定生产环境进行尝试。

JavaScript性能历史

WebAssembly的出现并不是偶然,让我们回顾一下JavaScript的性能发展史。
1995年,大神布兰登·艾奇仅仅花了10天就将我们伟大的JavaScript撸出来了。

浅谈WebAssmebly,在浏览器上进行图像处理_第3张图片
(蜜汁微笑)

当初语言的设计初衷是想设计出一个面向非专业编程人员和网页设计师的解释型语言。由于设计时间太短,语言的一些细节考虑得不够严谨,导致留下了不少坑,后来的很长一段时间,JavaScript的执行速度一直备受诟病。直到2008年,浏览器的性能大战打响,众多浏览器引入了即时编译(也就是我们熟知的JIT编译器),使得JavaScript运行速度快了一个量级。(大拐点)
浅谈WebAssmebly,在浏览器上进行图像处理_第4张图片

也是由于JIT的出现,使得JavaScript的应用范围从浏览器走到了服务器领域、桌面应用、移动应用等领域,大放异彩。
但是我们对性能的期望是无尽的,JIT带来的性能提升也早已因为JavaScript的动态特性达到了性能天花板,在复杂运算上仍然力不从心,特别在当前Web游戏的日益增多的情况下,问题显得日益突出。
那么我们很可能会有这样的疑问:下一个性能提升拐点是什么呢?
浅谈WebAssmebly,在浏览器上进行图像处理_第5张图片

很有可能就是WebAssembly

WebAssembly如何工作

工作原理:WebAssembly的工作原理简要来说是将C,C++, Rust等静态语言的程序编译成浏览器能够运行的wasm二进制文件,当浏览器加载wasm文件后编译为本地机器码后运行。

我们知道大多数静态高级语言是通过编译器前端编译成为中间代码,然后再由编译器后端把中间代码翻译成目标机器的可执行机器码的。
浅谈WebAssmebly,在浏览器上进行图像处理_第6张图片

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

那么如何编译出wasm文件呢,目前支持度最好的编译工具链是LLVM,Emscripten就是当前最常用一个使用LLVM、开源的SDK,前端采用clang,用clang将C代码变为LLVM中间代码,之后LLVM会对这些代码进行优化,而后端就是一个名为Fastcomp的LLVM 后端,它既可以直接wasm,也可以先生成asm.js,再转为wasm。
浅谈WebAssmebly,在浏览器上进行图像处理_第8张图片

对于以下的一个C函数,通过Emscripten sdk就能生成二进制格式的wasm文件了。

int add42 (int num) {
  return num + 42;
}

用编辑器打开wasm文件就可以看到熟悉的一堆十六进制数:

00 61 73 6D 0D 00 00 00 01 86 80 80 80 00 01 60
01 7F 01 7F 03 82 80 80 80 00 01 00 04 84 80 80
80 00 01 70 00 00 05 83 80 80 80 00 01 00 01 06
81 80 80 80 00 00 07 96 80 80 80 00 02 06 6D 65
6D 6F 72 79 02 00 09 5F 5A 35 61 64 64 34 32 69
00 00 0A 8D 80 80 80 00 01 87 80 80 80 00 00 20
00 41 2A 6A 0B

这里需要简单提一下,wasm文件还有一种等价的表达方式,就是下图中最下边的文本格式,是用S-表达式来表示的,它是一种源代码到WebAssembly的中间状态,让我们能够更好地读懂wasm文件,调试、或者徒手撸出WebAssembly,因为S-表达式与WebAssembly一一对应,所以可以很好地相互转化。在浏览器加载wasm文件并编译完成后会生成相应文本格式,在控制台中可以查看WebAssembly的文本格式文件wast。
浅谈WebAssmebly,在浏览器上进行图像处理_第9张图片

为什么WebAssembly更快?

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

JIT原理

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

首先JavaScript引擎的工作就是把我们看得懂的编程语言转换成机器能看懂的语言,我们与机器的沟通介质就是JavaScript引擎,没有这个翻译官,我们下达的命令机器就没法理解和执行。
浅谈WebAssmebly,在浏览器上进行图像处理_第10张图片

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

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

解释器是一部分一部分地边解释边执行。
浅谈WebAssmebly,在浏览器上进行图像处理_第11张图片

编译器是提前把源代码整个编译成目标代码,直接在支持目标代码的平台上运行,执行过程不需要编译器。
浅谈WebAssmebly,在浏览器上进行图像处理_第12张图片

两种方法各有利弊:
解释器好处除了易于实现跨平台外,最直接的就是对于我们前端开发人员来说,调试页面时,修改一行代码可以立即看到结果,不需要等待编译过程。但是对于同样的代码需要执行多次的情况弊端就很明显了,它需要执行多次的解释,就比如说执行循环。
编译器则可以在编译的时候可以对这些重复的代码等进行优化,使得执行得更快。由于花了许多时间在提前优化上,所以相对地需要牺牲的就是编译代码的时间。

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

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

浅谈WebAssmebly,在浏览器上进行图像处理_第13张图片

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

浅谈WebAssmebly,在浏览器上进行图像处理_第14张图片

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

浅谈WebAssmebly,在浏览器上进行图像处理_第15张图片

在优化编译器中,整个函数被编译在一起,并按照一个特定形式来优化。不过优化编译器需要一些前提,就比如它会假设由同一个构造函数生成的实例都有相同的属性名,并且都以同样的顺序初始化,那么就可以针对这一模式进行优化。也就是说,监视器根据代码执行情况做出假设,如果某个循环中先前每次迭代的对象都有相同的形状,那么它就会假设以后迭代的对象的形状都是相同的。监视器把这些信息提供给优化编译器作为前提,进行优化。

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

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

浅谈WebAssmebly,在浏览器上进行图像处理_第16张图片

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

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

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

所以,如果类型不固定,使用的是基线编译版本,每次循环都要进行一次类型检查。
浅谈WebAssmebly,在浏览器上进行图像处理_第18张图片

如果类型固定,使用的是优化编译器后的版本,在循环之前就进行一次类型检查就可以了,速度提升是相当显著的。
浅谈WebAssmebly,在浏览器上进行图像处理_第19张图片

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

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

  • JIT 编译器花了很多时间在猜测 Javascript 中的类型。
  • 在优化和去优化过程中造成了很大开销

而使用WebAssembly的出现的原因之一,就是为了消除这些开销。

JIT与WebAssembly的时间耗时对比

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

  • 一个是对JavaScript源码进行解释,生成抽象语法树或者字节码(parse
  • 一个是JIT编译器对那些热点代码的编译优化所花的时间(compile + optimize
  • 一个是当发生去优化时,重新优化所花的时间(re-optimize
  • 一个是执行代码的时间(execute
  • 再一个是就是GC垃圾回收所化的时间(garbage collection

浅谈WebAssmebly,在浏览器上进行图像处理_第20张图片

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

而需要提及的是,过去没有JIT时,JavaScript的执行时间需要更多的时间,就如同下图所示:
浅谈WebAssmebly,在浏览器上进行图像处理_第21张图片

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

1. 首先是解释源代码的时间。

JavaScript源代码需要由解释器生成抽象语法树后再转为中间代码(字节码),但是WebAssembly不一样,它本来就是一种中间代码,只不过比字节码更接近机器码,所以JavaScript引擎在这里不需要复杂地解析,只是解码并确保wasm没有错误即可。
浅谈WebAssmebly,在浏览器上进行图像处理_第22张图片

2. 然后是编译和优化的时间。

之前说过JIT会在JavaScript执行时进行热点代码的编译优化,相同代码对于不同变量类型会有不同的编译版本,于此相对的,WebAssembly因为是静态语言编译来的,编译优化前就已知变量类型,不需要编译多个版本,并且由于LLVM已经帮忙做了许多优化,所以在这个阶段JavaScript引擎消耗的时间是很少的
浅谈WebAssmebly,在浏览器上进行图像处理_第23张图片

3. 然后就是重新优化的时间。

在该阶段,在类型变化时JIT会有两大时间开销,第一就是从优化编译版本回退到基线编译版本的时间开销,第二则是再次检测到这段代码被多次调用,再次送到优化编译器进行优化而造成的反复去优化和重优化所造成的时间开销,而WebAssembly由于静态类型则完全不会造成这些开销。
浅谈WebAssmebly,在浏览器上进行图像处理_第24张图片

4. 然后在执行阶段。

在执行阶段中,如果了解JIT的优化机制的话,我们开发人员能够根据其写出执行效率更高的代码,但是这样一来代码的可读性将会收到很大的影响,因为适合JIT编译器优化的代码,对于常见的实现代码来说就如同hack一样,并且不同浏览器的JIT一般只对自己的浏览器做优化,在这个浏览器效率高,在另一个就不一定。但是WebAssembly就不一样,它是浏览器无关的,我们不需要懂得太多的编译器技巧,它就帮我们把所有优化做好了,并且提供了更理想的指令给机器,所以执行阶段的效率也是比原生JavaScript要高的。
浅谈WebAssmebly,在浏览器上进行图像处理_第25张图片

5. 最后,对于垃圾回收阶段。

为了追求性能,当前WebAssembly并不提供GC,除了基本类型都需要手动控制内存,以开发成本换取运行性能。不过,当前官方正在进行GC的讨论
浅谈WebAssmebly,在浏览器上进行图像处理_第26张图片

因此,最后WebAssembly就只有解码编译优化执行这三部分开销,对比原生性能开销减少了许多。对比如图所示
浅谈WebAssmebly,在浏览器上进行图像处理_第27张图片

当然,还有一个需要提及的是,wasm是二进制文件,比高度压缩过的JavaScript代码更小,在网络传输上是更快的。

所以这就是WebAssembly在大多数情况下比JavaScript更快的原因。

WebAssembly初体验

我们接下来用Emscripten SDK 和 c/c++简单展示一些小例子。首先需要去Emscripten官网参照教程下载安装并配置开发环境。配置好开发环境后,我们就可以在命令行中运行emcc命令编译我们的c/c++代码了。

调用C函数

我们首先展示C语言编写的斐波那契数列(递归)是如何编译生成wasm文件,并且在浏览器中是如何调用的。在某一目路中,C文件fib.c如下:

#include 

int fib(int n) {
    return n <= 1
        ? 1
        : fib(n - 1) + fib(n - 2);
}

执行emcc命令:

emcc fib.c -o fib.js -s WASM=1 -s EXPORTED_FUNCTIONS="['_fib']"

-o表示输出的文件名,-s后紧跟编译的配置(setting),所有配置项可以在这里查看。EXPORTED_FUNCTIONS表示我们要导出的方法。

以上命令将会生成一个fib.js文件,和一个fib.wasm文件,其中fib.js是胶接代码,是由于当前WebAssembly还未完成,提供的解析WebAssembly模块的API还未成熟,使用起来较为复杂,因此Emscripten在编译输出wasm的同时为我们提供了胶接代码。这也是为什么命令行输出文件名为fib.js而不是fib.wasm的原因。

Emscripten提供该胶接代码有一个模板,称为preamble.js (docgithub),对于不同编译输出,都会根据该模板生成不同的胶接代码。

生成的胶接代码在浏览器中将会使用 fetch api 或者 ajax 的方式获取wasm文件,并构造一个模块,该模块不仅提供了wasm中导出的方法,也提供了一些有用的JavaScript与WebAssembly的类型的相互转化方法(例如_malloc_freestringToCarrayToC等,这些函数可以将JavaScript的ArrayBuffer转为WebAssembly代码函数能够接受的指针等),建立起JavaScript与WebAssembly相互调用的桥梁,并在window上挂载该模块。

以下是当前较为常用地获取WebAssembly文件的方法,胶接代码也是使用这一个方法:

function loadWebAssembly (filename, imports = {}) {
  return fetch(filename)
    .then(response => response.arrayBuffer())
    .then(buffer => {
      imports.env = imports.env || {}
      Object.assign(imports.env, {
        memoryBase: 0,
        tableBase: 0,
        memory: new window.WebAssembly.Memory({ initial: 256, maximum: 256 }),
        table: new window.WebAssembly.Table({ initial: 0, maximum: 0, element: 'anyfunc' })
      })
      return window.WebAssembly.instantiate(buffer, imports)
    })
    .then(results => {
      return results.instance
    })
}

该方法通过fetch拉取二进制文件,获得相应的ArrayBuffer,并在本地浏览器进行编译解码,实例化模块,即可调用实例化后的WebAssembly模块上导出的字段、方法、类等。其中WebAssembly.instantiate是实例化方法,WebAssembly.Memory用来方法生成一块可变的ArrayBuffer内存,用于JavaScript与WebAssembly共同访问,主要用来传递指针等,WebAssembly. Table则用来生成一个存储函数引用的类数组结构。

在html中通过script标签将胶接代码引入:

<script src="static/fib/fib.js">script>

当其运行完毕后,我们将可以通过window.Module(默认挂载字段名为Module)对象访问各种字段、方法:

浅谈WebAssmebly,在浏览器上进行图像处理_第28张图片

我们WebAssembly模块的方法fib也可以获取:

浅谈WebAssmebly,在浏览器上进行图像处理_第29张图片

只不过方法是以_开头的,这是Emscripten的要求的,通过EXPORTED_FUNCTIONS导出的方法需要以下划线开头。于是我们可以如下调用fib函数:

浅谈WebAssmebly,在浏览器上进行图像处理_第30张图片

当然,如果我们写的c文件中如果要导出许多函数呢?这样在命令行中写一长串函数列表在EXPORTED_FUNCTIONS中是不是相当的麻烦呢?因此Emscripten给我们提供了一个很有用的宏定义EMSCRIPTEN_KEEPALIVE,该定义将使得LLVM不删除该函数,等同于导出该函数(EXPORTED_FUNCTIONS),所以fib.c可以写成这样:

#include 
#include 

int EMSCRIPTEN_KEEPALIVE fib(int n) {
    return n <= 1
        ? 1
        : fib(n - 1) + fib(n - 2);
}

命令行则可以省略EXPORTED_FUNCTIONS

emcc fib.c -o fib.js -s WASM=1

通过上述步骤,我们就可以在浏览器中调用c语言提供的函数了,一个简单的小例子就此完成~

在浏览器上进行图像处理

上述例子只是简单展示了在浏览器上如何调用WebAssembly函数,但是我们并没有展示出WebAssembly是怎样快速而又强大的。因此,接下来我们将展示在浏览器上进行图像处理,并比较JavaScript与WebAssembly的处理速度。

首先,说到图像处理,我们不得不提到著名的计算机视觉库OpenCV,它轻量级而且高效,由C++编写而成,并提供了Python、MATLAB、Java等语言的接口,实现了图像处理中的许多常用的通用算法和先进的算法。在这里,我们将通过Emscripten将OpenCV的部分接口和处理函数编译为WebAssembly文件,并在浏览器中进行调用,同时,我们也将实现JavaScript版本的处理方法,与WebAssembly进行对比。

在上一个例子中,我们实现的是JavaScript调用C函数,然而它并不适用于name mangled的C++函数,Emscripten提供了两个工具来实现C++和JavaScript的绑定,WebIDL BinderEmbind。两个工具让我们能够让C++的代码能够像JavaScript那样进行调用,WebIDL Binder支持能够被WebIDL表示的C++类型,而Embind则支持几乎所有的C++代码。WebIDL Binder的支持度较Embind是少许多的,不过在大多数情况下,它是足够适用的,例如一些有趣的物理引擎项目Box2D和Bullet(ammo.js),就是使用了WebIDL BinderEmbind则是受Boost.Python所启发,提供了更好的JavaScript到C++的桥梁,除了能够在JavaScript中任意调用C++提供的字段、函数、类等,还能反过来在C++中调用JavaScript,在这个例子中,我们使用Embind来完成绑定。

在编写绑定的cpp文件(bindings.cpp)时,我们需要添加如下代码:

#include 
using namespace emscripten;

包含Embind工具的头文件,并声明使用emscripten命名空间。这时,我们就可以在代码中使用各种绑定函数了。绑定需要使用EMSCRIPTEN_BINDINGS()代码块。

EMSCRIPTEN_BINDINGS() {}  // 在当前代码块中进行一组各种类、函数的绑定,该组中各绑定目标之间有一定的关联关系,调用时需提供一个标签名name来标记该组绑定。

例如C++类的绑定,我们可以:

#include 
using namespace emscripten;

class CppClass {
public:
  CppClass(int x, std::string y)
    : x(x)
    , y(y)
  {}

  void incrementX() {
    ++x;
  }

  int getX() const { return x; }
  void setX(int x_) { x = x_; }

  static std::string getStringFromInstance(const CppClass& instance) {
    return instance.y;
  }

private:
  int x;
  std::string y;
};

EMSCRIPTEN_BINDINGS(my_module) {
    class_("JsUseClass")  // 绑定C++类CppClass,命名为"JsUseClass"
      .constructor<int, std::string>()  // 构造函数接收参数类型为[int, string]
      .function("incrementX", &CppClass::incrementX)  // 绑定C++类CppClass的成员函数incrementX,命名为"incrementX"
      .property("x", & CppClass::getX, &CppClass::setX)  // 绑定C++类CppClass的属性x,命名为"x"
      .class_function("getStringFromInstance", &CppClass::getStringFromInstance);  // 绑定C++类CppClass的静态函数getStringFromInstance,命名为"getStringFromInstance"
}

对于一些结构体数据,Emscripten提供了value_arrayvalue_object来让我们将它们转化为JavaScript的数组和对象:

#include 
using namespace emscripten;

struct Point2f {
    float x;
    float y;
};

struct PersonRecord {
    std::string name;
    int age;
};

PersonRecord findPersonAtLocation(Point2f);

EMSCRIPTEN_BINDINGS(my_value_example) {
    value_array("Point2f")
        .element(&Point2f::x)
        .element(&Point2f::y)
        ;

    value_object("PersonRecord")
        .field("name", &PersonRecord::name)
        .field("age", &PersonRecord::age)
        ;

    function("findPersonAtLocation", &findPersonAtLocation);
}

对于常量和枚举,则可以:

#include 
using namespace emscripten;

enum class NewStyle {
    ONE,
    TWO
};
const int SOME_CONSTANT = 666;

EMSCRIPTEN_BINDINGS(my_enum_example) {
    enum_("NewStyle")
        .value("ONE", NewStyle::ONE)
        .value("TWO", NewStyle::TWO)
        ;

    constant("SOME_CONSTANT", SOME_CONSTANT);
}

Emscripten Embind还提供了typed_memory_viewval两个方法,typed_memory_view用于在JavaScript中能够将C++中的内存指针作为TypeArray来访问,而val则用于在C++中能够直接访问JavaScript中的对象,进行各种操作。

更多的用法可以去官方文档、官方API和头文件定义查阅。

编写好绑定文件(bindings.cpp)后,我们执行以下命令进行编译即可:

emcc --bind -o bindings.js bindings.cpp -s WASM=1

--bind表示使用Embind来建立C++与JavaScript的绑定。输出同样为bindings.js和bindings.wasm两个文件。

对于C++类的绑定的例子来说,我们绑定了C++类CppClass并命名为”JsUseClass”,让JavaScript可以通过JsUseClass来调用。在html中使用script标签引入bindings.js脚本之后我们可以在控制台中可以看到:

浅谈WebAssmebly,在浏览器上进行图像处理_第31张图片

可以看到,我们能够使用C++所实现的类CppClass的任何属性和方法。

所以,通过Emscripten和它的Embind工具,我们能够将C++项目编译成WebAssembly,在浏览器中调用。

当然OpenCV也不例外,将OpenCv编译成WebAssembly后,我们就可以在浏览器中进行复杂地图像处理操作了。诚然,将整个OpenCV编译成WebAssembly是一件不容易的事情,不仅需要将每一个接口进行绑定,还需要花大量时间在配置编译参数上。因此,我们可以仅导出OpenCV通用的一些类型如MatSizePoint等,和我们所需要的处理函数,如Cannydft等,而在配置编译参数上,我们可以利用Emscripten提供的一些python工具类如shared.py等,来帮助我们构造编译脚本。

对于OpenCV编译到WebAssembly,github上已经有一个项目opencvjs实现了,作者正是利用Embind工具和上述的shared.py来完成编译的,由于篇幅关系,本文中不一一说明其编译的步骤,绑定程序和编译脚本可以给我们作为一个很好的例子。

为了对比JavaScript和WebAssembly的性能,我们在浏览器中进行几个算法的对比。

注意

  1. 实现的算法不完全一致,JavaScript中可能无法使用OpenCV中的优化方法;
  2. 耗时中,包括了由ImageData转为OpenCV的Mat或JavaScript数组以供图像处理的时间,OpenCV WebAssembly版本包括了用JavaScript处理由Mat转为ImageData以供canvas显示的时间,而JavaScript版本并不包括,体现在一些简单操作上JavaScript版本稍快;
  3. JavaScript方法和WebAssembly方法都跑在WebWorker中

机器参数
Mac mini (Late 2014)
处理器 2.6 GHz Intel Core i5
内存 16 GB 1600 MHz DDR3
显卡 Intel Iris 1536 MB

浏览器
Google Chrome 版本 61.0.3163.100(正式版本) (64 位)

图像大小
256 * 256

图片灰度化
浅谈WebAssmebly,在浏览器上进行图像处理_第32张图片

将彩色图处理为灰度图像,该处理较为简单,从结果中可以看到OpenCV WebAssembly版本速度稍快,但优势不明显。

sobel算子
浅谈WebAssmebly,在浏览器上进行图像处理_第33张图片

一种边缘提取算法,由于需要进行卷积计算,处理较为复杂,可以看到OpenCV WebAssembly版本明显快了许多。

图像频域低通滤波
浅谈WebAssmebly,在浏览器上进行图像处理_第34张图片

使用快速傅里叶变换对图像进行频域处理,频域图像中中心部分是低频信息,对应图像的概貌,频域图像离中心远的部分是高频信息,对应图像的细节,过滤掉一些高频信息后逆变换得到的处理结果图片,可以看到肉眼是很难区分处理结果与原图的区别的,而结果与原图作差后,再进行对数处理,可以看到有一定的细节信息出现在difference图中。快速傅里叶变换是一种较为复杂的算法,可以看到,OpenCV WebAssembly版本在频域处理阶段和逆变换阶段中都明显快了许多。

图像频域添加盲水印
浅谈WebAssmebly,在浏览器上进行图像处理_第35张图片

图像频域处理有很多作用,可以实现一些空间域上难以实现的处理,包括滤波、去噪、增强等,当然也可以添加频域盲水印。我们这里实现了非常简单低配的水印嵌入,仅仅是在频域图像上的高频部分加入了’666666’等字符,可以看到逆变换后的处理结果图片与原图在肉眼上根本分不清区别,我们的超低配版盲水印成功的嵌入了图片中,只有通过频域正变换才能够看到(需要指出的是,这里的盲水印例子仅仅是为了展示,真实的频域盲水印并不是这样处理的,它更为复杂,不仅可能会在频域处理中引入小波变换,对水印进行加密而不是直接出现在频域图中,并且在识别时也可能不允许通过对比原水印信息来恢复水印,而是通过特征阈值来从水印库中识别出来)。同样的,OpenCV WebAssembly版本妥妥的胜出,完全可以在浏览器中做到实时处理。

通过几个例子可以看到,OpenCV WebAssembly版本的性能相当强劲,虽然例子中JavaScript的速度稍慢有一部分原因是实现方法不够优秀,但是有一点毋庸置疑的是,通过WebAssembly,我们可以直接使用C++大量的优秀项目,而不需要使用JavaScript重新制作轮子,同时享受WebAssembly带来的性能的提升,在浏览器中进行更多的复杂运算,做更多有趣的事情,除了我们提到的图像处理,还包括视频解码、WebVR/AR、Web游戏、加密算法等等。以下再列举网上几个WebAssembly的例子:

Unity tutorial game
人脸、人眼检测
game ZenGarden
asm-dom WebAssembly虚拟DOM
jq-web JSON命令行处理器jq的WebAssembly版本

总结

总的来说,WebAssembly是一种能够由多种静态语言编译而成的一种新的二进制格式代码,它的目的是能够被浏览器fetch更快,在浏览器执行得更快,以实现更多复杂的应用,并且能够在浏览器与JavaScript能够友好地相互协作,作为JavaScript的一个良好补充。随着WebAssembly的不断改进和发展,越来越多的静态语言能够参与到Web的开发中来,与JavaScript相互协作,浏览器似乎真的越来越像一个小型的“操作系统”了。

Refrence

  1. https://hacks.mozilla.org/2017/02/a-cartoon-intro-to-webassembly/
  2. https://webassembly.github.io/spec/intro/index.html
  3. https://github.com/kripken/emscripten/wiki
  4. http://webassembly.org/docs/high-level-goals/
  5. https://www.zhihu.com/question/31415286
  6. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly

你可能感兴趣的:(浅谈WebAssmebly,在浏览器上进行图像处理)