Node.js Add-ons for High Performance Numeric Computing

stdlib
Athan Reines | @kgryte | @stdlibjs

Survey

Overview

     
  1. Intro
  2. Toolchain
  3. Numeric Computing
  4. Basic Example
  5. BLAS
  6. Performance
  7. Challenges
  8. N-API
  9. Conclusions

Native Add-ons

Interface between JS running in Node.js and C/C++ libraries

APIs

     
  • V8
  • libuv
  • Internal Libraries
  • Dependencies

Examples

Why?

     
  • Leverage existing codebases
  • Access lower-level APIs
  • Non-JavaScript features
  • Performance

Toolchain

node-gyp

GYP

Challenges

     
  • V8
  • NAN
  • GYP
  • Engine Bias

Numeric Computing

Basic Example


/* hypot.h */
#ifndef C_HYPOT_H
#define C_HYPOT_H

#ifdef __cplusplus
extern "C" {
#endif

double c_hypot( const double x, const double y );

#ifdef __cplusplus
}
#endif

#endif
				

/* hypot.c */
#include <math.h>
#include "hypot.h"

double c_hypot( const double x, const double y ) {
    double tmp;
    double a;
    double b;
    if ( isnan( x ) || isnan( y ) ) {
        return NAN;
    }
    if ( isinf( x ) || isinf( y ) ) {
        return INFINITY;
    }
    a = x;
    b = y;
    if ( a < 0.0 ) {
        a = -a;
    }
    if ( b < 0.0 ) {
        b = -b;
    }
    if ( a < b ) {
        tmp = b;
        b = a;
        a = tmp;
    }
    if ( a == 0.0 ) {
        return 0.0;
    }
    b /= a;
    return a * sqrt( 1.0 + (b*b) );
}
				

/* addon.cpp */
#include <nan.h>
#include "hypot.h"

namespace addon_hypot {

    using Nan::FunctionCallbackInfo;
    using Nan::ThrowTypeError;
    using Nan::ThrowError;
    using v8::Number;
    using v8::Local;
    using v8::Value;

    void node_hypot( const FunctionCallbackInfo<Value>& info ) {
        if ( info.Length() != 2 ) {
            ThrowError( "invalid invocation. Must provide 2 arguments." );
            return;
        }
        if ( !info[ 0 ]->IsNumber() ) {
            ThrowTypeError( "invalid input argument. First argument must be a number." );
            return;
        }
        if ( !info[ 1 ]->IsNumber() ) {
            ThrowTypeError( "invalid input argument. Second argument must be a number." );
            return;
        }
        const double x = info[ 0 ]->NumberValue();
        const double y = info[ 1 ]->NumberValue();

        Local<Number> h = Nan::New( c_hypot( x, y ) );
        info.GetReturnValue().Set( h );
    }

    NAN_MODULE_INIT( Init ) {
        Nan::Export( target, "hypot", node_hypot );
    }

    NODE_MODULE( addon, Init )
}
				

# binding.gyp
{
  'targets': [
    {
      'target_name': 'addon',
      'sources': [
        'addon.cpp',
        'hypot.c'
      ],
      'include_dirs': [
		'<!(node -e "require(\'nan\')")',
		'./'
      ]
    }
  ]
}
				

# Navigate to add-on directory:
$ cd path/to/hypot/binding.gyp

# Generate build files:
$ node-gyp configure

# On Windows:
# node-gyp configure --msvs_version=2015

# Build add-on:
$ node-gyp build
				

/* hypot.js */
var hypot = require( './path/to/build/Release/addon.node' ).hypot;

var h = hypot( 5.0, 12.0 );
// returns 13.0
				
ops/sec perf
Builtin 3,954,799 1x
Native 4,732,108 1.2x
JavaScript 7,337,790 1.85x

BLAS


! dasum.f
! Computes the sum of absolute values.
double precision function dasum( N, dx, stride )
  implicit none
  integer :: stride, N
  double precision :: dx(*)
  double precision :: dtemp
  integer :: nstride, mp1, m, i
  intrinsic dabs, mod
  ! ..
  dasum = 0.0d0
  dtemp = 0.0d0
  ! ..
  if ( N <= 0 .OR. stride <= 0 ) then
    return
  end if
  ! ..
  if ( stride == 1 ) then
    m = mod( N, 6 )
    if ( m /= 0 ) then
      do i = 1, m
        dtemp = dtemp + dabs( dx( i ) )
      end do
      if ( N < 6 ) then
        dasum = dtemp
        return
      end if
    end if
    mp1 = m + 1
    do i = mp1, N, 6
      dtemp = dtemp + &
        dabs( dx( i ) ) + dabs( dx( i+1 ) ) + &
        dabs( dx( i+2 ) ) + dabs( dx( i+3 ) ) + &
        dabs( dx( i+4 ) ) + dabs( dx( i+5 ) )
    end do
  else
    nstride = N * stride
    do i = 1, nstride, stride
      dtemp = dtemp + dabs( dx( i ) )
    end do
  end if
  dasum = dtemp
  return
end function dasum
				

! dasumsub.f
! Wraps dasum as a subroutine.
subroutine dasumsub( N, dx, stride, sum )
  implicit none
  ! ..
  interface
    double precision function dasum( N, dx, stride )
      integer :: stride, N
      double precision :: dx(*)
    end function dasum
  end interface
  ! ..
  integer :: stride, N
  double precision :: sum
  double precision :: dx(*)
  ! ..
  sum = dasum( N, dx, stride )
  return
end subroutine dasumsub
				

/* dasum_fortran.h */
#ifndef DASUM_FORTRAN_H
#define DASUM_FORTRAN_H

#ifdef __cplusplus
extern "C" {
#endif

void dasumsub( const int *, const double *, const int *, double * );

#ifdef __cplusplus
}
#endif

#endif
				

/* dasum.h */
#ifndef DASUM_H
#define DASUM_H

#ifdef __cplusplus
extern "C" {
#endif

double c_dasum( const int N, const double *X, const int stride );

#ifdef __cplusplus
}
#endif

#endif
				

/* dasum_f.c */
#include "dasum.h"
#include "dasum_fortran.h"

double c_dasum( const int N, const double *X, const int stride ) {
    double sum;
    dasumsub( &N, X, &stride, &sum );
    return sum;
}
				

/* addon.cpp */
#include <nan.h>
#include "dasum.h"

namespace addon_dasum {

    using Nan::FunctionCallbackInfo;
    using Nan::TypedArrayContents;
    using Nan::ThrowTypeError;
    using Nan::ThrowError;
    using v8::Number;
    using v8::Local;
    using v8::Value;

    void node_dasum( const FunctionCallbackInfo<Value>& info ) {
        if ( info.Length() != 3 ) {
            ThrowError( "invalid invocation. Must provide 3 arguments." );
            return;
        }
        if ( !info[ 0 ]->IsNumber() ) {
            ThrowTypeError( "invalid input argument. First argument must be a number." );
            return;
        }
        if ( !info[ 2 ]->IsNumber() ) {
            ThrowTypeError( "invalid input argument. Third argument must be a number." );
            return;
        }
        const int N = info[ 0 ]->Uint32Value();
        const int stride = info[ 2 ]->Uint32Value();

        TypedArrayContents<double> X( info[ 1 ] );

        Local<Number> sum = Nan::New( c_dasum( N, *X, stride ) );
        info.GetReturnValue().Set( sum );
    }

    NAN_MODULE_INIT( Init ) {
        Nan::Export( target, "dasum", node_dasum );
    }

    NODE_MODULE( addon, Init )
}
				

$ gfortran \
    -std=f95 \
    -ffree-form \
    -O3 \
    -Wall \
    -Wextra \
    -Wimplicit-interface \
    -fno-underscoring \
    -pedantic \
    -fPIC \
    -c \
    -o dasum.o \
    dasum.f
