Непоследовательная производительность Google V8, выполняющего WebAssembly - PullRequest
1 голос
/ 19 июня 2020

Я пытаюсь выполнить довольно тривиальный тест WebAssembly с движком Google V8 (как в браузере, используя текущую версию Google Chrome (версия 83.0.4103.106, 64-бит), так и через встраивание V8 (версия 8.5. 183) в программе на C ++. Все тесты выполняются в macOS 10.14.6 с процессором Intel i7 8850H. Замена RAM не использовалась.

Я использую следующий код C в качестве теста. ( Обратите внимание, что на текущем Intel Core 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 измеряет время в Самый простой способ c следующий:

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);

Результат, который я получаю в браузере (например, в WasmFiddle или на настраиваемом веб-сайте) в Google Chrome, следующий (для нескольких последовательных выполнения) в миллисекундах:

3687
1757
1837
1753
1726
1731
1774
1741
1771
1727
3549
1742
1731
1847
1734
1745
3515
1731
1772

Обратите внимание на выбросы, выполняющиеся на половине скорости остальных. Как и почему возникают выбросы с такой стабильной производительностью? Как можно тщательнее ible был принят, чтобы гарантировать, что никакие другие процессы не используют процессорное время.

Для встроенной версии библиотека monolithi c 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

Код C ++, встраивающий библиотеку V8 и выполняющий сценарий Wasm (код 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 с - почти в три раза больше, чем при выполнении в Chrome / WasmFiddle! Я что-нибудь пропустил? Может какие переключатели оптимизации? Этот результат отлично воспроизводится, и я даже тестировал различные версии библиотеки V8 - все равно результат тот же.

1 Ответ

2 голосов
/ 19 июня 2020

Ах, радости микробенчмаркинга: -)

V8 имеет два компилятора для Wasm: неоптимизирующий базовый компилятор, который производит код очень быстро, и оптимизирующий компилятор, который создает код намного дольше. , но этот код обычно примерно в два раза быстрее. Когда модуль загружен, текущие версии сначала компилируют все функции с помощью базового компилятора. Как только это будет сделано, можно начать выполнение, и оптимизированные задания компиляции будут запущены в фоновом режиме. Когда оптимизированное задание компиляции завершено, код соответствующей функции заменяется местами, и следующий вызов функции будет использовать его. ( Подробности здесь, скорее всего, изменятся в будущем, но общий принцип останется. ) Таким образом, типичные приложения получат как хорошую задержку запуска, так и хорошую пиковую производительность.

Но, как и с любой эвристией c или стратегией, вы можете создать случай, когда она ошибается ...

В вашем тесте каждая функция вызывается только один раз. В быстрых случаях оптимизация kernel завершается до возврата init. В медленных случаях kernel вызывается до того, как будет выполнено его оптимизированное задание компиляции, поэтому запускается его базовая версия. По-видимому, при непосредственном встраивании V8 вы надежно получаете второй сценарий, тогда как при запуске через WasmFiddle в Chrome вы получаете первый большую часть времени, но не всегда.

Я не могу объяснить, почему ваш пользовательский встраивание выполняется даже медленнее, чем в Chrome; Я не вижу этого на своей машине (OTOH, в Chrome, я вижу еще большую дельту: около 1100 мс для быстрого запуска и 4400 мс для медленного запуска); однако я использовал оболочку d8 вместо компиляции собственного вложения. Одно отличие состоит в том, что при измерении с помощью time в командной строке вы включаете запуск и инициализацию процесса, которые не включаются в вызовы Date.now() около main(). Но это должно составлять всего 10-50 миллисекунд или около того, а не разницу в 3,6 с → 5,0 с.

Хотя эта ситуация может показаться весьма неудачной для вашего микробенчмарка, в целом она работает так, как задумано, т.е. ошибка, и, следовательно, вряд ли изменится на стороне V8. Есть несколько вещей, которые вы можете сделать, чтобы сделать эталонный тест более точным отражением реального поведения (при условии, что этот тест не совсем соответствует вашему реальному приложению):

  • выполнять функции несколько раз; вы увидите, что первый запуск будет медленнее (или, в зависимости от размера функции, размера модуля и количества доступных ядер ЦП и удачного планирования, первые несколько запусков)
  • подождите немного перед вызовом самые популярные функции, например, выполняя

    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, самые ранние версии V8, поддерживающие WebAssembly не было многоуровневого, только оптимизированная компиляция. Таким образом, модули всегда должны были ждать, пока это завершится sh. Это было не очень удобно для пользователей; для больших модулей время ожидания могло составлять десятки секунд. Наличие базового компилятора совершенно очевидно Лучшее решение в целом, даже если оно достигается за счет того, что максимальная производительность не доступна сразу. Хорошо выглядеть на искусственных однострочниках - это не то, что матовый RS на практике; обеспечение хорошего взаимодействия с пользователем для больших реальных приложений.)

...