By Xulun (from F(x) Team)
As frontend pages become complex, the performance of Javascript has been criticized repeatedly. Javascript is not designed for performance optimization, which makes the local language that can run on the browser popular.
From the perspective of compatibility, WebAssembly has reached the 94.7% high coverage rate from canlUse data. This value is similar to Javascript's await support program. Browsers after 2017 are supported, which has been five years since then. Mainstream browsers are all supported, including Chrome, Chrome for Android, Android Browser, Safari, Safari on iOS, Edge, Firefox, and Opera.
Since there is a chance to land, we will start the ice-breaking journey of WebAssembly.
WebAssembly is an assembly language defined on an abstract machine. The browser is responsible for compiling it into a local code. The js engine (such as v8) is still responsible for this part of the work. Since it is an abstract machine, it can run across platforms. We can run with the interpreter provided by the toolchain or through the local Node.js.
Unlike x86 assembly, which uses imperative assembly language, WebAssembly is written using S expressions similar to the Lisp language. S expression is the language of statements enclosed in parentheses.
Look at a simple example:
(module
(func (result i32)
(i32.const 666)
)
(export "const_i32" (func 0))
)
The outermost parentheses are modules, and the code for WebAssembly is organized as a module. Functions can be defined through func in the module. The function has no name. If you want to give it an accessible name, bind it to a name through export.
It is known that compiling machine instructions in assembly language requires an assembler. Meanwhile, a lot of binary toolchains, such as objdump, disassembly tools, etc., are needed.
WebAssembly Community Group provided us with WebAssembly Binary Toolkit (wabt).
Wabt is a project with multiple Git libraries. You can download the code here:
git clone --recursive https://github.com/WebAssembly/wabt
cd wabt
git submodule update --init
Then, cmake can be used to compile:
$ mkdir build
$ cd build
$ cmake ..
$ cmake --build .
After successful compilation, tools (such as assembler wat2wasm) can be used.
For example, if the example in the previous section is saved as test001.wat, it can be compiled like this:
wabt/build/wat2wasm test001.wat
When the compilation succeeds, test001.wasm is generated.
We can use wasm-objdump to view the contents of wasm:
wabt/build/wasm-objdump -s -d -x test001.wasm
-d is the disassembly. -x is the display section details. -s is the display raw data.
The following is the output:
test001.wasm: file format wasm 0x1
Section Details:
Type[1]:
- type[0]() -> i32
Function[1]:
- func[0] sig=0 <const_i32>
Export[1]:
- func[0] <const_i32> -> "const_i32"
Code[1]:
- func[0] size=5 <const_i32>
Code Disassembly:
000026 func[0] <const_i32>:
000027: 41 9a 05 | i32.const 666
00002a: 0b | end
Contents of section Type:
000000a: 0160 0001 7f .`...
Contents of section Function:
0000011: 0100 ..
Contents of section Export:
0000015: 0109 636f 6e73 745f 6933 3200 00 ..const_i32..
Contents of section Code:
0000024: 0105 0041 9a05 0b ...A...
Wabt provides the wasm interpreter with wasm-interp. We can run all exported functions, for example:
wabt/build/wasm-interp --run-all-exports ./test001.wasm
The following is the output:
const_i32() => i32:666
Since WebAssembly is the standard, it can be used without installing any third-party packages.
Running a function in WebAssembly only takes three steps:
We only need to use the fs API to read the file and run it directly in the Node environment:
const {readFileSync} = require('fs')
const outputWasm = './test001.wasm';
async function run(){
const buffer = readFileSync(outputWasm);
const module = await WebAssembly.compile(buffer);
const instance = await WebAssembly.instantiate(module);
console.log(instance.exports.const_i32());
}
run();
As WebAssembly is the standard, the API usage in the browser is the same as in Node.js. The difference is how to read the wasm file.
Start a local server and use the fetch function to obtain the local wasm file:
<!DOCTYPE html>
<html lang="en">
<meta charset="utf-8" />
<head>
<script type="text/javascript">
function fetchAndInstantiate(url) {
return fetch(url)
.then((response) => response.arrayBuffer())
.then((bytes) => WebAssembly.instantiate(bytes))
.then((results) => results.instance);
}
function test001() {
fetchAndInstantiate("./test001.wasm").then((instance) => {
alert(instance.exports.const_i32());
});
}
</script>
</head>
<body onload="test001()">
<p> WebAssembly测试页</p>
</body>
</html>
The following result is returned:
Since it is an assembly language, it is easier to disassemble.
Wabt provides the wasm2wat tool to disassemble the wasm file:
wabt/build/wasm2wat test001.wasm
Look at the results of the disassembly:
(module
(type (;0;) (func (result i32)))
(func (;0;) (type 0) (result i32)
i32.const 666)
(export "const_i32" (func 0)))
The result of disassembling has one thing more than what we have written, which is the definition of the type. Since this type of assembler can be inferred, there is no need to write by hand.
Through disassembly, it is seen that functions have no name. Bind a symbol as a name only when you need to export it to an external call. Similarly, function parameters have no names. However, we can give a parameter when writing the assembly. In wat, function parameters should be declared with param keyword, and a name starting with $
can be given.
Look at an example:
(module
(func (param $a i32) (result i32)
(i32.add
(local.get $a)
(i32.const 1)
)
)
(export "inc_i32" (func 0))
)
The following is the result of disassembling with wasm2wat:
(type (;0;) (func (param i32) (result i32)))
(func (;0;) (type 0) (param i32) (result i32)
local.get 0
i32.const 1
i32.add)
(export "inc_i32" (func 0)))
It is found that the $a
parameter has disappeared. It does not exist at the time of declaration and becomes the serial number 0 at the time of the call.
In addition, changes are found in the order of instructions. The pre-order expression we use (or Polish expression) is that the i32.add instruction is in the front, and the two operands are in the back. After disassembly, it becomes the inverse Polish expression, namely, the post-order expression. The i32.add instruction is at its back, and its two operands are at the front.
WebAssembly, hereinafter referred to as wasm, has four types of numbers:
There are four complete instruction sets for these four types.
There is no unsigned number type, but the integer type is an instruction for unsigned calculation.
Corresponding to four basic number types, there are four instructions to push a constant of this type into the stack: i32.const, i64.const, f32.const, and f64.const.
There are four types of addition, one for each type:
There are also four types of subtraction, one for each type:
The same goes for multiplication:
Look at an example of f32 multiplication:
(module
(func (param $a f32) (result f32)
(f32.mul
(local.get $a)
(f32.const 1024)
)
)
(export "mul_1k_f32" (func 0))
)
Write a Node.js script and run:
const {readFileSync} = require('fs')
const outputWasm = './test003.wasm';
async function run(){
const buffer = readFileSync(outputWasm);
const module = await WebAssembly.compile(buffer);
const instance = await WebAssembly.instantiate(module);
console.log(instance.exports.mul_1k_f32(3.14));
}
run();
The division is simple for floating-point numbers, with only one div instruction:
Division is classified into signed division and unsigned division for integers:
In addition, there are two instructions for the signed remainder and unsigned remainder:
Let's take the 64-bit remainder as an example:
(module
(func (param $a i64) (param $b i64) (result i64)
(i64.rem_u
(local.get $a)
(local.get $b)
)
)
(export "rem_u_i64" (func 0))
)
When i64 type is running on Node.js, you need to enter BigInt and add an n suffix when entering:
const {readFileSync} = require('fs')
const outputWasm = './test_remu.wasm';
async function run(){
const buffer = readFileSync(outputWasm);
const module = await WebAssembly.compile(buffer);
const instance = await WebAssembly.instantiate(module);
console.log(instance.exports.rem_u_i64(1000n,256n));
}
run();
Absolute Value
Inverse
Rounds up or down to an Integer
Square Root
Maximum and Minimum
Take Sign Bit
Let's start with the unfamiliar copysign. It replaces the sign of the current number with the sign of another number.
(module
(func (param $a f64) (result f64)
(f64.copysign
(local.get $a)
(f64.const -1.0)
)
)
(export "copysign_f64" (func 0))
)
We are copying the symbol of -1.0 to the 64-bit integer from the copysign_f64 function.
Try 3.14:
const {readFileSync} = require('fs')
const outputWasm = './copysign.wasm';
async function run(){
const buffer = readFileSync(outputWasm);
const module = await WebAssembly.compile(buffer);
const instance = await WebAssembly.instantiate(module);
console.log(instance.exports.copysign_f64(3.14));
}
run();
The result of the operation was -3.14, which was replaced by the symbol of -1.0.
Only integers can judge whether it is 0, so it is an instruction for each of the two integers:
Look at an example:
(module
(func (param $a i32) (result i32)
(i32.eqz
(local.get $a)
)
)
(export "i32_eqz" (func 0))
)
Run it:
const {readFileSync} = require('fs')
const outputWasm = './cmp.wasm';
async function run(){
const buffer = readFileSync(outputWasm);
const module = await WebAssembly.compile(buffer);
const instance = await WebAssembly.instantiate(module);
console.log(instance.exports.i32_eqz(0));
console.log(instance.exports.i32_eqz(-1));
}
run();
The output is 1, 0.
Four Equal:
Four Unequal:
Floating point numbers are simple. Smaller than is lt and larger than is gt:
Integers are also divided into signed and unsigned:
If it is less than or equal to, replace lt with le. If it is greater than or equal to, replace gt with ge.
Although the function has no name, it can be marked with a reference and called by the call instruction.
Use i32_eqz2 to encapsulate the i32_eqz function of the preceding section.
(module
(func $f1 (param $a i32) (result i32)
(i32.eqz
(local.get $a)
)
)
(func (param $b i32) (result i32)
(call $f1 (local.get $b))
)
(export "i32_eqz2" (func 1))
)
Look at the result of the disassembly. The reference value is translated into the index number of the function:
(module
(type (;0;) (func (param i32) (result i32)))
(func (;0;) (type 0) (param i32) (result i32)
local.get 0
i32.eqz)
(func (;1;) (type 0) (param i32) (result i32)
local.get 0
call 0)
(export "i32_eqz2" (func 1)))
Wasm provides an if
instruction, which reads an integer of type i32 from the top of the stack. If the parameter is not 0, the code of then
is executed. Otherwise, the code of else
is executed.
Let's take an example and rewrite i32_eqz:
(module
(func (param $a i32)(result i32)
(local.get $a)
(if (result i32)
(then (i32.const 0))
(else (i32.const 1))
)
)
(export "i32_eqz3" (func 0))
)
if
can return to a value like a function and both then
and else
are required.
The requirement of if
is not immediate operand. It needs to be placed in the stack in advance to avoid an error.
Look at the results after disassembly. then
is not a keyword. The if...else...end
structure can also be used to write:
(module
(type (;0;) (func (param i32) (result i32)))
(func (;0;) (type 0) (param i32) (result i32)
local.get 0
if (result i32) ;; label = @1
i32.const 0
else
i32.const 1
end)
(export "i32_eqz3" (func 0)))
Write the following – making sure not to put brackets outside if
, otherwise (if) blocks are expected to be (then) and (else) blocks:
(module
(func (param $a i32)(result i32)
(local.get $a)
if (result i32)
i32.const 0
else
i32.const 1
end
)
(export "i32_eqz4" (func 0))
)
It is used to loop. If we want to proceed to the next round of loops in advance, the br instruction can be used, which is equivalent to the continue
statement in the C language.
If we want to exit the loop, the br_if
instruction can be used.
Now, I will follow the imperative language, which the loop comparison follows to write the assembly:
(module
(func (param $a i32)(result i32)
(local $sum i32)
(local.set $sum (i32.const 0))
loop
local.get $a
i32.const -1
i32.add
local.set $a
local.get $a
local.get $sum
i32.add
local.set $sum
local.get $a
br_if 0
end
(return (local.get $sum))
)
(export "i32_sum" (func 0))
)
The local instruction is used to define local variables. The local.set instruction is used to assign values to local variables. The return instruction is used to return a function.
Although WebAssembly is an abstract machine, it also needs to restore the hardware's capabilities. SIMD is short for Single Instruction Multiple Data. Speaking of SIMD, it was derived from the MMX instruction set of Intel CPU in 1996. It can use a 64-bit register as a combination of two 32-bit registers or eight 8-bit registers. In 1999, an SSE instruction set supporting 128-bit registers was introduced on the Pentium III processor. In 2008, Intel introduced the AVX instruction set supporting 256-bit registers on the second-generation Core processor Sandy Bridge. In 2013, Intel released the AVX 512 instruction set with 512-bit registers. The AVX 512 instruction set has increased power consumption. Linus Torvalds commented, “I hope AVX-512 dies a painful death.” However, I am getting off topic. WebAssembly also supports 128-bit SIMD instruction sets called vector instruction sets.
Look at an example to get a better understanding:
(module
(func (result i32)
v128.const i32x4 1 1 1 1
v128.const i32x4 2 2 2 2
i32x4.add
v128.any_true
return
)
(export "v128_anytrue" (func 0))
)
Unlike numeric constants (such as i32, i64, f32, and f64), v128 constants require instructions on how to interpret the usage of 128 bits. For example, as such, we use it as four 32-bit registers. The i32x4 instruction set is used to do the operation.
Meanwhile, we can also deal with v128 as a whole, using the instruction set of v128.
In another example, we want to use the swizzle command to reorder 8x16 numbers:
Write an 8x16 instruction:
v128.const i8x16 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
v128.const i8x16 1 2 0 3 4 5 6 7 8 9 10 11 12 13 14 15
i8x16.swizzle
If we disassemble it, it was disassembled into i32x4 format, but it does not affect the use of i8x16 instruction.
v128.const i32x4 0x04030201 0x08070605 0x0c0b0a09 0x100f0e0d
v128.const i32x4 0x03000201 0x07060504 0x0b0a0908 0x0f0e0d0c
i8x16.swizzle
SIMD instructions in wasm were first developed as an extension of Javascript, SIMD.js. Now, they are a part of wasm. Please refer to this link for details.
The article briefly introduces the instruction set of the WebAssembly abstract machine and the writing and running methods of assembly language. With this basic knowledge, we will be able to read the wasm code compiled through emsdk, and it is easy to understand when we see v8-related code.
The Design Draft Generation Code (imgcook) Can Develop Animation
Intuitive Comparison of Four NLP Models - Neural Network, RNN, CNN, and LSTM
66 posts | 3 followers
FollowAlibaba Cloud Community - March 9, 2022
Alibaba Cloud Native Community - March 6, 2023
Alibaba Cloud Community - September 14, 2023
Aliware - January 4, 2021
Alibaba Clouder - November 23, 2020
Alibaba Container Service - April 28, 2020
66 posts | 3 followers
FollowA HPCaaS cloud platform providing an all-in-one high-performance public computing service
Learn MoreMore Posts by Alibaba F(x) Team