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 的编译器:一个非优化基线编译器,它生成代码的速度非常快,另一个优化编译器生成代码需要更长的时间,但该代码的速度通常是它的两倍左右。加载模块时,当前版本首先使用基线编译器编译所有函数。一旦完成,就可以开始执行,优化的编译作业将在后台安排到 运行。优化编译作业完成后,相应函数的代码将被交换,下一次函数调用将使用它。 (这里的细节将来很可能会改变,但一般原则将保持不变。)这样,典型的应用程序将获得良好的启动延迟和良好的峰值性能。

但是,与任何启发式或策略一样,您可以设计一个错误的案例...

在您的基准测试中,每个函数只被调用一次。在快速的情况下,优化 kernelinit 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 版本没有分层,只有优化编译。所以模块总是不得不等待它完成。这不是一个好的用户体验;对于大型模块,等待时间可能是几十秒。拥有一个基线编译器显然是总体上更好的解决方案,即使它是以不能立即获得最大性能为代价的。在人工单行上看起来不错在实践中并不重要;提供良好的用户体验对于大型现实世界的应用程序很重要。)