Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
zhuyingda opened this issue Sep 18, 2021 · 11 comments
Closed

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

zhuyingda opened this issue Sep 18, 2021 · 11 comments
Labels

Comments

@zhuyingda
Copy link

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?

@zhuyingda
Copy link
Author

zhuyingda commented Sep 18, 2021

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

@NickNaso
Copy link
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.

@JckXia
Copy link
Member

JckXia commented Sep 21, 2021

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?

@zhuyingda
Copy link
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?

@zhuyingda
Copy link
Author

zhuyingda commented Sep 22, 2021

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

@github-actions
Copy link
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.

@github-actions github-actions bot added the stale label Dec 22, 2021
@NickNaso NickNaso removed the stale label Jan 10, 2022
@github-actions
Copy link
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.

@github-actions github-actions bot added the stale label Apr 11, 2022
@Wyctus
Copy link

Wyctus commented Apr 16, 2022

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

@github-actions github-actions bot removed the stale label Apr 17, 2022
@zhuyingda
Copy link
Author

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

@Wyctus FYI https://stackoverflow.com/questions/69231342/how-to-generate-jsobject-in-node-js-c-addon-as-fast-as-in-pure-js/69235332?noredirect=1#comment122622138_69235332
I think you would like to read this :)

@Wyctus
Copy link

Wyctus commented Apr 23, 2022

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

@github-actions
Copy link
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
Labels
Projects
None yet
Development

No branches or pull requests

4 participants