GeezerVison is showing github.com — How to generate JsObject in Node.js c++ addon as fast as in pure js? · Issue #1074 · nodejs/node-addon-api
How to generate JsObject in Node.js c++ addon as fast as in pure js? #1074DescriptionI 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:
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:
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? Activityzhuyingda commented on 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:
NickNaso commented on Sep 20, 2021 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. Reacted by Michael Dawson and isaac sakyi Reacted by Sugar Zhu JckXia commented on 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 commented on Sep 22, 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?
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 commented on 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:
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:
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:
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:
So it seems like it is not slow that pass big JsObject from native side to JavaScript side in NAN c++ addon.
Conclusion:
- This is the fastest way I have ever found to generate JsObject in Node.js c++ addon: using NAN and
v8::ObjectTemplate.
- 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 commented on Dec 22, 2021 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 commented on Apr 11, 2022 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. Wyctus commented on Apr 16, 2022 I'm also interested in this topic, is there any new clue? zhuyingda commented on Apr 23, 2022 Wyctus commented on Apr 23, 2022 @zhuyingda Wow, this looks really interesting, thank you for mentioning! :) github-actions commented on Jul 23, 2022 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.
MetadataMetadataDevelopmentNo branches or pull requestsIssue actions
|