3.2 发挥WebAssembly的威力
WebAssembly
对于在JavaScript
中重新创建Photoshop
的计算密集型图形处理是一个不可或缺的要素。Adobe
使用Emscripten
编译器将他们现有的C/C++代码库移植到WebAssembly
模块中。
兼容性
还是熟悉的配方,我们通过caniuse
来查看,桌面浏览器对WebAssembly
的支持程度。哇塞,形势一片大好。各大厂商都意识到这个神兽能给我们带来更多意想不到的可能性。
其实,暂且不看市面上公司如何使用,从各个厂商的积极程度也侧面反应了,我们的预期。
几个WebAssembly
的功能至关重要:
- 线程 -
Photoshop
使用工作线程以并行方式执行任务,比如处理图像块 SIMD
- SIMD矢量指令加速像素操作和过滤。- 异常处理 - C++异常广泛用于整个Photoshop的代码库。
- 流式实例化 -
Photoshop
的80MB+ WASM模块需要流式编译。 - 调试 - Chrome的
WebAssembl
y在DevTools
中的调试支持是非常有价值的。
如果大家对WebAssembly
还不是很了解的话,可以翻阅我们之前写的浏览器第四种语言-WebAssembly。针对wasm
的概念性东西这里就不在过多介绍了。在里面我们还介绍了利用Emscripten
实现了 将C/C++
代码编译为WebAssembly
并且,在我们之前还介绍过,C
与JS
代码之间的互操作。感兴趣的可以参看WebAssembly-C与JS互相操作。
而我们来讲讲SIMD
的东西。
SIMD
SIMD
代表单指令,多数据。是Single Instruction, Multiple Data
的缩写。SIMD操作这个术语指的是一种计算方法,它能够通过单个指令来处理多个数据。相比之下,传统的顺序方法使用一条指令来处理每个单独的数据,这被称为标量操作。
以简单的加法为例,下面说明了标量操作
和SIMD操作
之间的差异。
使用传统的标量操作
,必须依次执行四个加法指令才能获得如图(a)所示的总和。与此同时,SIMD
只使用一条加法指令就能获得相同的结果,如图(b)所示。由于处理相同数量的数据所需的指令更少,SIMD操作比标量操作具有更高的效率。
这里简单的说一句题外话:标量这个词是不是感觉似曾相识。其实,我们在介绍Rust
数据结构的时候就有过接触呢。
这下估计就知道标量操作就是单一操作了。
SIMD
指令是一类特殊指令,通过同时对多个数据元素执行相同的操作,来充分利用应用程序中的数据并行性。计算密集型应用程序,如音频/视频编解码器、图像处理器,都是利用SIMD
指令来加速性能的示例。
SIMD的限制
尽管SIMD
操作具有能够在一条指令中处理多个数据的优势,但它们只能应用于特定预定义的处理模式。下图展示了一个这样的模式,在该模式中,所有数据都执行相同的加法操作。
SIMD
操作不能用于以不同方式处理多个数据。下图中提供了一个典型的示例,其中一些数据需要相加,而其他数据需要相减、相乘或相除。
SIMD
操作就是需要所有数据都是执行相同的操作)
想了解更多关于SIMD
概念性的东西,可以参看SIMD基础介绍 (需要🪜)
由于SIMD
是wasm
的特性,而我们的关注点又是浏览器环境,我们就挑一个我们熟悉的浏览器chrome
来描述一下,所以我们看看V8
是如何支持这个特性的。(V8
和chrome/chromium
的关系我们之前聊过)
由于可编译为wasm
的语言过于多,例如C/C++/Rust
甚至TypeScript
。所以,我们就挑一个我们比较感兴趣的语言来说明。
将Rust代码编译为WebAssembly SIMD
当将Rust
代码编译为WebAssembly SIMD
目标时,我们需要启用与Emscripten
中的simd128 LLVM
特性相同的特性。
当启用msimd128
特性时,默认情况下启用LLVM
的自动矢量化器,即在优化级别-O2
和-O3
时启用。
如果可以直接控制rustc
标志或通过环境变量RUSTFLAGS
,可以传递-C target-feature=+simd128
:
RUSTFLAGS="-C target-feature=+simd128" cargo build
或者
RUSTFLAGS="-C target-feature=+simd128" cargo build
例如,考虑以下函数,该函数将两个输入数组的元素相乘并将结果存储在输出数组中。
pub fn multiply_arrays(out: &mut [i32], in_a: &[i32], in_b: &[i32]) { in_a.iter() .zip(in_b) .zip(out) .for_each(|((a, b), dst)| { *dst = a * b; }); }
如果没有传递msimd128
特性,编译器会生成以下WebAssembly
循环:
(loop (i32.store … 获取`out`中的地址 … (i32.mul (i32.load … 获取`in_a`中的地址 …) (i32.load … 获取`in_b`中的地址 …) … )
但是,当使用msimd128
特性时,自动矢量化器会将其转换为包含以下循环的代码:
(loop (v128.store align=4 … 获取`out`中的地址 … (i32x4.mul (v128.load align=4 … 获取`in_a`中的地址 …) (v128.load align=4 … 获取`in_b`中的地址 …) … ) )
循环体的结构相同,但在循环体内部使用SIMD指令加载、相乘和存储四个元素。
启发&应用场景
与其说是启发,倒不如说是,WebAssembly
能够给我们带来多大的惊喜。在之前的文章中,我们聊过WebAssembly
在哪些场景下能够大放异彩。 而今天既然聊到了WebAssembly -SIMD
,那我们到底看看它是否还有绝活。
WebAssembly SIMD
提案旨在加速高计算应用程序,如音频/视频编解码器、图像处理应用程序、加密应用程序等。
MediaPipe
是一个用于构建多模式(例如视频、音频、任何时间序列数据)应用机器学习管道的框架。
其中一个视觉效果最吸引人的演示,可以很容易地观察到SIMD带来的性能差异,是一个仅使用CPU
构建的手部跟踪系统。没有启用SIMD
时,在现代笔记本电脑上只能获得大约14-15帧每秒(FPS
),而在Chrome Canary中启用SIMD
后,您可以获得更平滑的体验,帧率可达38-40
帧每秒。
OpenCV
,这是一个流行的计算机视觉库,也可以编译成WebAssembly。
OpenCV
是Intel
开源计算机视觉库。它由一系列C 函数和少量C++ 类构成,实现了图像处理和计算机视觉方面的很多通用算法。
其中它有一个WebCamera的项目,如下图。做了一下用于图像和视觉处理的工具。
此图,就是用于用摄像头识别银行卡中的帐号数据,并对齐提取。
那是不是,在我们遇到类似的功能点时,我们在已经引入到对应的视觉处理工具后,效果还没达到我们的心里预期,那WebAssembly SIMD
不就有了用武之地了吗。
不仅,视觉处理,还有音频处理也是相同的道理。这里就不展开说明了。
3.3 将sRGB替换为P3
还有老样子,让我们can
一下。
DCI-P3
,有时也称为P3
或Display P3
,DCI
是Digital Cinema Initiatives
的缩写,是数字电影领域的一个标准。
sRGB
代表标准红绿蓝,是一种颜色空间,也是一组特定的颜色,由惠普和微软于1996
年创建,旨在标准化电子设备中显示的颜色。sRGB
是目前最流行的颜色空间,用于Windows
、大多数网络浏览器以及大多数控制台和个人电脑游戏,除非它们支持高动态(HDR
)。
显示器或其他设备的色域告诉我们设备可以重现哪个颜色空间,以及它可以在0-100%
甚至更高的范围内呈现多少颜色。除了sRGB
,其他常见的颜色空间还包括Adobe RGB
和P3
,它们都比sRGB
更大,即包含更多颜色。
下面是XY色度图
,代表了人眼可以看到的所有颜色范围。在那个颜色范围内,有一个白色的三角形,它勾勒出sRGB
标准所包括的颜色。例如,如果我们试图在sRGB
显示器上查看超出该三角形范围的颜色的图像,那些额外的颜色会显得不准确和饱和不足。
sRGB vs P3
虽然sRGB
是标准,但其他颜色空间也可能更具吸引力。P3
表现色域比sRGB
更大(覆盖更多颜色)。
P3
显示的色域比sRGB
显示器宽50%
。
白线显示了sRGB
的边缘。它上面和右边的部分是Display-P3
颜色,而这些颜色在sRGB中是不可用的。请注意,绿色扩展得很大,而蓝色扩展则远远不及。
sRGB
和P3
之间的另一个区别是P3
可以处理10位颜色。
Photoshop
使用新的color()
函数和Canvas
API来释放P3的全部光彩,实现更准确的颜色呈现。
color: color(display-p3 1 0.5 0)
上面的语法等同于旧有的css表示颜色的语法。
hsl(42, 70%, 50%)
rgb(3, 5, 11)
#abc
如果我们想使用p3
可以使用@supports
功能查询。
/* sRGB颜色。 */ :root { --bright-green: rgb(0, 255, 0); } /* Display-P3颜色,如果支持的话。 */ @supports (color: color(display-p3 1 1 1)) { :root { --bright-green: color(display-p3 0 1 0); } } header { color: var(--bright-green); }
启发
如果我们在项目中,想要实现一下对色彩饱和度有强烈要求的功能,那么我们就可以使用color(display-p3)
来增强我们的项目。虽然,写不出五彩斑斓的黑,红的发紫的白。但是这也算是我们扩展应用功能的一把利器。
好钢要用在刀刃上。
3.4 利用Web组件提升UI的灵活性
Photoshop
利用基于Lit
构建的标准化Web
组件策略,可以实现应用程序之间的UI一致性。
Photoshop
的用户界面元素来自Adobe
的Spectrum Web Components库,该库实现了Adobe
的设计系统。
Spectrum Web Components
具备以下特点:
- 默认支持辅助功能 — 开发时考虑了现有和新兴的浏览器规范,以支持辅助技术。
- 轻量级 — 使用
LitElement
实现,减小了额外开销。 - 基于标准 — 基于
Web Componment
标准,如自定义元素和Shadow DOM
构建。 - 框架无关 — 由于浏览器级别的支持,可以与任何框架一起使用。
此外,整个Photoshop
应用程序都是使用基于Lit
的Web
组件构建的。Lit
的模板和虚拟DOM diff
使得UI更新更加高效。Web组件的封装性也使得在需要时可以轻松集成来自其他团队的React代码。
我们在之前的文章中也有对Lit
有过涉猎,本人也在项目中也有对应的简单应用。咋说呢,感觉Lit
的封装还是很nice
的。但是,如果硬要刨根问底的话,其实还是要从Web Componment
来讲。
如果想了解Lit
可以通过Lit 官网去学习和查阅。这里就不在多讲。
虽然,现在Vue/React
等,框架大行其道,但是Web Componment
中的一些理念和使用方式是最贴合浏览器渲染机制的。然后,由于篇幅有限,我们打算,针对Web Componment
会做一次深度的解析和教学。
启发
虽然,不知道PS
团队,不知出于何种目的,选用了Lit
作为了构建前端页面的UI库。但是,想象一下,如果现在我们有一个大前端团队,由于历史包袱原因,即有React
,又有Vue
团队。现在有一个庞大的功能需求需要全团队配合去做。那是不是利用Lit
或者Web Componment
可以解决这个痛点。
但是呢,使用Lit
只是一个跨语言合作的一个可行方案,我相信市面上肯定有很多解决方案,例如:Veaury
还有很多巨石应用
的解决方案。这里也不过多展开了。
4. 优化Photoshop在浏览器中的性能
尽管新的Web功能提供了基础,但像Photoshop
这样的高强度桌面应用程序仍然需要进行大量的跟踪和性能优化工作,以提供一流的在线体验。
4.1 使用Service Workers缓存资源和代码
Service Workers
允许Web应用将其资源、代码和其他资源本地缓存,以在初始访问后加载速度更快。虽然它还不是一个完全离线可用的应用程序,但Photoshop
已经利用Service Workers
来缓存其WebAssembly
模块、脚本和其他资源。
Adobe
使用Workbox库更轻松地将Service Worker
缓存集成到他们的构建过程中。
针对Worker
呢,我们之前的文章就有过介绍。
分别对Web Worker
和Service Worker
做了一次比较透彻的分析。
然后呢,其实在Service Worker
始终有一种欲罢不能的感觉。就像上面说的,Photoshop
是借助了Workbox
实现了资源的本地缓存。同时,我也是第一次听说这个技术(Workbox
),然后就这两天开始着手找资料学习和研究。发现,其中的很多点都很喜欢,然后也是有很多的点可讲。但是呢,由于现在有些东西还未亲身实践,所以有些想着,等我实践完然后准备写一篇或者多篇的文章,深度解析一下这个东西。
在这里,我就偷个懒了哈,不过更文列表中已经有计划了。优先级还很高呢。
4.2 V8对已缓存资源的优化
当资源从Service Worker
缓存中返回时,V8会进行一些优化:
- 在安装期间缓存的资源会被急切地编译,并立即进行代码缓存,以保持一致、快速的性能。
- 通过Cache API缓存的资源在第二次加载时进行了优化缓存,比通常的缓存速度更快。
- V8会检测已缓存资源的重要性,并更积极地进行编译。
这些优化允许对Photoshop
庞大的缓存Wasm模块
进行优化。
4.3 流式传输和缓存大型WebAssembly模块
Photoshop
的代码库需要多个大型WebAssembly
模块,其中一些超过80MB
。V8
和Chrome
中的流式编译支持使这些大型模块可以在性能方面进行有效处理。
也就是说,V8
不用讲wasm
模块全盘接收,再开启编译模式。这样在很大程度上节省了时间,尤其在遇到大文件的时候。
此外,第一次从Service Worker
请求WebAssembly
模块时,V8会生成并存储一个优化版本以进行缓存,这对于Photoshop
庞大的代码尺寸至关重要。
4.4 用于并行图形操作的多线程
Photoshop
中的许多核心图像处理操作,如像素变换,可以通过跨线程并行执行来大幅加速。WebAssembly
的线程支持可以利用多核设备进行计算密集型图形任务。
这允许Photoshop
在转移到WebAssembly
后,使用与桌面相同的多线程方法来处理性能关键的图像处理函数。
4.5 用于优化的WebAssembly调试
在开发过程中,强大的WebAssembly
调试支持对于诊断和解决性能瓶颈至关重要。
Chrome DevTool
的能力可以对WASM
代码进行性能分析、设置断点,并检查丰富的变量,这与JavaScript的调试性质相似。
5. 使用TensorFlow.js集成本地设备上的机器学习
最近版本的Web
上的Photoshop
包括使用TensorFlow.js
的AI功能。在设备上运行模型而不是在云端改善了隐私、延迟和成本。
TensorFlow.js是Google
推出的针对JavaScript
开发者的开源机器学习库,能够在浏览器中客户端运行。它是用于Web机器学习的最成熟选择,具有全面的WebGL
和WebAssembly
后端操作支持,未来还将提供WebGPU后端选项,以在浏览器中获得更快的性能,以适应新的Web标准的发展。"
"选择主题"功能利用机器学习自动提取图像中的主要前景对象,大大加速了复杂的选择操作。
为了实现本地执行,该模型从TensorFlow
转换为TensorFlow.js
// 加载“选择主题”模型 const model = await tf.loadGraphModel('select_subject.json'); // 在图像张量上运行推理 const {mask, background} = model.execute(imgTensor);
Adobe和Google合作开发了一个用于Emscripten的代理API,以解决Photoshop的WebAssembly代码和TensorFlow.js之间的同步问题,从而实现了这两个框架的无缝集成。
“由于Google团队通过其各种支持的后端(WebGL、WASM、Web GPU)提高了
TensorFlow.js
的硬件执行性能,这导致模型的性能改进在30%到200%之间(尤其是对于倾向于获得最大性能提升的较大模型而言),在浏览器中几乎实时性能。”
启发
这个点,算是继wasm
在浏览器中实现,又一个让我眼前一亮的特性。现在大家都在卷各种大模型,国内国外都是如此,我们公司也有自己的AI团队。而大部分的开发模式,基本上都是将AI模型
配置到后端,然后前端页面都是通过异步接口进行传值处理。其实这和旧有的前端开发模式没有任何的改变。
但是,在PS
团队实现了基于TensorFlow.js
的前端AI模型,那是不是变现的说,万物即可AI,并且在前端也会有一席之地。
等着,给我一段时间,到时候给大家出一篇在前端界面中使用TensorFlow.js
的教程。(这个是真心喜欢研究的东西)
后记
写到这里,其实里面的内容过于多,本来不想再继续聒噪了,但是还是有感而发。
人人都说前端已死,但是你如果看到上面的一些技术和特性,你还会有这种感觉吗。
不要学着别人去,自怨自艾,只有自己内心渴望一个东西,你才会有勇气和动力去面对和征服它。
分享是一种态度。
全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。