Skip to content

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

Description

@zhuyingda

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?

Activity

  1. zhuyingda commented on Sep 18, 2021

    @zhuyingda
    Author

    And I found the Nan c++ addon is faster than node-addon-api, but still slower than pure JS....

    like this:

    #include <nan.h>
    
    void CreateObject(const Nan::FunctionCallbackInfo<v8::Value>& info) {
      v8::Local<v8::Context> context = info.GetIsolate()->GetCurrentContext();
      v8::Local<v8::Object> node = Nan::New<v8::Object>();
    
      v8::Local<v8::Array> body = Nan::New<v8::Array>();
      for (auto i = 0; i < 2000000; i++) {
        v8::Local<v8::Object> el = Nan::New<v8::Object>();
        el->Set(context, Nan::New("foo").ToLocalChecked(), Nan::New<v8::Number>(1));
        Nan::Set(body, i, el);
      }
    
      node->Set(context, Nan::New("body").ToLocalChecked(), body);
    
      info.GetReturnValue().Set(node);
    }
    
    void Init(v8::Local<v8::Object> exports, v8::Local<v8::Object> module) {
      v8::Local<v8::Context> context = exports->CreationContext();
      module->Set(context,
                  Nan::New("exports").ToLocalChecked(),
                  Nan::New<v8::FunctionTemplate>(CreateObject)
                      ->GetFunction(context)
                      .ToLocalChecked());
    }
    
    NODE_MODULE(addon, Init)
    
    

    time cost:

    perf: 1.559s
    
  2. NickNaso commented on Sep 20, 2021

    @NickNaso
    Member

    Hi @zhuyingda,
    when you implement a native addon there is a cost on passing data as input and output to your native function. The data you pass will be copied from JavaScript side to native side and vice versa. Usually the time you spent on pass data is not relevant to the time taken to complete some computation. If you need to tranfer large amounts of data from JavaScript to native side the best solution shoul be Napi::TypedArray and Napi::ArrayBuffer and Napi::Buffer in this case the buffer is shared between the JavaScript and native side. This is documented here: https://github.com/nodejs/node-addon-api/blob/main/doc/array.md and you can find an exapmle here: https://github.com/nodejs/node-addon-examples/tree/HEAD/array_buffer_to_native/node-addon-api
    Node-API is technically a layer over the Node.js and V8 APIs this is the reason because using NAN you have better performance. NAN is a thin and transparent layer over the Node.js and V8 APIs it guarantees the API stability, but you need to recompile your native addon changing the Node.js version, instead the Node-API should guarantees the API and ABI stability this means that you don't need to recompile you native module switching Node.js version.

  3. JckXia commented on Sep 21, 2021

    @JckXia
    Member

    In addition to what Nick has said. After doing some testing (not returning the object, void function) it looks like the addon still takes a while to finish. If your code requires this big for loop to work, then I recommend also trying out the ThreadSafeFunction to split up work among other threads. As for why pure js code executes faster I am not quite sure. My guess is that v8 does some optimization behind the scenes at the assembly level?

  4. zhuyingda commented on Sep 22, 2021

    @zhuyingda
    Author

    In addition to what Nick has said. After doing some testing (not returning the object, void function) it looks like the addon still takes a while to finish. If your code requires this big for loop to work, then I recommend also trying out the ThreadSafeFunction to split up work among other threads. As for why pure js code executes faster I am not quite sure. My guess is that v8 does some optimization behind the scenes at the assembly level?

    I want to figure out how could I generate JSObject in c++ addon also benefit from the optimization which v8 does behind this scenes?

  5. zhuyingda commented on Sep 22, 2021

    @zhuyingda
    Author

    when you implement a native addon there is a cost on passing data as input and output to your native function. The data you pass will be copied from JavaScript side to native side and vice versa. Usually the time you spent on pass data is not relevant to the time taken to complete some computation.

    Hi @NickNaso, thanks for your reply.
    Actually, after doing a lot of testing, the data passing between JavaScript side and native side is not the main slowness (just like @JckXia said).
    I change previous NAPI code to this:

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

    it just cost:

    perf: 794.548ms
    

    And also, in NAN case:

    v8::Local<v8::Object> el = Nan::New<v8::Object>();
    el->Set(context, Nan::New("foo").ToLocalChecked(), Nan::New<v8::Number>(1));
    
    for (auto i = 0; i < 2000000; i++) {
      Nan::Set(body, i, el);
    }
    

    it just cost:

    perf: 281.696ms
    

    AKA the main time-cost is because of Napi::Object::New() and stmt.Set().

    After moving the Object::New() out of the big for loop, it save a lot of time.
    However, the code will not work anymore, because all element in the JS-Object-Array is the same JsObject...
    So, I also found another way in NAN addon that I can use V8::ObjectTemplate to optimize JsObject generating in c++:

    v8::Local<v8::ObjectTemplate> objTpl = Nan::New<v8::ObjectTemplate>();
    objTpl->Set(Nan::New("foo").ToLocalChecked(), Nan::New<v8::Number>(1));
    
    for (auto i = 0; i < 2000000; i++) {
      Nan::Set(body, i, objTpl->NewInstance(context).ToLocalChecked());
    }
    

    This time cost:

    perf: 736.499ms
    

    Also, I change the NAN c++ addon code to this:

    #include <nan.h>
    
    void CreateObject(const Nan::FunctionCallbackInfo<v8::Value>& info) {
      v8::Local<v8::Context> context = info.GetIsolate()->GetCurrentContext();
      v8::Local<v8::Object> node = Nan::New<v8::Object>();
    
      v8::Local<v8::Array> body = Nan::New<v8::Array>();
    
      v8::Local<v8::ObjectTemplate> objTpl = Nan::New<v8::ObjectTemplate>();
      objTpl->Set(Nan::New("foo").ToLocalChecked(), Nan::New<v8::Number>(1));
    
      for (auto i = 0; i < 2000000; i++) {
        Nan::Set(body, i, objTpl->NewInstance(context).ToLocalChecked());
      }
    
      node->Set(context, Nan::New("body").ToLocalChecked(), body);
    
      info.GetReturnValue().Set(Nan::New<v8::Number>(1));
    }
    
    void Init(v8::Local<v8::Object> exports, v8::Local<v8::Object> module) {
      v8::Local<v8::Context> context = exports->CreationContext();
      module->Set(context,
                  Nan::New("exports").ToLocalChecked(),
                  Nan::New<v8::FunctionTemplate>(CreateObject)
                      ->GetFunction(context)
                      .ToLocalChecked());
    }
    
    NODE_MODULE(addon, Init)
    

    AKA just return a JsNumber to JavaScript but not the big JsObject, the time cost is:

    perf: 756.789ms
    

    So it seems like it is not slow that pass big JsObject from native side to JavaScript side in NAN c++ addon.

    Conclusion:

    1. This is the fastest way I have ever found to generate JsObject in Node.js c++ addon: using NAN and v8::ObjectTemplate.
    2. Big Object(JsArray) passing from c++ addon native side to JavaScript side is not slow(in my case it just cost a few ms).

    I am still working on this. I want to found the way to generate JsObject in c++ addon as fast as generate JsObject in pure JavaScript.
    Looking forward to your reply :)

  6. github-actions commented on Dec 22, 2021

    @github-actions
    Contributor

    This issue is stale because it has been open many days with no activity. It will be closed soon unless the stale label is removed or a comment is made.

  7. github-actions commented on Apr 11, 2022

    @github-actions
    Contributor

    This issue is stale because it has been open many days with no activity. It will be closed soon unless the stale label is removed or a comment is made.

  8. Wyctus commented on Apr 16, 2022

    @Wyctus

    I'm also interested in this topic, is there any new clue?

  9. zhuyingda commented on Apr 23, 2022

    @zhuyingda
    Author
  10. Wyctus commented on Apr 23, 2022

    @Wyctus

    @zhuyingda Wow, this looks really interesting, thank you for mentioning! :)

  11. github-actions commented on Jul 23, 2022

    @github-actions
    Contributor

    This issue is stale because it has been open many days with no activity. It will be closed soon unless the stale label is removed or a comment is made.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions