stdlib
stdlib
Athan Reines | @kgryte | @stdlibjs

Node.js Add-ons for High Performance Numeric Computing

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 3954799 1x
Native 4732108 1.2x
JavaScript 7337790 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 22438020 7435590 0.33x
100 4350384 4594292 1.05x
1000 481417 827513 1.71x
10000 28186 97695 3.46x
100000 1617 9471 5.85x
1000000 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 Native Perf
10 22438020 7084870 0.31x
100 4350384 6428626 1.47x
1000 481417 3289090 6.83x
10000 28186 355172 12.60x
100000 1617 30058 18.58x
1000000 153 1850 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