-
Notifications
You must be signed in to change notification settings - Fork 29.8k
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
Expose a faster way to create objects with properties for Node-API #45905
Comments
Dictionary mode happens when an object has too many properties. In that respect Instantiating an As a practical concern: support for JS engines other than V8 is a design goal of n-api and I don't think other engines have a concept that maps cleanly to |
Hmm, in my benchmark all the objects I created have exactly 2 properties. Did I misinterpret the profile (what MigrateToMap does)?
They must have some way of constructing an object with properties upfront through, similar to creating an object literal. And if not, the API could always fall back to constructing an object and assigning properties one by one internally. |
"Map" in V8 nomenclature is what other JS engines call "object shape" or "hidden class", it's a piece of metadata that describes the object's layout. It's used every time an object's layout changes (at least in C++ code), not just when switching from fast to slow mode.
Oh yeah, they probably do. I was thinking of lifetime issues. A It's probably not insurmountable but some upfront research is necessary. (I'm not volunteering!) |
I created a simple benchmark in this repo showing the difference between various methods. Each creates 100000 objects with 2 properties and appends them to an array.
Even with ObjectTemplate, JS object literals are still significantly faster. |
I'm also encountering this while working on a serde (serilization) implementation for Neon (Node-API bindings for Rust). I'm having difficulty getting my direct Rust<->JS implementation to outperform JSON ( I setup a benchmark with criterion serializing the pokedex data set and profiled with pprof and JSON is 2x faster. Most time is spent in Looking at the V8 JSON parsing code, it looks like a lot of work goes into optimizing the construction of a JSON object from a list of properties. It would be great if these optimizations could be brought over to a Node-API method. Perhaps something like:
|
Sounds like such an API could be made faster in JSC as well according to @Jarred-Sumner of Bun: https://twitter.com/jarredsumner/status/1605566356895801344?s=20&t=ZlICNZzNZotl6gtaOtBULA |
Someone would need to add the corresponding API to V8 first. |
v8 does have a version of But I noticed in the implementation it calls |
We discussed a bit in the Node-api team meeting. @vmoroz is going to take a look and do some experimentation. |
My hope was that we can improve perf for setting properties by adding an API to create internalized strings ( It would be great to add an API like So far it seems that Node-API or V8 API cannot create JS objects as fast as the JS parser does from object literals. It makes sense: the parser has the full information upfront and can optimize the creation unlike anything we can do through the API. Adding a special V8 API as for JSON object might address this issue. But frankly, it all feels a bit wrong: on one side we have C++ or Rust that can be extremely efficient with handling large amount of data, and on the other side we have JavaScript that was never meant to be that efficient, and still when we use C++ we lose. The reason we lose is because we make C++ to play by JavaScript rules. Since the interop has an overhead, we theoretically cannot achieve the same perf as in JavaScript. The only way to win is to avoid playing by JavaScript rules. It means avoiding creation a lot of small objects from C++ and instead explore other techniques. My understanding is that these small objects are needed to represent some UI scene graphs. Games written in C++ often avoid creation of many small objects and instead use arrays for better cache locality. Thus, the proposal is to rethink the JS code that works with the native APIs in a way that it keeps data mostly in big arrays and is not "chatty".
I realize that I am not helping to address the issue as it was stated, but I am quite interested to find a work around to this type of problems. It would be great to create a Node-API example that shows these techniques. |
Thanks for the investigation! In my case, I'm building a compiler in Rust which allows custom JavaScript visitor plugins. This consists of JS functions that are called when a node in the AST is visited. For a given file, the functions may be called hundreds or thousands of times, with small objects as arguments. It would be pretty hard to avoid creating all these objects in this case I think. If you're curious, the docs have more info. Do you think it would be worth filing a ticket with V8 asking to optimize |
It is a very cool project! And it is completely different scenario from the scene graph... TypedArray or "less chatty" API will not work. Based on the spec and examples it seems that each argument type may have several properties.
Hopefully, this way we can reduce amount of work done per each argument object and mostly reuse all the code as prototypes. As an alternative, it is possible to use As for asking V8 team for the new API, it should be worth doing it. We must be able to wrap it in a Node-API function if they implement it. |
In my use case, key names are known upfront and interning strings (with Since I'm writing a Rust serializer, in most cases, keys and even types are known and can be aggregated up front, similar to the optimizations in the V8 JSON parser. Interestingly, these optimizations appear to only exist in the JSON parser. I also tried writing a |
I don't know how it works in V8, but in JSC there is an explicit JSC::Structure class which holds the properties defined for an object and the structure can be reused for multiple objects. To create new objects with the same properties quickly in JSC, we can:
It has a special fast path when there are under 64 properties and then a fast but not as fast path for objects with more than 63 properties The particular types of the properties in the JSC::Structure don't matter much. It only needs to know how many, what are the property names, and what are the attributes It's fastest to persistently hold on to a Structure if it's going to be reused, but then it needs to be visited by the garbage collector and that's tricky because who owns it? In bun's case, we use this optimization in a couple places and we lazily mark it as owned by the global object which means once created, it's never freed. This is okay because we are selective of when we do this optimization I recommend against going the approach of using getter/setters because it will make the runtime performance of using the objects worse due to lack of inlining. |
It sounds very promising for the interning strings case!
V8 source code is included in Node.JS. I wonder if we can prototype and measure the JSON-like approach.
It would be interesting to learn more about such JS engine-specific optimizations and see if we can have a new Node-API function that takes advantage of them. Could you share a link to the related JSC source code or examples for it? |
I did not. When I get a chance I can setup a local copy of nodejs for hacking. I also took a bit of a shortcut on string interning and used thread-local-storage instance of instance data. However, I doubt that will have much of a performance impact. So far, my best performance is using Node-API calls directly for simple types and switching to JSON for objects. |
@vmoroz I tried |
@kjvalencik, thank you for testing it! Now we have two possible "knobs" to do slight improvements for the property settings: interned strings and |
Let me know if you can think of a better way to hold those references for testing purposes. |
Ah, I see. If you are hacking Node.JS code, then you could enable |
@vmoroz Thanks for the tip! Making that change to
Setting the property is still the bottleneck. |
@kjvalencik , @devongovett , it looks like V8 already has a function to create Object literals :) /**
* Creates a JavaScript object with the given properties, and
* a the given prototype_or_null (which can be any JavaScript
* value, and if it's null, the newly created object won't have
* a prototype at all). This is similar to Object.create().
* All properties will be created as enumerable, configurable
* and writable properties.
*/
static Local<Object> New(Isolate* isolate, Local<Value> prototype_or_null,
Local<Name>* names, Local<Value>* values,
size_t length); |
Yeah that's the one I linked to above.
|
Hmm... In that case the important part is that at least the API exists. So, instead of asking V8 team to create a new API, we can ask for improving the perf. EDIT: You are right: the perf for this EDIT 2: @devongovett, I re-read the thread and I am sorry that I was repeating the same points you made before. You already pointed at the |
Using |
@kjvalencik , I am playing with the benchmark sample created by @devongovett. So far, I cannot beat the perf offered by the
If I put the I bet your perf scenario is a bit more balanced and thus produces different results. |
In many scenarios, there would need to be a persistent reference to the Edit |
Usage: Creating it: |
@devongovett, it would be nice to have the whole the end-to-end prototype first. Just to make sure that we ask for the right thing.
The fast object creation can be achieved by
The API will essentially combine JavaScript Object initializer, Object.assign(), and Object.fromEntries() behavior. This API will have one significant drawback - it will not support property descriptors and focus only on the writable, enumerable, and configurable value properties. Is it a big disadvantage? Should we use the property descriptors as @kjvalencik proposed above? The fast array creation should mimic the JavaScript Array literals. There can be three overloads: for Integers, Doubles, and general napi_value. The hope is that knowing the size and type of values we can improve the initial array creation speed. The string internalizing can give advantage for the V8 and other JS engines. The JS engines that do not support it can just return a regular string. Thus, the full list of new APIs may look like: napi_status node_api_create_object_from_entries(
napi_env env,
napi_value object_tempate, // optional object to clone
napi_value prototype, // either object or null to use when no object_tempate given
size_t property_count, // number of names and values entries
napi_value* names,
napi_value* values,
napi_value* result);
napi_status node_api_create_array_int32(napi_env env, size_t count, int32_t* values, napi_value* result);
napi_status node_api_create_array_double(napi_env env, size_t count, double* values, napi_value* result);
napi_status node_api_create_array_from_values(napi_env env, size_t count, napi_value* values, napi_value* result);
napi_status node_api_create_property_key_utf8(napi_env env, const char* str, size_t length, napi_value* result);
napi_status node_api_create_property_key_utf16(napi_env env, const char16_t* str, size_t length, napi_value* result);
napi_status node_api_create_property_key(napi_env env, napi_value str_or_sym, napi_value* result);
|
Looks good to me!
I think this is a much more rare use case, so it's ok if it's done through a different API that isn't as optimized. This would be similar to creating an object literal in JS vs |
I wasn't proposing it, just benchmarking. I don't think this is necessary.
I think only |
Any news?
17575 / 703391 = 0.02498610303515399 Only ~2.5% ops of C addon without create jsobj.
|
Implement defining properties via V8's `v8::Object::CreateDataProperty()`, which is faster for Re: nodejs#45905 Signed-off-by: Gabriel Schulhof <Gschulhof@auction.com>
Implement defining properties via V8's `v8::Object::CreateDataProperty()`, which is faster for Re: nodejs#45905 Signed-off-by: Gabriel Schulhof <Gschulhof@auction.com>
Implement defining properties via V8's `v8::Object::CreateDataProperty()`, which is faster for data-valued, writable, configurable, and enumerable properties. Re: nodejs#45905 Signed-off-by: Gabriel Schulhof <Gschulhof@auction.com>
Implement defining properties via V8's `v8::Object::CreateDataProperty()`, which is faster for data-valued, writable, configurable, and enumerable properties. Re: nodejs#45905 Signed-off-by: Gabriel Schulhof <Gschulhof@auction.com>
Implement defining properties via V8's `v8::Object::CreateDataProperty()`, which is faster for data-valued, writable, configurable, and enumerable properties. Re: nodejs#45905 Signed-off-by: Gabriel Schulhof <Gschulhof@auction.com>
Implement defining properties via V8's `v8::Object::CreateDataProperty()`, which is faster for data-valued, writable, configurable, and enumerable properties. Re: #45905 Signed-off-by: Gabriel Schulhof <gabrielschulhof@gmail.com> PR-URL: #48440 Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: Tobias Nießen <tniessen@tnie.de> Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
Implement defining properties via V8's `v8::Object::CreateDataProperty()`, which is faster for data-valued, writable, configurable, and enumerable properties. Re: #45905 Signed-off-by: Gabriel Schulhof <gabrielschulhof@gmail.com> PR-URL: #48440 Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: Tobias Nießen <tniessen@tnie.de> Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
Implement defining properties via V8's `v8::Object::CreateDataProperty()`, which is faster for data-valued, writable, configurable, and enumerable properties. Re: nodejs#45905 Signed-off-by: Gabriel Schulhof <gabrielschulhof@gmail.com> PR-URL: nodejs#48440 Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: Tobias Nießen <tniessen@tnie.de> Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
Implement defining properties via V8's `v8::Object::CreateDataProperty()`, which is faster for data-valued, writable, configurable, and enumerable properties. Re: nodejs#45905 Signed-off-by: Gabriel Schulhof <gabrielschulhof@gmail.com> PR-URL: nodejs#48440 Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: Tobias Nießen <tniessen@tnie.de> Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
Implement defining properties via V8's `v8::Object::CreateDataProperty()`, which is faster for data-valued, writable, configurable, and enumerable properties. Re: #45905 Signed-off-by: Gabriel Schulhof <gabrielschulhof@gmail.com> PR-URL: #48440 Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: Tobias Nießen <tniessen@tnie.de> Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
Implement defining properties via V8's `v8::Object::CreateDataProperty()`, which is faster for data-valued, writable, configurable, and enumerable properties. Re: #45905 Signed-off-by: Gabriel Schulhof <gabrielschulhof@gmail.com> PR-URL: #48440 Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: Tobias Nießen <tniessen@tnie.de> Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
There has been no activity on this feature request for 5 months and it is unlikely to be implemented. It will be closed 6 months after the last non-automated comment. For more information on how the project manages feature requests, please consult the feature request management document. |
Not stale |
What is the problem this feature will solve?
At the moment, there are a few ways to create objects using the napi C APIs.
napi_create_object
, and thennapi_set_named_property
for each property.napi_create_object
, and usenapi_define_properties
to set all properties at once (under the hood it still sets them one by one though).napi_define_class
+napi_new_instance
, and then add properties using one of the above methods. You can define accessors on the prototype, or primitive values but instance properties must be set the same way as above.In my use case I need to create a lot of small objects of the same shape. I've noticed in profiling that v8 functions under
napi_set_named_property
are slow, specificallyv8::internal::JSObject::MigrateToMap
. From my research in the v8 source code, this indicates that the object is transitioning from a fast struct/class-like object to a slower dictionary representation (I could be wrong here).I think v8 exposes
ObjectTemplate
andFunctionTemplate->InstanceTemplate
which are meant to help with this by defining the instance properties up front. This way, the object property slots can be allocated in the object itself rather than in a separate hash map. I found some useful info about v8 object representations here. I see that Node makes use ofObjectTemplate
for internal objects, and a previous issue nodejs/node-addon-api#1074 also found this to be the fastest way to create objects (though still not as fast as doing it in JS).My problem is that napi does not expose a way to create an object together with its properties, or a way to define a template for class instance properties. This means, as far as I can tell, all objects constructed through napi will end up in the slow dictionary mode, leading to slower perf when both constructing and accessing properties.
What is the feature you are proposing to solve the problem?
It would be awesome if node exposed a new
napi_create_object_with_properties
function, which would accept a list of property descriptors likenapi_define_properties
and allocate an object and assign properties all at once. This could potentially allow using faster v8 methods to create the object and assign properties so that it doesn't go into hashmap mode when adding properties one by one.Alternatively, a way to define an instance property template for classes would also work for me. For example,
napi_property_attributes
could be extended with anapi_instance
attribute for defining properties on the instance template rather than the prototype template.What alternatives have you considered?
I'm not a node or v8 internal expert, just noticed that setting properties on objects seemed slower than expected, so I could be totally wrong about everything above. Opening this issue to get a conversation started. Totally open to other suggestions!
The text was updated successfully, but these errors were encountered: