N-API add-on and JS code performance comparison

Denis Malykhin
Sep 2, 2018 · 6 min read

As soon as N-API became stable, since Node.js version 10, I was planning to try this technology. It was exciting for me to know how fast C add-ons can perform in compare with JavaScript code. To simplify add-on development, I choose the node-addon-api. Let’s write some code to test the performance, and the first test is the array sorting.

Working with arrays, sorting.

In this section, we’ll implement bubble sort and quicksort algorithms using JS and N-API and compare the performance. As you might know, the bubble sort algorithm has the complexity O(n²) and quicksort O(n log(n)). So we’re expecting that quicksort is faster than the bubble sort.

At first, we need some test data, let’s generate an array with random integers from 0 to 999 of given length:

const ARRAY_LENGTH = 1000

const targetArray = Array(ARRAY_LENGTH)
.fill(null)
.map(() => Math.round(Math.random() * 1000))

We are going to sort this array using the native sort method. The Array.prototype.sort is based on the quicksort algorithm for the V8 engine. We use console.time to measure the execution time.

console.time('JS native (quick) sort')
const sortedArrayJS = targetArray.sort()
console.timeEnd('JS native (quick) sort')

The time of computation is approximately 1,6 ms. Alright, what about the bubble sort? Here is the code snippet:

const sortedArrayJSBubble = [...targetArray]console.time('JS for bubble sort')
for (let i = 0; i < (sortedArrayJSBubble.length - 1); ++i) {
for (let j = 0; j < sortedArrayJSBubble.length - 1 - i; ++j ) {
if (sortedArrayJSBubble[j] > sortedArrayJSBubble[j + 1]) {
const temp = sortedArrayJSBubble[j + 1]
sortedArrayJSBubble[j + 1] = sortedArrayJSBubble[j]
sortedArrayJSBubble[j] = temp
}
}
}
console.timeEnd('JS for bubble sort')

The algorithm is straightforward. The execution time is 4,6 ms.

Let’s compare the results with C add-ons, but we should make some preparations first.

We should create the bindings.gyp file in the project root, something just like this:

{
"targets": [
{
"target_name": "module",
"cflags!": [ "-fno-exceptions" ],
"cflags_cc!": [ "-fno-exceptions" ],
"sources": [ "./src/module.cc" ],
'include_dirs': [
"<!@(node -p \"require('node-addon-api').include\")"
],
'dependencies': [
"<!(node -p \"require('node-addon-api').gyp\")"
],
'defines': [ 'NAPI_DISABLE_CPP_EXCEPTIONS' ],
}
]
}

So now we can create our midule.cc in the ./src folder and place the ‘Sort’ method there:

napi_value Sort(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
const Napi::Array inputArray = info[0].As<Napi::Array>();
const int sortType = info[1].As<Napi::Number>().Uint32Value();

unsigned int length = inputArray.Length();
unsigned int array[length];
unsigned int i;

for (i = 0; i < length; i++) {
array[i] = inputArray[i].As<Napi::Number>().Uint32Value();
}
unsigned int *arrayPointer = &array[0];

switch (sortType) {
case BUBBLE_SORT:
bubbleSort(arrayPointer, length);
break;
case QUICK_SORT:
quickSort(arrayPointer, length);
break;
default:
break;
}
Napi::Array outputArray = Napi::Array::New(env, length);
for (i = 0; i < length; i++) {
outputArray[i] = Napi::Number::New(env, double(array[i]));
}
return outputArray;
}

As you can see N-API has particular data structures with related methods. You can find the full documentation for wrapper.

Let’s take a look at code snippet above. First, we should get an environment in which method is being run — Napi::Env env = info.Env(). Second, we should get the method input params: array to sort and sorting type (quick or bubble). Then, get the input array length, create an empty array of unsigned integers with the length of the input array. The next step is to get values from the input array as unsigned integers and copy them to the blank array. Then, we are passing the array pointer to the sorting method. The last step is to create output array. So, we are creating new Napi::Array in env with the length of the input array and filling it with numbers from the sorted array. Then return the result.

I don’t describe sort methods here, they are really simple, but you can find them in the git repository.

To export the Sort method from the module, we should use this:

Napi::Object Init(Napi::Env env, Napi::Object exports) {
exports.Set(Napi::String::New(env, "sort"),
Napi::Function::New(env, Sort));
return exports;
}
NODE_API_MODULE(module, Init)

We should add the following script to the package.json file:

“start”: “node-gyp configure build && node — no-warnings index.js”

It builds our module and runs the JS script.

Alright, we are ready to use our module:

const addon = require('./build/Release/module.node')console.time('N-API bubble sort')
const sortedArrayCbubble = addon.sort(targetArray, BUBBLE_SORT)
console.timeEnd('N-API bubble sort')


console.time('N-API quick sort')
const sortedArrayCquick = addon.sort(targetArray, QUICK_SORT)
console.timeEnd('N-API quick sort')

Executing the above lines give as the following results: 0,310 ms for quicksort and 0,843 ms for bubble sort.

To compare the results I made several runs of the algorithms with the array sizes from 10 to 100000. You can see the results in the table below:

Here is the chart with results:

That’s it for arrays, let’s do more.

Image processing.

To compare the image processing, I chose Jimp — the great library written using JS and OpenCV — great C library.

I have to admit that the comparison is a little bit wrong because these libraries may implement different algorithms. However, it was interesting for me to compare their performance.

Let’s start with Jimp. We are going to convert 512 x 512 pixels Lenna RGB photo to grayscale:

console.time('Jimp to B&W')
Jimp.read('Lenna.png')
.then(lenna => {
return lenna
.greyscale()
.write('Lenna-bw-jimp.png')
})
.then(() => console.timeEnd('Jimp to B&W'))

This async operation lasts nearly 178 ms.

The same image resize to 1024 x 1024 pixels lasts approximately 439 ms:

const x = 1024
const y = 1024
console.time('Jimp resize')
Jimp.read('Lenna.png')
.then(lenna => {
return lenna
.resize(x, y)
.write('Lenna-resize-jimp.png')
})
.then(() => console.timeEnd('Jimp resize'))

To work with OpenCV, we should install it first. You can do it quickly on OSX:

brew install opencv@2
brew link --force opencv@2

Then we should modify bindings.gyp by adding this code to the ‘module’ target:

'libraries': [ "<!(pkg-config opencv --libs)"],'conditions': [
[
"OS==\"mac\"", {
"xcode_settings": {
"OTHER_CFLAGS": [
"-mmacosx-version-min=10.7",
"-std=c++11",
"-stdlib=libc++",
"<!(pkg-config opencv --cflags)"
],
"GCC_ENABLE_CPP_RTTI": "YES",
"GCC_ENABLE_CPP_EXCEPTIONS": "YES"
}
}
]
]

The ‘conditions’ section is used to define OS-specific parameters. The next step is to add a new method to the module.cc:

napi_value ToGrayScale(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
cv::string inPath = info[0].As<Napi::String>().Utf8Value();
cv::string outPath = info[1].As<Napi::String>().Utf8Value();
toGrayScale(inPath, outPath);
return Napi::Number::New(env, 1);
}

Here we’re getting the input and output file paths as strings and calling toGrayScale:

void toGrayScale(cv::string inPath, cv::string outPath) {
cv::Mat image, gray_image;
image = cv::imread(inPath, 1);
cv::cvtColor(image, gray_image, CV_BGR2GRAY);
cv::imwrite(outPath, gray_image);
}

This function makes all OpenCV calls: read file, convert it and save.

Don’t forget to export the function from module.cc:

exports.Set(Napi::String::New(env, "toGrayScale"),   
Napi::Function::New(env, ToGrayScale));

Now we can make calls in our JS code:

console.time('opencv to B&W')
addon.toGrayScale(path.join(__dirname, 'Lenna.png'), path.join(__dirname, 'Lenna-bw-n-api.png'))
console.timeEnd('opencv to B&W')

Resize method looks similar, you can find it in the git repository. The same image converts in 9.7 ms.

You can find the results in a table and on the chart:

Conclusion

As you can see from tests results N-API add-on performing much more faster than native JS code. It’s a little harder to write them without any C/C++ experience, but it’s not a big deal: you can handle it with some practice. Using the node-addon-api makes the developing easier. I hope, that performance comparison from this post motivates you to start using the N-API in your project. You can find the full code from this experiment here: https://github.com/malykhin/n-api-experiments

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade