Google 的 V8 执行 WebAssembly 的性能不一致
Inconsistent performance of Google's V8 executing WebAssembly
我正在尝试使用 Google 的 V8 引擎执行一个相当简单的 WebAssembly 基准测试(都在浏览器中使用 Google Chrome 的当前版本(版本 83.0. 4103.106,64 位)并通过在 C++ 程序中嵌入 V8(版本 8.5.183)。所有基准测试均在 macOS 10.14.6 和 Intel i7 8850H 处理器上执行。未使用 RAM 交换。
我使用以下 C 代码作为基准。 (请注意,在当前的英特尔酷睿 i7 上,运行时间约为秒)
static void init(int n, int path[1000][1000]) {
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
path[i][j] = i*j%7+1;
if ((i+j)%13 == 0 || (i+j)%7==0 || (i+j)%11 == 0) {
path[i][j] = 999;
}
}
}
}
static void kernel(int n, int path[1000][1000]) {
for (int k = 0; k < n; k++) {
for(int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
path[i][j] = path[i][j] < path[i][k] + path[k][j] ? path[i][j] : path[i][k] + path[k][j];
}
}
}
}
int path[1000][1000];
int main(void) {
int n = 1000;
init(n, path);
kernel(n, path);
return 0;
}
这可以通过 https://wasdk.github.io/WasmFiddle/ 轻松执行。最基本的测时间对应的JS代码如下:
var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule, wasmImports);
var a = new Date();
wasmInstance.exports.main();
var b = new Date();
log(b-a);
我在 Google Chrome 的浏览器中(例如在 WasmFiddle 或自定义网站上)得到的结果如下(对于多次连续执行)以毫秒为单位:
3687
1757
1837
1753
1726
1731
1774
1741
1771
1727
3549
1742
1731
1847
1734
1745
3515
1731
1772
注意异常值的执行速度是其余值的一半。如何以及为什么会有如此一致的表现的异常值?已尽可能小心地确保没有其他进程用完 CPU 时间。
对于嵌入式版本,单体 V8 库是使用以下构建配置从源代码构建的:
is_component_build = false
is_debug = false
target_cpu = "x64"
use_custom_libcxx = false
v8_monolithic = true
v8_use_external_startup_data = false
v8_enable_pointer_compression = false
嵌入V8库并执行Wasm脚本的C++代码(Wasm代码正是WasmFiddle编译器生成的代码):
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "include/libplatform/libplatform.h"
#include "include/v8.h"
int main(int argc, char* argv[]) {
// Initialize V8.
v8::V8::InitializeICUDefaultLocation(argv[0]);
v8::V8::InitializeExternalStartupData(argv[0]);
std::unique_ptr<v8::Platform> platform = v8::platform::NewDefaultPlatform();
v8::V8::InitializePlatform(platform.get());
v8::V8::Initialize();
// Create a new Isolate and make it the current one.
v8::Isolate::CreateParams create_params;
create_params.array_buffer_allocator = v8::ArrayBuffer::Allocator::NewDefaultAllocator();
v8::Isolate* isolate = v8::Isolate::New(create_params);
{
v8::Isolate::Scope isolate_scope(isolate);
// Create a stack-allocated handle scope.
v8::HandleScope handle_scope(isolate);
// Create a new context.
v8::Local<v8::Context> context = v8::Context::New(isolate);
v8::Context::Scope context_scope(context);
{
const char csource[] = R"(
let bytes = new Uint8Array([
0x0, 0x61, 0x73, 0x6D, 0x01, 0x00, 0x00, 0x00, 0x01, 0x85, 0x80, 0x80, 0x80, 0x00, 0x01, 0x60,
0x00, 0x01, 0x7F, 0x03, 0x82, 0x80, 0x80, 0x80, 0x00, 0x01, 0x00, 0x04, 0x84, 0x80, 0x80, 0x80,
0x00, 0x01, 0x70, 0x00, 0x00, 0x05, 0x83, 0x80, 0x80, 0x80, 0x00, 0x01, 0x00, 0x3E, 0x06, 0x81,
0x80, 0x80, 0x80, 0x00, 0x00, 0x07, 0x91, 0x80, 0x80, 0x80, 0x00, 0x02, 0x06, 0x6D, 0x65, 0x6D,
0x6F, 0x72, 0x79, 0x02, 0x00, 0x04, 0x6D, 0x61, 0x69, 0x6E, 0x00, 0x00, 0x0A, 0x8F, 0x82, 0x80,
0x80, 0x00, 0x01, 0x89, 0x82, 0x80, 0x80, 0x00, 0x01, 0x08, 0x7F, 0x41, 0x00, 0x21, 0x02, 0x41,
0x10, 0x21, 0x05, 0x03, 0x40, 0x20, 0x05, 0x21, 0x07, 0x41, 0x00, 0x21, 0x04, 0x41, 0x00, 0x21,
0x03, 0x03, 0x40, 0x20, 0x07, 0x20, 0x04, 0x41, 0x07, 0x6F, 0x41, 0x01, 0x6A, 0x41, 0xE7, 0x07,
0x20, 0x02, 0x20, 0x03, 0x6A, 0x22, 0x00, 0x41, 0x07, 0x6F, 0x1B, 0x41, 0xE7, 0x07, 0x20, 0x00,
0x41, 0x0D, 0x6F, 0x1B, 0x41, 0xE7, 0x07, 0x20, 0x00, 0x41, 0x0B, 0x6F, 0x1B, 0x36, 0x02, 0x00,
0x20, 0x07, 0x41, 0x04, 0x6A, 0x21, 0x07, 0x20, 0x04, 0x20, 0x02, 0x6A, 0x21, 0x04, 0x20, 0x03,
0x41, 0x01, 0x6A, 0x22, 0x03, 0x41, 0xE8, 0x07, 0x47, 0x0D, 0x00, 0x0B, 0x20, 0x05, 0x41, 0xA0,
0x1F, 0x6A, 0x21, 0x05, 0x20, 0x02, 0x41, 0x01, 0x6A, 0x22, 0x02, 0x41, 0xE8, 0x07, 0x47, 0x0D,
0x00, 0x0B, 0x41, 0x00, 0x21, 0x06, 0x41, 0x10, 0x21, 0x05, 0x03, 0x40, 0x41, 0x10, 0x21, 0x00,
0x41, 0x00, 0x21, 0x01, 0x03, 0x40, 0x20, 0x01, 0x41, 0xA0, 0x1F, 0x6C, 0x20, 0x06, 0x41, 0x02,
0x74, 0x6A, 0x41, 0x10, 0x6A, 0x21, 0x02, 0x41, 0x00, 0x21, 0x07, 0x03, 0x40, 0x20, 0x00, 0x20,
0x07, 0x6A, 0x22, 0x04, 0x20, 0x04, 0x28, 0x02, 0x00, 0x22, 0x04, 0x20, 0x05, 0x20, 0x07, 0x6A,
0x28, 0x02, 0x00, 0x20, 0x02, 0x28, 0x02, 0x00, 0x6A, 0x22, 0x03, 0x20, 0x04, 0x20, 0x03, 0x48,
0x1B, 0x36, 0x02, 0x00, 0x20, 0x07, 0x41, 0x04, 0x6A, 0x22, 0x07, 0x41, 0xA0, 0x1F, 0x47, 0x0D,
0x00, 0x0B, 0x20, 0x00, 0x41, 0xA0, 0x1F, 0x6A, 0x21, 0x00, 0x20, 0x01, 0x41, 0x01, 0x6A, 0x22,
0x01, 0x41, 0xE8, 0x07, 0x47, 0x0D, 0x00, 0x0B, 0x20, 0x05, 0x41, 0xA0, 0x1F, 0x6A, 0x21, 0x05,
0x20, 0x06, 0x41, 0x01, 0x6A, 0x22, 0x06, 0x41, 0xE8, 0x07, 0x47, 0x0D, 0x00, 0x0B, 0x41, 0x00,
0x0B
]);
let module = new WebAssembly.Module(bytes);
let instance = new WebAssembly.Instance(module);
instance.exports.main();
)";
// Create a string containing the JavaScript source code.
v8::Local<v8::String> source = v8::String::NewFromUtf8Literal(isolate, csource);
// Compile the source code.
v8::Local<v8::Script> script = v8::Script::Compile(context, source).ToLocalChecked();
// Run the script to get the result.
v8::Local<v8::Value> result = script->Run(context).ToLocalChecked();
}
}
// Dispose the isolate and tear down V8.
isolate->Dispose();
v8::V8::Dispose();
v8::V8::ShutdownPlatform();
delete create_params.array_buffer_allocator;
return 0;
}
我编译如下:
g++ -I. -O2 -Iinclude samples/wasm.cc -o wasm -lv8_monolith -Lout.gn/x64.release.sample/obj/ -pthread -std=c++17
使用 time ./wasm
执行时,我的执行时间在 4.9 秒到 5.1 秒之间——几乎是 in-Chrome/WasmFiddle 执行时间的三倍!我错过了什么吗?也许一些优化开关?这个结果是完全可重现的,我什至测试了各种不同版本的 V8 库 - 仍然是相同的结果。
啊,微基准测试的乐趣:-)
V8 有两个适用于 Wasm 的编译器:一个非优化基线编译器,它生成代码的速度非常快,另一个优化编译器生成代码需要更长的时间,但该代码的速度通常是它的两倍左右。加载模块时,当前版本首先使用基线编译器编译所有函数。一旦完成,就可以开始执行,优化的编译作业将在后台安排到 运行。优化编译作业完成后,相应函数的代码将被交换,下一次函数调用将使用它。 (这里的细节将来很可能会改变,但一般原则将保持不变。)这样,典型的应用程序将获得良好的启动延迟和良好的峰值性能。
但是,与任何启发式或策略一样,您可以设计一个错误的案例...
在您的基准测试中,每个函数只被调用一次。在快速的情况下,优化 kernel
在 init
returns 之前完成。在缓慢的情况下,kernel
在其优化编译作业完成之前被调用,因此其基线版本 运行s。显然,当直接嵌入 V8 时,你可靠地得到后一种情况,而当 运行 在 Chrome 中通过 WasmFiddle 宁时,你大部分时间都会得到前者,但并非总是如此。
我无法解释为什么您的自定义嵌入 运行 比 Chrome 中的慢情况还要慢;我没有在我的机器上看到它(OTOH,在 Chrome 中,我看到一个更大的增量:大约 1100 毫秒用于快速 运行 和 4400 毫秒用于慢速 运行);但是我使用 d8
shell 而不是编译我自己的嵌入。有一点不同的是,当在命令行上使用 time
进行测量时,您包括进程启动和初始化,Date.now()
调用 main()
不包括这些。但这应该只占 10-50 毫秒左右,而不是 3.6s → 5.0s 的差异。
虽然这种情况对于您的微基准测试来说可能看起来很不幸,但它通常按预期工作,即不是错误,因此不太可能在 V8 方面改变。您可以做几件事来使基准测试更能反映真实世界的行为(假设这个并不完全代表您拥有的某些实际应用程序):
- 多次执行函数;你会看到第一个 运行 会变慢(或者,根据函数大小和模块大小以及可用 CPU 内核的数量和调度运气,前几个 运行s)
稍等一下再调用最热门的函数,例如通过
var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule, wasmImports);
window.setTimeout(() => {
var a = Date.now();
wasmInstance.exports.main();
var b = Date.now();
log(b-a);
}, 10);
在我对 d8
的测试中,我发现即使是一个愚蠢的忙等待也能达到目的:
let wait = Date.now() + 10;
while (Date.now() < wait) {}
instance.exports.main();
- 通常会使基准测试变得更大更复杂:拥有并执行更多不同的功能,不要只在一行中花费 99% 的时间。
(FWIW,最早支持 WebAssembly 的 V8 版本没有分层,只有优化编译。所以模块总是不得不等待它完成。这不是一个好的用户体验;对于大型模块,等待时间可能是几十秒。拥有一个基线编译器显然是总体上更好的解决方案,即使它是以不能立即获得最大性能为代价的。在人工单行上看起来不错在实践中并不重要;提供良好的用户体验对于大型现实世界的应用程序很重要。)
我正在尝试使用 Google 的 V8 引擎执行一个相当简单的 WebAssembly 基准测试(都在浏览器中使用 Google Chrome 的当前版本(版本 83.0. 4103.106,64 位)并通过在 C++ 程序中嵌入 V8(版本 8.5.183)。所有基准测试均在 macOS 10.14.6 和 Intel i7 8850H 处理器上执行。未使用 RAM 交换。
我使用以下 C 代码作为基准。 (请注意,在当前的英特尔酷睿 i7 上,运行时间约为秒)
static void init(int n, int path[1000][1000]) {
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
path[i][j] = i*j%7+1;
if ((i+j)%13 == 0 || (i+j)%7==0 || (i+j)%11 == 0) {
path[i][j] = 999;
}
}
}
}
static void kernel(int n, int path[1000][1000]) {
for (int k = 0; k < n; k++) {
for(int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
path[i][j] = path[i][j] < path[i][k] + path[k][j] ? path[i][j] : path[i][k] + path[k][j];
}
}
}
}
int path[1000][1000];
int main(void) {
int n = 1000;
init(n, path);
kernel(n, path);
return 0;
}
这可以通过 https://wasdk.github.io/WasmFiddle/ 轻松执行。最基本的测时间对应的JS代码如下:
var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule, wasmImports);
var a = new Date();
wasmInstance.exports.main();
var b = new Date();
log(b-a);
我在 Google Chrome 的浏览器中(例如在 WasmFiddle 或自定义网站上)得到的结果如下(对于多次连续执行)以毫秒为单位:
3687
1757
1837
1753
1726
1731
1774
1741
1771
1727
3549
1742
1731
1847
1734
1745
3515
1731
1772
注意异常值的执行速度是其余值的一半。如何以及为什么会有如此一致的表现的异常值?已尽可能小心地确保没有其他进程用完 CPU 时间。
对于嵌入式版本,单体 V8 库是使用以下构建配置从源代码构建的:
is_component_build = false
is_debug = false
target_cpu = "x64"
use_custom_libcxx = false
v8_monolithic = true
v8_use_external_startup_data = false
v8_enable_pointer_compression = false
嵌入V8库并执行Wasm脚本的C++代码(Wasm代码正是WasmFiddle编译器生成的代码):
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "include/libplatform/libplatform.h"
#include "include/v8.h"
int main(int argc, char* argv[]) {
// Initialize V8.
v8::V8::InitializeICUDefaultLocation(argv[0]);
v8::V8::InitializeExternalStartupData(argv[0]);
std::unique_ptr<v8::Platform> platform = v8::platform::NewDefaultPlatform();
v8::V8::InitializePlatform(platform.get());
v8::V8::Initialize();
// Create a new Isolate and make it the current one.
v8::Isolate::CreateParams create_params;
create_params.array_buffer_allocator = v8::ArrayBuffer::Allocator::NewDefaultAllocator();
v8::Isolate* isolate = v8::Isolate::New(create_params);
{
v8::Isolate::Scope isolate_scope(isolate);
// Create a stack-allocated handle scope.
v8::HandleScope handle_scope(isolate);
// Create a new context.
v8::Local<v8::Context> context = v8::Context::New(isolate);
v8::Context::Scope context_scope(context);
{
const char csource[] = R"(
let bytes = new Uint8Array([
0x0, 0x61, 0x73, 0x6D, 0x01, 0x00, 0x00, 0x00, 0x01, 0x85, 0x80, 0x80, 0x80, 0x00, 0x01, 0x60,
0x00, 0x01, 0x7F, 0x03, 0x82, 0x80, 0x80, 0x80, 0x00, 0x01, 0x00, 0x04, 0x84, 0x80, 0x80, 0x80,
0x00, 0x01, 0x70, 0x00, 0x00, 0x05, 0x83, 0x80, 0x80, 0x80, 0x00, 0x01, 0x00, 0x3E, 0x06, 0x81,
0x80, 0x80, 0x80, 0x00, 0x00, 0x07, 0x91, 0x80, 0x80, 0x80, 0x00, 0x02, 0x06, 0x6D, 0x65, 0x6D,
0x6F, 0x72, 0x79, 0x02, 0x00, 0x04, 0x6D, 0x61, 0x69, 0x6E, 0x00, 0x00, 0x0A, 0x8F, 0x82, 0x80,
0x80, 0x00, 0x01, 0x89, 0x82, 0x80, 0x80, 0x00, 0x01, 0x08, 0x7F, 0x41, 0x00, 0x21, 0x02, 0x41,
0x10, 0x21, 0x05, 0x03, 0x40, 0x20, 0x05, 0x21, 0x07, 0x41, 0x00, 0x21, 0x04, 0x41, 0x00, 0x21,
0x03, 0x03, 0x40, 0x20, 0x07, 0x20, 0x04, 0x41, 0x07, 0x6F, 0x41, 0x01, 0x6A, 0x41, 0xE7, 0x07,
0x20, 0x02, 0x20, 0x03, 0x6A, 0x22, 0x00, 0x41, 0x07, 0x6F, 0x1B, 0x41, 0xE7, 0x07, 0x20, 0x00,
0x41, 0x0D, 0x6F, 0x1B, 0x41, 0xE7, 0x07, 0x20, 0x00, 0x41, 0x0B, 0x6F, 0x1B, 0x36, 0x02, 0x00,
0x20, 0x07, 0x41, 0x04, 0x6A, 0x21, 0x07, 0x20, 0x04, 0x20, 0x02, 0x6A, 0x21, 0x04, 0x20, 0x03,
0x41, 0x01, 0x6A, 0x22, 0x03, 0x41, 0xE8, 0x07, 0x47, 0x0D, 0x00, 0x0B, 0x20, 0x05, 0x41, 0xA0,
0x1F, 0x6A, 0x21, 0x05, 0x20, 0x02, 0x41, 0x01, 0x6A, 0x22, 0x02, 0x41, 0xE8, 0x07, 0x47, 0x0D,
0x00, 0x0B, 0x41, 0x00, 0x21, 0x06, 0x41, 0x10, 0x21, 0x05, 0x03, 0x40, 0x41, 0x10, 0x21, 0x00,
0x41, 0x00, 0x21, 0x01, 0x03, 0x40, 0x20, 0x01, 0x41, 0xA0, 0x1F, 0x6C, 0x20, 0x06, 0x41, 0x02,
0x74, 0x6A, 0x41, 0x10, 0x6A, 0x21, 0x02, 0x41, 0x00, 0x21, 0x07, 0x03, 0x40, 0x20, 0x00, 0x20,
0x07, 0x6A, 0x22, 0x04, 0x20, 0x04, 0x28, 0x02, 0x00, 0x22, 0x04, 0x20, 0x05, 0x20, 0x07, 0x6A,
0x28, 0x02, 0x00, 0x20, 0x02, 0x28, 0x02, 0x00, 0x6A, 0x22, 0x03, 0x20, 0x04, 0x20, 0x03, 0x48,
0x1B, 0x36, 0x02, 0x00, 0x20, 0x07, 0x41, 0x04, 0x6A, 0x22, 0x07, 0x41, 0xA0, 0x1F, 0x47, 0x0D,
0x00, 0x0B, 0x20, 0x00, 0x41, 0xA0, 0x1F, 0x6A, 0x21, 0x00, 0x20, 0x01, 0x41, 0x01, 0x6A, 0x22,
0x01, 0x41, 0xE8, 0x07, 0x47, 0x0D, 0x00, 0x0B, 0x20, 0x05, 0x41, 0xA0, 0x1F, 0x6A, 0x21, 0x05,
0x20, 0x06, 0x41, 0x01, 0x6A, 0x22, 0x06, 0x41, 0xE8, 0x07, 0x47, 0x0D, 0x00, 0x0B, 0x41, 0x00,
0x0B
]);
let module = new WebAssembly.Module(bytes);
let instance = new WebAssembly.Instance(module);
instance.exports.main();
)";
// Create a string containing the JavaScript source code.
v8::Local<v8::String> source = v8::String::NewFromUtf8Literal(isolate, csource);
// Compile the source code.
v8::Local<v8::Script> script = v8::Script::Compile(context, source).ToLocalChecked();
// Run the script to get the result.
v8::Local<v8::Value> result = script->Run(context).ToLocalChecked();
}
}
// Dispose the isolate and tear down V8.
isolate->Dispose();
v8::V8::Dispose();
v8::V8::ShutdownPlatform();
delete create_params.array_buffer_allocator;
return 0;
}
我编译如下:
g++ -I. -O2 -Iinclude samples/wasm.cc -o wasm -lv8_monolith -Lout.gn/x64.release.sample/obj/ -pthread -std=c++17
使用 time ./wasm
执行时,我的执行时间在 4.9 秒到 5.1 秒之间——几乎是 in-Chrome/WasmFiddle 执行时间的三倍!我错过了什么吗?也许一些优化开关?这个结果是完全可重现的,我什至测试了各种不同版本的 V8 库 - 仍然是相同的结果。
啊,微基准测试的乐趣:-)
V8 有两个适用于 Wasm 的编译器:一个非优化基线编译器,它生成代码的速度非常快,另一个优化编译器生成代码需要更长的时间,但该代码的速度通常是它的两倍左右。加载模块时,当前版本首先使用基线编译器编译所有函数。一旦完成,就可以开始执行,优化的编译作业将在后台安排到 运行。优化编译作业完成后,相应函数的代码将被交换,下一次函数调用将使用它。 (这里的细节将来很可能会改变,但一般原则将保持不变。)这样,典型的应用程序将获得良好的启动延迟和良好的峰值性能。
但是,与任何启发式或策略一样,您可以设计一个错误的案例...
在您的基准测试中,每个函数只被调用一次。在快速的情况下,优化 kernel
在 init
returns 之前完成。在缓慢的情况下,kernel
在其优化编译作业完成之前被调用,因此其基线版本 运行s。显然,当直接嵌入 V8 时,你可靠地得到后一种情况,而当 运行 在 Chrome 中通过 WasmFiddle 宁时,你大部分时间都会得到前者,但并非总是如此。
我无法解释为什么您的自定义嵌入 运行 比 Chrome 中的慢情况还要慢;我没有在我的机器上看到它(OTOH,在 Chrome 中,我看到一个更大的增量:大约 1100 毫秒用于快速 运行 和 4400 毫秒用于慢速 运行);但是我使用 d8
shell 而不是编译我自己的嵌入。有一点不同的是,当在命令行上使用 time
进行测量时,您包括进程启动和初始化,Date.now()
调用 main()
不包括这些。但这应该只占 10-50 毫秒左右,而不是 3.6s → 5.0s 的差异。
虽然这种情况对于您的微基准测试来说可能看起来很不幸,但它通常按预期工作,即不是错误,因此不太可能在 V8 方面改变。您可以做几件事来使基准测试更能反映真实世界的行为(假设这个并不完全代表您拥有的某些实际应用程序):
- 多次执行函数;你会看到第一个 运行 会变慢(或者,根据函数大小和模块大小以及可用 CPU 内核的数量和调度运气,前几个 运行s)
稍等一下再调用最热门的函数,例如通过
var wasmModule = new WebAssembly.Module(wasmCode); var wasmInstance = new WebAssembly.Instance(wasmModule, wasmImports); window.setTimeout(() => { var a = Date.now(); wasmInstance.exports.main(); var b = Date.now(); log(b-a); }, 10);
在我对
d8
的测试中,我发现即使是一个愚蠢的忙等待也能达到目的:let wait = Date.now() + 10; while (Date.now() < wait) {} instance.exports.main();
- 通常会使基准测试变得更大更复杂:拥有并执行更多不同的功能,不要只在一行中花费 99% 的时间。
(FWIW,最早支持 WebAssembly 的 V8 版本没有分层,只有优化编译。所以模块总是不得不等待它完成。这不是一个好的用户体验;对于大型模块,等待时间可能是几十秒。拥有一个基线编译器显然是总体上更好的解决方案,即使它是以不能立即获得最大性能为代价的。在人工单行上看起来不错在实践中并不重要;提供良好的用户体验对于大型现实世界的应用程序很重要。)