В чем причина медленных вызовов из JavaScript кода к аддонам на C, C++ или подключенных через N-API?nodejs-13

Основные факторы влияющие на производительность

1. Граница между JavaScript и нативным кодом

Переход между JS и C++ требует сериализации/десериализации данных и контекстного переключения:

const addon = require('./native-addon');
// Каждый такой вызов - переход между V8 и нативным кодом
const result = addon.compute(42);

2. Маршалинг данных

Преобразование типов между JS и C++:

JavaScript тип C++ тип Накладные расходы
Numberdouble/int32_tМинимальные
Stringchar*/v8::StringКопирование
Objectv8::ObjectГлубокий анализ
Bufferchar*Копирование

3. N-API накладные расходы

Хотя N-API стабильнее Native Abstractions, он добавляет уровень абстракции:

napi_status napi_get_value_double(napi_env env, napi_value value, double* result);

Каждый вызов N-API функции:

  1. Проверяет аргументы
  2. Конвертирует типы
  3. Обрабатывает возможные ошибки

4. Управление памятью

  • V8 garbage collector vs ручное управление в C++
  • Копирование данных вместо разделяемой памяти
  • Создание временных объектов

5. Блокировка Event Loop

Синхронные вызовы нативного кода блокируют основной поток:

// Этот вызов остановит весь Event Loop
const result = addon.syncCompute();

Пример замедления

Нативный метод, который просто возвращает число:

Napi::Number Add(const Napi::CallbackInfo& info) {
  double a = info[0].As<Napi::Number>().DoubleValue();
  double b = info[1].As<Napi::Number>().DoubleValue();
  return Napi::Number::New(info.Env(), a + b);
}

Вызов из JS:

const sum = addon.add(2, 3); // В 100-1000 раз медленнее чисто JS

Оптимизации

  1. Пакетные операции:

    • Передавать массивы вместо отдельных значений
    • Выполнять больше работы за один вызов
  2. Асинхронные вызовы:

    • Использовать worker threads
    • N-API async operations
  3. Буферы вместо объектов:

    • Работать с Binary Data напрямую
  4. Кэширование часто используемых значений:

static napi_value cachedValue;
if (!cachedValue) {
  napi_create_string_utf8(env, "cached", NAPI_AUTO_LENGTH, &cachedValue);
}

Резюмируем

  1. Основные причины замедления:

    • Контекстное переключение между JS и C++
    • Маршалинг и проверка типов
    • Накладные расходы N-API
    • Управление памятью
  2. Критические моменты:

    • Синхронные вызовы особенно дороги
    • Сложные структуры данных требуют больше преобразований
    • Каждый вызов - отдельная операция
  3. Оптимизации:

    • Уменьшение количества вызовов
    • Использование буферов
    • Асинхронные интерфейсы
    • Пакетная обработка

Для максимальной производительности минимизируйте переходы между JS и нативным кодом и оптимизируйте передачу данных.