Sugar
Sugar

Reputation: 150

How to generate JsObject in Node.js c++ addon as fast as in pure js?

I am new as a Node.js cpp-addon developer, I need to return a very big Object from cpp(Node.js cpp-addon) to Javascript logic, but I found that it is very slow to generate JsObject in C++ addon rather than pure js. Here is my code and running result:

cpp-addon case:

var addon = require('bindings')('addon.node')

console.time('perf');
console.log('This should be result:', addon.getBigObject('foo bar'));
console.timeEnd('perf');
#include <napi.h>

Napi::Value GetBigObject(const Napi::CallbackInfo& info) {
  Napi::Env m_env = info.Env();

  Napi::Object node = Napi::Object::New(m_env);
  node.Set(Napi::String::New(m_env, "foo"), Napi::Number::New(m_env, 1));
  node.Set(Napi::String::New(m_env, "body"), Napi::Array::New(m_env));


  for (int i = 0; i < 2000000; i++) {
    Napi::Object stmt = Napi::Object::New(m_env);
    stmt.Set(Napi::String::New(m_env, "foo"), Napi::Number::New(m_env, 1));
    Napi::Array body = node.Get("body").As<Napi::Array>();
    auto len = std::to_string(body.Length());
    body.Set(Napi::String::New(m_env, len), stmt);
  }


  return node;
}

Napi::Object Init(Napi::Env env, Napi::Object exports) {
  exports.Set(Napi::String::New(env, "getBigObject"), Napi::Function::New(env, GetBigObject));
  return exports;
}

NODE_API_MODULE(addon, Init)

Running this code time cost:

perf: 2.782s

pure-js case:

var addon = require('./addon.js');

console.time('perf');
console.log('This should be result:', addon.getBigObject('var foo = 1;'));
console.timeEnd('perf');
module.exports.getBigObject = function() {
  const node = {};
  node.foo = 1;
  node.body = [];
  for (let i = 0; i < 2000000; i++) {
    let body = node.body;
    body.push({
      foo: 1
    });
  }
  return node;
}

Running this code time cost:

perf: 220.973ms

Why is this so slow that generate JsObject in cpp-addon? Perhaps it is relative to V8 Hidden-Class? How could I generate JsObject in cpp-addon faster?

Upvotes: 1

Views: 625

Answers (1)

jmrk
jmrk

Reputation: 40521

(V8 developer here.)

Why is this so slow that generate JsObject in cpp-addon?

In short: because in JavaScript, you benefit from engine optimizations; whereas in C++, you have to optimize things yourself.

How could I generate JsObject in cpp-addon faster?

The general answer to "how can I make anything faster?" is: profile it to see where exactly the time is spent, and then see if you can avoid or optimize that.

Based on your code, I can make a few guesses at things that may be worth trying:

for (int i = 0; i < 2000000; i++) {

That is a huge iteration count; creating so many handles (in the loop body) in a single HandleScope is probably contributing to the slowness. I'd use a nested loop to create a new HandleScope in the outer loop for every 1000 or so iterations of the inner loop. See here.

Napi::Object stmt = Napi::Object::New(m_env);
stmt.Set(Napi::String::New(m_env, "foo"), Napi::Number::New(m_env, 1));

The V8 API has the ObjectTemplate system to churn out fresh copies of similar-looking objects as quickly as possible. It looks like N-API doesn't expose that though; so you could either move from N-API to using the V8 API directly, or simply live without it, hoping that it wouldn't make too much of a difference.

Napi::Array body = node.Get("body").As<Napi::Array>();

Reading the "body" property of node in every iteration is unnecessary, as you know that it'll return the array you allocated previously, so just cache that in a local variable.

auto len = std::to_string(body.Length());

Reading the length in every iteration is unnecessary, as it's always identical to i.

body.Set(Napi::String::New(m_env, len), stmt);

Creating a string for an array index just to force the engine to convert that string back to a number is quite inefficient. napi_set_element avoids the unnecessary back-and-forth.

Another thing that the V8 API lets you do (but I can't find it on the N-API documentation) is compiling and executing any JS code you want. So if you really want the exact equivalent of the JS snippet, you can just call that JS snippet. (To save recompilation effort, I'd compile it as a separate step and cache the resulting function if you intend to call it repeatedly.)

Upvotes: 2

Related Questions