c++node.jsv8node-addon-api

Why is the efficiency worse when I use c++ addon to iterate an array in node.js


// addon.cc
#include <napi.h>
Napi::Value myFunc(const Napi::CallbackInfo& info) {
  Napi::Env env = info.Env();

  Napi::Array arr = info[0].As<Napi::Array>();
  Napi::Function cb = info[1].As<Napi::Function>();

  // iterate over array and call callback for each element
  for (uint32_t i = 0; i < arr.Length(); i++) {
    arr[i] = cb.Call(env.Global(), {arr.Get(i)});
  }

  return arr;
}

Napi::Object Init(Napi::Env env, Napi::Object exports) {
  return Napi::Function::New(env, myFunc);
}

NODE_API_MODULE(addon, Init)
var addon = require("bindings")("addon")

for (let i = 0; i < 10; i++) {
  const arr1 = [];
  while (arr1.length < 10000000) {
    arr1.push(Math.random());
  }
  const arr2 = [];
  while (arr2.length < 10000000) {
    arr2.push(Math.random());
  }
  const arr3 = [];
  while (arr3.length < 10000000) {
    arr3.push(Math.random());
  }

  console.time("map");
  const a = arr1.map((cur) => cur * 2);
  console.timeEnd("map");

  console.time("myAddon");
  const b = addon(arr2, (i) => i * 2);
  console.timeEnd("myAddon");

  const c = [];
  console.time('for');
  for (let i = 0; i < arr3.length; i++) {
    c.push(arr3[i] * 2);
  }
  console.timeEnd('for');
  console.log('--------------')
}
map: 411.9ms
myAddon: 3.220s
for: 218.143ms
--------------
map: 363.966ms
myAddon: 2.841s
for: 86.077ms
--------------
map: 481.605ms
myAddon: 2.819s
for: 75.333ms
--------------

Solution

  • (V8 developer here.)
    This is expected. The reason is that crossing the boundary between C++ and JavaScript (in either direction) is comparatively expensive. That's why in V8, we don't implement Array.map and similar built-in functions in C++; however the internal techniques we use to accomplish that ("CodeStubAssembler", "Torque") aren't available to Node addons.

    It's really that the for loop cost a long time, but why?

    It's not the for-loop itself, it's the cb.Call(...) expression.

    Shouldn't c++ faster than js?

    No, not necessarily. Optimized JS can be just as fast. In very rare cases it can even be faster, when you create a scenario where dynamic optimizations are more powerful than static optimizations. In the case at hand, it's not about which language is faster though, it's about the cross-language function calls.

    And is there any way to improve my addon efficiency?

    Write it in JavaScript. Or find a way to have fewer invocations of any callbacks in either direction (i.e. neither calling JS from C++ for every array element nor calling C++ from JS for every array element). For numerical code, TypedArrays can sometimes be useful for this, depending on your use case.


    Side note: for a fairer comparison, the third case based on a plain old JavaScript for loop should preallocate the result array, i.e. replace const c = [] with const c = new Array(arr3.length), and c.push with c[i] = . It'll be twice as fast that way.