pondělí 6. října 2014

Node.js FFI vs. Addon performance

Node.js is by itself suprisingly fast but sometimes it is neccessery to add to your application some 'horse power' by calling some code written in C/C++. Fortunatelly Node.js offers even two ways how to accomplish that.
  • FFI - Foreign Function Interface
  • Addon
Let me start with the easier one and that is FFI. FFI is a mechanism how one program can call functions from another written usually in different programming language. To use it in node you have to create a dynamic linked library and than use node package 'ffi' that will load this library during runtime. See https://github.com/node-ffi/node-ffi for more info.

Example of library usable for FFI calls:

#include <stdint.h>

#if defined(WIN32) || defined(_WIN32)
#define EXPORT __declspec(dllexport)
#else
#define EXPORT
#endif

extern "C" EXPORT int add(int a, int b) {
 return a + b;
}

Client using FFI call:

var ffi = require('ffi');

var lib = ffi.Library('./ffi/lib/libtest', {
 'add': ['int', ['int', 'int']]
});

lib.add.async(3, 5, function(err, res) {
 console.log('This should be eight: ' + res);   
 });
}
The other way of calling C/C++ code from node is to create an addon. This addon than wrappes the library we want to use. The disadvantage of this approach is that you actually have to program something in C/C++. It is not long code but still for somebody this might be dealbreaker. However this approach has also one huge advantage and that is the performance.

Example of function wrapper in addon:

#include <nan.h>
#include <iostream>

using namespace v8;

NAN_METHOD(AddAsync) {
  NanScope();  
...
  double arg0 = args[0]->NumberValue();
  double arg1 = args[1]->NumberValue();
  Local<Number> num = NanNew(arg0 + arg1); 
...
  Local<Function> cb = args[2].As<Function>();
  const unsigned argc = 1;
  Local<Value> argv[argc] = { NanNew(num)};
  NanMakeCallback(NanGetCurrentContext()->Global(), cb, argc, argv);

  NanReturnUndefined();
}

Client using the addon:

var addon = require('bindings')('addon');

addon.addAsync(3, 2, function(value) {
  console.log('This should be five: ' + value); 
});
}

I tested it on a async operation than sums two numbers and returns result.
Results for 10 000 and 1 000 000 calls:

$ ./benchmark.sh 10000
Running 'addon' test...
Operation count: 10000
Execution time: 2
Running 'FFI' test...
Operation count: 10000
Execution time: 168

$ ./benchmark.sh 1 000 000
Running 'addon' test...
Operation count: 1 000 000
Execution time: 167
Running 'FFI' test...
Operation count: 1 000 000
Execution time: 16212



Please follow this link to the git repository if you want to run the tests on your own. My script is quite easy to use.

As you can see FFI is almost 100 times slower than simple addon. But a user can still benefit by using FFI for calling functions with heavy calculations or functions that are not called very often.

Note that purposse of this article is just to compare performance of those two technologies in as simple example as possible. That is why the code of addon is not actually fully assynchronous. This approach is ussually good enought for most of the cpp library calls. For fully assynchronous but more complex sollution please take a look at NanAsyncWorker + NanAsyncQueueWorker.