$ gfortran \
    -std=f95 \
    -ffree-form \
    -O3 \
    -Wall \
    -Wextra \
    -Wimplicit-interface \
    -fno-underscoring \
    -pedantic \
    -fPIC \
    -c \
    -o dasumsub.o \
    dasumsub.f
$ gcc \
    -std=c99 \
    -O3 \
    -Wall \
    -pedantic \
    -fPIC \
    -I ../include \
    -c \
    -o dasum_f.o \
    dasum_f.c
$ gcc -o dasum dasum_f.o dasumsub.o dasum_f.o -lgfortran
				

# binding.gyp
{
  'variables': {
    'addon_target_name%': 'addon',
    'addon_output_dir': './src',
    'fortran_compiler%': 'gfortran',
    'fflags': [
      '-std=f95',
      '-ffree-form',
      '-O3',
      '-Wall',
      '-Wextra',
      '-Wimplicit-interface',
      '-fno-underscoring',
      '-pedantic',
      '-c',
    ],
    'conditions': [
      [
        'OS=="win"',
        {
          'obj': 'obj',
        },
        {
          'obj': 'o',
        }
      ],
    ],
  },
				

# binding.gyp (cont.)
  'targets': [
    {
      'target_name': '<(addon_target_name)',
      'dependencies': [],
      'include_dirs': [
        '<!(node -e "require(\'nan\')")',
        '../include',
      ],
      'sources': [
        'dasum.f',
        'dasumsub.f',
        'dasum_f.c',
        'addon.cpp'
      ],
      'link_settings': {
        'libraries': [
          '-lgfortran',
        ],
        'library_dirs': [],
      },
      'cflags': [
        '-Wall',
        '-O3',
      ],
      'cflags_c': [
        '-std=c99',
      ],
      'cflags_cpp': [
        '-std=c++11',
      ],
      'ldflags': [],
      'conditions': [
        [
          'OS=="mac"',
          {
            'ldflags': [
              '-undefined dynamic_lookup',
              '-Wl,-no-pie',
              '-Wl,-search_paths_first',
            ],
          },
        ],
        [
          'OS!="win"',
          {
            'cflags': [
              '-fPIC',
            ],
          },
        ],
      ],
				

# binding.gyp (cont.)
      'rules': [
        {
          'extension': 'f',
          'inputs': [
            '<(RULE_INPUT_PATH)'
          ],
          'outputs': [
            '<(INTERMEDIATE_DIR)/<(RULE_INPUT_ROOT).<(obj)'
          ],
          'conditions': [
            [
              'OS=="win"',
              {
                'rule_name': 'compile_fortran_windows',
                'process_outputs_as_sources': 0,
                'action': [
                  '<(fortran_compiler)',
                  '<@(fflags)',
                  '<@(_inputs)',
                  '-o',
                  '<@(_outputs)',
                ],
              },
              {
                'rule_name': 'compile_fortran_linux',
                'process_outputs_as_sources': 1,
                'action': [
                  '<(fortran_compiler)',
                  '<@(fflags)',
                  '-fPIC',
                  '<@(_inputs)',
                  '-o',
                  '<@(_outputs)',
                ],
              }
            ],
          ],
        },
      ],
    },
				

# binding.gyp (cont.)
    {
      'target_name': 'copy_addon',
      'type': 'none',
      'dependencies': [
        '<(addon_target_name)',
      ],
      'actions': [
        {
          'action_name': 'copy_addon',
          'inputs': [],
          'outputs': [
            '<(addon_output_dir)/<(addon_target_name).node',
          ],
          'action': [
            'cp',
            '<(PRODUCT_DIR)/<(addon_target_name).node',
            '<(addon_output_dir)/<(addon_target_name).node',
          ],
        },
      ],
    },
  ],
}
				

$ cd path/to/dasum/binding.gyp
$ node-gyp configure
# node-gyp configure --msvs_version=2015
$ node-gyp build
				

/* dasum.js */
var dasum = require( './path/to/src/addon.node' ).dasum;

var x = new Float64Array( [ 1.0, -2.0, 3.0, -4.0, 5.0 ] );
var s = dasum( x.length, x, 1 );
// returns 15.0
				
Length JavaScript Native Perf
10 22,438,020 7,435,590 0.33x
100 4,350,384 4,594,292 1.05x
1,000 481,417 827,513 1.71x
10,000 28,186 97,695 3.46x
100,000 1,617 9,471 5.85x
1,000,000 153 873 5.7x

/* dasum_cblas.h */
#ifndef DASUM_CBLAS_H
#define DASUM_CBLAS_H

#ifdef __cplusplus
extern "C" {
#endif

double cblas_dasum( const int N, const double *X, const int stride );

#ifdef __cplusplus
}
#endif

#endif
				

/* dasum_cblas.c */
#include "dasum.h"
#include "dasum_cblas.h"

double c_dasum( const int N, const double *X, const int stride ) {
    return cblas_dasum( N, X, stride );
}
				

# binding.gyp
{
  'variables': {
    'addon_target_name%': 'addon',
    'addon_output_dir': './src',
  },
  'targets': [
    {
      'target_name': '<(addon_target_name)',
      'dependencies': [],
      'include_dirs': [
        '<!(node -e "require(\'nan\')")',
        './../include',
      ],
      'sources': [
        'dasum_cblas.c',
        'addon.cpp'
      ],
      'link_settings': {
        'libraries': [
          '-lblas',
        ],
        'library_dirs': [],
      },
      'cflags': [
        '-Wall',
        '-O3',
      ],
      'cflags_c': [
        '-std=c99',
      ],
      'cflags_cpp': [
        '-std=c++11',
      ],
      'ldflags': [
		'-undefined dynamic_lookup',
        '-Wl,-no-pie',
        '-Wl,-search_paths_first'
      ],
    },
    {
      'target_name': 'copy_addon',
      'type': 'none',
      'dependencies': [
        '<(addon_target_name)',
      ],
      'actions': [
        {
          'action_name': 'copy_addon',
          'inputs': [],
          'outputs': [
            '<(addon_output_dir)/<(addon_target_name).node',
          ],
          'action': [
            'cp',
            '<(PRODUCT_DIR)/<(addon_target_name).node',
            '<(addon_output_dir)/<(addon_target_name).node',
          ],
        },
      ],
    },
  ],
}
				
Length JavaScript wasm Native Perf
10 22,438,020 18,226,375 7,084,870 0.31x
100 4,350,384 6,428,586 6,428,626 1.47x
1,000 481,417 997,234 3,289,090 6.83x
10,000 28,186 110,540 355,172 12.60x
100,000 1,617 11,157 30,058 18.58x
1,000,000 153 979 1,850 12.09x

Challenges

     
  • Bugs
  • Standards
  • Proprietary
  • Windows
  • Portability
  • Complexity

Modularity


{
    "options": {
        "os": "linux",
        "blas": "",
        "wasm": false
    },
    "fields": [
        {
            "field": "src",
            "resolve": true,
            "relative": true
        },
        {
            "field": "include",
            "resolve": true,
            "relative": true
        },
        {
            "field": "libraries",
            "resolve": false,
            "relative": false
        },
        {
            "field": "libpath",
            "resolve": true,
            "relative": false
        }
    ],
    "confs": [
        {
            "os": "linux",
            "blas": "",
            "wasm": false,
            "src": [
                "./src/dasum.f",
                "./src/dasumsub.f",
                "./src/dasum_f.c"
            ],
            "include": [
                "./include"
            ],
            "libraries": [],
            "libpath": [],
            "dependencies": []
        },
        {
            "os": "linux",
            "blas": "openblas",
            "wasm": false,
            "src": [
                "./src/dasum_cblas.c"
            ],
            "include": [
                "./include"
            ],
            "libraries": [
                "-lopenblas",
                "-lpthread"
            ],
            "libpath": [],
            "dependencies": []
        },
        {
            "os": "mac",
            "blas": "",
            "wasm": false,
            "src": [
                "./src/dasum.f",
                "./src/dasumsub.f",
                "./src/dasum_f.c"
            ],
            "include": [
                "./include"
            ],
            "libraries": [],
            "libpath": [],
            "dependencies": []
        },
        {
            "os": "mac",
            "blas": "apple_accelerate_framework",
            "wasm": false,
            "src": [
                "./src/dasum_cblas.c"
            ],
            "include": [
                "./include"
            ],
            "libraries": [
                "-lblas"
            ],
            "libpath": [],
            "dependencies": []
        },
        {
            "os": "mac",
            "blas": "openblas",
            "wasm": false,
            "src": [
                "./src/dasum_cblas.c"
            ],
            "include": [
                "./include"
            ],
            "libraries": [
                "-lopenblas",
                "-lpthread"
            ],
            "libpath": [],
            "dependencies": []
        },
        {
            "os": "win",
            "blas": "",
            "wasm": false,
            "src": [
                "./src/dasum.c"
            ],
            "include": [
                "./include"
            ],
            "libraries": [],
            "libpath": [],
            "dependencies": []
        },
        {
            "os": "",
            "blas": "",
            "wasm": true,
            "src": [
                "./src/dasum.c"
            ],
            "include": [
                "./include"
            ],
            "libraries": [],
            "libpath": [],
            "dependencies": []
        }
    ]
}
				

N-API

Features

     
  • Stability
  • Compatibility
  • VM Neutrality

/* addon.cpp */
#include <node_api.h>
#include <assert.h>
#include "hypot.h"

namespace addon_hypot {

    napi_value node_hypot( napi_env env, napi_callback_info info ) {
        napi_status status;

        size_t argc = 2;
        napi_value argc[ 2 ];
        status = napi_get_cb_info( env, info, &argc, args, nullptr, nullptr );
        assert( status == napi_ok );

        if ( argc < 2 ) {
            napi_throw_type_error( env, "invalid invocation. Must provide 2 arguments." );
            return nullptr;
        }

        napi_value vtype0;
        status = napi_typeof( env, args[ 0 ], &vtype0 );
        assert( status == napi_ok );
        if ( vtype0 != napi_number ) {
            napi_throw_type_error( env, "invalid input argument. First argument must be a number." );
            return nullptr;
        }

        napi_value vtype1;
        status = napi_typeof( env, args[ 0 ], &vtype1 );
        assert( status == napi_ok );
        if ( vtype1 != napi_number ) {
            napi_throw_type_error( env, "invalid input argument. Second argument must be a number." );
            return nullptr;
        }

        const double x;
        status = napi_get_value_double( env, args[ 0 ], &x );
        assert( status == napi_ok );

        const double y;
        status = napi_get_value_double( env, args[ 1 ], &y );
        assert( status == napi_ok );

        napi_value h;
        status = napi_create_number( env, c_hypot( x, y ), &h );
        assert( status == napi_ok );

        return h;
    }

    #define DECLARE_NAPI_METHOD( name, func ) { name, 0, func, 0, 0, 0, napi_default, 0 }

    void Init( napi_env env, napi_value exports, napi_value module, void* priv ) {
        napi_status status;
        napi_property_descriptor addDescriptor = DECLARE_NAPI_METHOD( "hypot", node_hypot );
        status = napi_define_properties( env, exports, 1, &addDescriptor );
        assert( status == napi_ok );
    }

    NAPI_MODULE( addon, Init )
}
				

Conclusions

     
  • Parity
  • Performance
  • Progress

Thank you!

stdlib

https://github.com/stdlib-js/stdlib
https://www.patreon.com/athan

Appendix






















				

The End