nodejs / node-addon-api Public
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
Question: Exporting a larger module from c++ seems to have a performance overhead when calling functions from js? #1103
Comments
|
There is nothing in Node-API itself that makes us believe it should be slower with more functions on a single object. In terms of general JavaScript, we could see that if you have more properties on an Object, the lookup of the function on the object could get longer based on the number of properties and possibly it might be harder for V8 to optimize when those properties are a native function. A good experiment might be to try saving the properties from the exports object into local variables once at the start and then calling the saved values. @gabrielschulhof will add an example following the meeting. |
|
So, instead of const addon = require("my_addon");
my_addon.someFunction(/*...*/);
my_addon.someOtherFunction(/*...*/);
my_addon.yetAnotherFunction(/*...*/);
/*...*/You can write const { someFunction, someOtherFunction, yetAnotherFunction, /*...*/ } = require("my_addon");
someFunction(/*...*/);
someOtherFunction(/*...*/);
yetAnotherFunction(/*...*/);
/*...*/Admittedly it's much more verbose, but if the performance improves it may be worth the trouble. Also, the |
|
I appreciate you being able to read this and respond here. At your suggestions I tried using destructuring to import just the needed functions, but I can't observe any difference in performance from changing this. I had thought that it might have been a linear lookup issue, and if so wouldn't changing the order of exports also improve performance in that sense? Does setting the exports from c++ guarantee order? Before posting here I had tried shifting the order of the export so the most frequently called function was the first (then later tried last) export and that had no noticable improvement. |
|
We discussed again in the team meeting today, we are puzzled but nobody has had a chance to take a look yet. I'm not sure changing the order of the exports would make a different as I expect there is something like a hashtable being used so the order exports are added may not affect the lookup directly. Do you have simple/small recreate that shows the issue? |
|
I ended up finding a few places where I was using .ToNumber() on my Napi::Values where I should have been casting with .AsNapi::Number(), which ended up closing the gap around half (now ~11% difference instead of 20%). So ultimately this is leading me to think there are some other differences in how I implemented it that are at fault, though I have been hard-pressed so far to find anything else that would affect it, particularly in the draw call function that's being called the most. So I am inclined to agree its not the size of the export that would be causing any performance issues. I currently only have the repo I've been writing myself. I can include some instructions on switching between the two versions if anyone would be generous enough to take a look. I made a quick file to compare the handwritten vs code generated functions that are used in my test case - if it would be helpful to look at the direct differences. https://github.com/twuky/raylib-4.0/blob/master/src/comparison.cc |
|
I am triaging this issue now |
|
@twuky I used a script to generate a very large binding file, |
|
@twuky after running the above script to create a binding object with 10k functions, there was no noticeable performance overhead when one of the functions is required and called as below |
|
the generated large binding object looks like |
|
@twuky IMO, type cast in the cpp layer especially something like uint arrays or buffer to string adds to performance , is what I have noticed |
|
there was one case in my experience with a node-java bridge addon , where passing a uint array from javascript and typecasting it into a string in the bridged java code caused 10x load. We have to look case by case to specifically point at a performance overhead |
|
I did a performance test and find that the first call to addon function does takes more time incase of a larger module, but after that regardless of small or larger exported module the results were always the same |
|
Performance entry for a first call to a function in a smaller module Performance entry for a first call to a function in larger module I have repeated the test locally (OSX, node.js 14.0 ) many times , result is always consistent |
|
Hi @twuky , As you can see from @deepakrkris 's analysis above, there was no noticeable difference in executing functions in small and large bindings, except for a small difference when calling the function the first time -- this is expected. Do you have any concerns from your code? If you believe there are further issues, is it possible for you to provide us with a fully reproducible repo so we can investigate your code directly? |
This is just a question really - I'm not running into any actual errors at the moment.
I've been experimenting with using this api to create bindings for this graphics library: https://github.com/raysan5/raylib
I started with the yeoman template and wrote a binding that exported 13 functions to be able to test loading textures and drawing them to the screen. The binding has functions that simply wrap library functions and then I add them to an Napi::Object Init, like in the template and export it.
Once I got the hang of using this api I started working on a script that could generate a .cc file with the bindings for the rest of the functions, which at this point seems to compile and run though I haven't tested them all. This version has 475 exported functions.
I wrote a quick 'benchmark' to test how many textures it could draw per frame above a certain framerate. My initial test bindings seem to run this benchmark ~20% faster than the module with 475 functions.
I understand there is some overhead in transferring data from JS to the CPP addon - but do you have any information or reasoning why adding more functions to the addon would slow things down like this? Is there some other way to structure the module that better handles a larger amount of functions - or prioritize ones that need to be called more frequently?
The text was updated successfully, but these errors were encountered: