How to get a performance boost using Node.js native addons

Probably you have heard about it thousands of times but today I want to show you what are Node.js native modules and why you should care about them.

Node.js Addons are dynamically-linked shared objects, written in C or C++, that can be loaded into Node.js using the require() function, and used just as if they were an ordinary Node.js module.

Sounds pretty well right but, why should I write C++ code since I’m very comfortable with Javascript and the last time I saw C++ code was in the university… the answer is nothing else than Performance!

A practical example

Let’s say we are building an online palindrome calculator tool, thanks to Javascript it’s a hight level language, you came up with a clean solution very fast:

Genius right? you just deploy the code to production and go to sleep… but after some days you realize that your algorithm is not as good as it look, actually is very slow, so you start exploring new horizons…

Node addons ecosystem

The following tools are the ones we will need to create our addon:

  • node-gyp: cross-platform cli for compiling native addons.
  • node-bindings: Helper module for loading your native addon .node file.
  • nan: Tool for making add-on development easier across Node versions.

Install all of them doing

$ npm i node-gyp -g && npm i node-bindings nan --save

Later, let’s add “gypfile”: true to our package and create a new file called binding.gyp:

{
“targets”: [
{
“target_name”: “palindrome”,
“sources”: [ “palindrome.cc” ],
“include_dirs”: [ “<!(node -e \”require(‘nan’)\”)” ]
}
]
}

Now, we are ready to write our palindrome method on C++, let’s take a look at the code:

Probably not the best implementation but it works in O(n) time, so we are good for now. We are ready to review the code, first the following lines

void Init(Local<Object> exports, Local<Object> module) { 
NODE_SET_METHOD(module, “exports”, IsPalindrome);
}
NODE_MODULE(addon, Init)

are just exposing the IsPalindrome function to later be required by Node, take a look at NODE_MODULE and NODE_SET_METHOD methods from the Node source code.

Other interesting part of the code is the following

void IsPalindrome(const FunctionCallbackInfo<Value>& info) {
String::Utf8Value sentence(info[0]->ToString());
std::string str = std::string(*sentence);

here we are using some v8 classes, FunctionCallbackInfo provides access to information about the context of the call, including the receiver, the number and values of arguments, and the holder of the function. Finally, we use the Utf8Value class to convert the argument to std string, something that we can use later to operate in the algorithm.

It’s time to measure the performance between both implementations, for do that we’ll use benchmarkjs, which is the same library that jsperf.com uses internally:

$ npm i --save benchmark
C palindrome x 1,353,176 ops/sec ±1.98% (80 runs sampled)
Javascript palindrome x 293,383 ops/sec ±1.34% (87 runs sampled)
Fastest: C palindrome
Slowest: Javascript palindrome

Here we go, C palindrome is 460% faster than the Javascript one! 😏😏 Crazy improvement right? but wait, we have cheated a bit here… We used 2 different implementations, the javascript one liner was quite more expensive than the C++ one hehe. Now, let’s try the exactly same implementation than C++ in Javascript:

C palindrome x 1,370,148 ops/sec ±1.32% (80 runs sampled)
Javascript palindrome x 3,326,042 ops/sec ±0.98% (82 runs sampled)
Fastest: Javascript palindrome
Slowest: C palindrome

Surprisingly the javascript implementation is 240% faster now 😲… Does that makes any sense? Fortunately we can do something to fix that.

Introducing nan

Native Abstractions for Node.js
Thanks to the changes in V8 (and some in Node core), keeping native addons compiling happily across versions, is a minor nightmare, the goal of nan is to store all logic necessary to develop native Node.js addons without having to inspect NODE_MODULE_VERSION.

The reason of why our native implementation was slower than the js is this:

String::Utf8Value sentence(info[0]->ToString());
std::string str = std::string(*sentence);

Here we are doing nothing else than casting the first argument into a std string, but due to how the Utf8Value works internally we are paying a high price in the conversion. Now let’s take a look at how this must be done using nan:

Nan::Utf8String arg0(info[0]);
char *str = *arg0;

Now instead of using the v8 Utf8Value, we will just a nan Uft8String and later cast it into an array of chars. We also have to apply some other minors changes to code to make it work with the char array:

C palindrome x 5,753,415 ops/sec ±1.40% (84 runs sampled)
Javascript palindrome x 3,307,899 ops/sec ±1.28% (84 runs sampled)
Fastest: C palindrome
Slowest: Javascript palindrome

C++ do 170% better than js now thanks to nan 😃

Biggest palindrome ever

Finally, let’s try with the bigger palindrome ever, just to see if the performance gain is still the same or get better with bigger strings. Let me introduce you to the 17,826 Word Palindrome 😈. Here are the results:

C palindrome x 4,636 ops/sec ±1.10% (83 runs sampled)
Javascript palindrome x 1,712 ops/sec ±1.22% (83 runs sampled)
Fastest: C palindrome
Slowest: Javascript palindrome

Again C++ gets a better performance, this time 270%. Maybe not that much, but, you can figure out how this get better with bigger strings.

Conclusion

We just saw how to create and benchmark Node.js native addons, probably in the above example we didn’t get that much performance since we are just working with strings but at least it shows you and end to end example of how to do it.

You can find all the code of the palindrome addon here as well as the benchmark example we used

Finally I have to say that was really difficult to find proper documentation and examples of node addons, anyways here you have the references I used during my journey.