Node.js 遇到效能瓶頸?試試 C++ Addons
覺得撰寫 Node.js 時有什麼效能瓶頸嗎?如果想讓它更快,可以試試看使用 C++ Addons 來撰寫模組。這種模組是用 C/C++ 來撰寫,最後可以當成一般的 npm 模組,在 JavaScript 中直接 require 使用。
覺得撰寫 Node.js 時有什麼效能瓶頸嗎?如果想讓它更快,可以試試看使用 C++ Addons 來撰寫模組。這種模組是用 C/C++ 來撰寫,最後可以當成一般的 npm 模組,在 JavaScript 中直接 require
使用。
最近在看一點定翼機的東西,想用 Electron 寫程式分析 Pixhawk 的飛行記錄,再做點視覺化。但是 log 檔案一次飛回來大概都有 50MB 左右,原本是用 Node.js 原生的 fs
和 readline
模組來讀取(中間有做一些其他計算),雖然做起來沒有什麼問題,但就是每次讀檔案都要跑個三四十秒,在做新功能的過程實在很頭痛。
在這邊第一個想到的是 WebAssembly,但一方面是資料傳遞很麻煩,只能用 ArrayBuffer
來處理各種資料,一開始是想說使用一個超大的 double
陣列來存這些資料,後來翻了很多資料說,其實 WASM 現在的速度其實並沒有比 JavaScript 快很多。那既然用 Node.js,就來試試看 C++ Addons 好了。
其實我也不是什麼 C++ 大師,只是曾經也寫過一點演算法競賽的題目,也稍微熟悉一點 C/C++ 寫法,想說如果只是簡單的文字處理應該可以寫寫看。換句話說:如果只是簡單的東西用 JS 跑很慢的話,你也可以嘗試用 C++ 改寫看看。
Node.js C++ Addon 是什麼?
Node.js C++ Addon 是用 C++ 撰寫的動態連接庫,換句話說有點類似 Windows 的 dll。Node.js 程式可以透過 require
方法載入,並直接共用變數。
此外,C++ Addon 感覺上就像在執行另外一個 C++ 程式,所以你可以直接讀取電腦上的 I/O、甚至使用底層 API。在這個例子中,因為我要讀取的 Pixhawk 記錄存在電腦上,我可以不需要透過其他方法傳遞給 Addon,而可以直接讓 Addon 用 C++ 的 ifstream infile
來讀取檔案。
撰寫的過程會建議先當作純 C++ 來寫,用 std::cout
來測試東西寫得對不對,最後再來考慮怎麼從 JS 讀參數、做完以後怎麼把結果丟回 JS。
C++ 程式範例
這邊先講一段 C++ 程式語言的範例,大概就是讀檔案進來,做點奇怪的處理、算算看檔案有幾行,最後輸出這個數值。
#include <iostream>
#include <string>
#include <fstream>
int main(int argc, char* argv[]){
FILE* fp;
char* filename = argv[1];
if ((fp = fopen(filename, "r")) == NULL) {
// 檔案不存在
std::cout << "file opened failed";
return 1;
}
std::ifstream infile(filename); // 開檔
std::string line;
int GPS_COUNT = 0;
while (std::getline(infile, line)) {
if (line.find("GPS") == 0) {
// 找到 GPS 資訊,開始做點正事
// do_something();
GPS_COUNT++;
}
}
std::cout << GPS_COUNT << std::endl;
return 0;
}
為了等等好處理,我會建議先把重點的部分抽離 main
函式,另外開一個函式來處理。像這樣:
#include <iostream>
#include <string>
#include <fstream>
int process(char* filename) {
std::ifstream infile(filename); // 開檔
std::string line;
int GPS_COUNT = 0;
while (std::getline(infile, line)) {
if (line.find("GPS") == 0) {
// 找到 GPS 資訊,開始做點正事
// do_something();
GPS_COUNT++;
}
}
return GPS_COUNT;
}
int main(int argc, char* argv[]) {
FILE* fp;
char* filename = argv[1];
if ((fp = fopen(filename, "r")) == NULL) {
// 檔案不存在
std::cout << "file opened failed";
return 1;
}
int GPS_COUNT = process(filename);
std::cout << GPS_COUNT << std::endl;
return 0;
}
把你這段邏輯弄完後,可以先用 g++
之類的編譯器編譯並跑跑看,確定這段邏輯沒有問題。不然等等接上 Node.js 上大爆炸,會搞不清楚到底是環境弄錯還是程式寫錯。
在 Windows 上,你可能會使用 Visual Studio 來撰寫 C++,就使用內建的編譯器吧。在 Linux/Mac 上,你可能可以使用這個指令來編譯並執行:
g++ hello.cc -o hello chmod +x hello ./hello
安裝 node-gyp 環境
等到主要程式邏輯沒有問題後,接著要用 node-gyp 這個東西編譯成 Node.js 可以用的 .node
模組。首先請安裝 node-gyp
:
npm install -g node-gyp
如果你是 Windows 使用者,你可能還得安裝
windows-build-tools
:npm install -g windows-build-tools
接著在你的專案根目錄(通常就是放 package.json
的那個資料夾)新增一個檔案叫 bindings.gyp
,裡面內容放這樣:
{
"targets": [
{
"target_name": "hello",
"sources": [ "addons/hello.cc" ],
"include_dirs": [
"<!(node -e \"require('nan')\")"
]
}
]
}
其中 "hello"
要改成你這個 addon 套件的名字,待會從 JS 裡面 require
的時候會用到;"addons/hello.cc"
要改成你的 C++ 檔案的名字。
安裝 NAN
我們剛剛在 bindings.gyp
裡面加入了 require('nan')
這樣的字眼,因此我們會需要 NAN 這個套件。這個 NAN 並不是 JS 中的 NaN、Not a Number,而是 Native Abstractions for Node.js,它有點像是 C++ 和 V8(JS 引擎)的中間層。要不是有 NAN 的存在,我們可能每次 V8 改版都要重寫 C++ 檔案。
另外為了方便等等快速載入我們的 Addon,我們會順便安裝 bindings
這個套件:
npm install --save nan bindings
把 C++ 程式接上 Node.js
接著就是要如何把這兩個東西接起來。先看一下簡單的 NAN 範例:
#include <nan.h>
void Add(const Nan::FunctionCallbackInfo<v8::Value>& info) {
if (info.Length() < 2) {
Nan::ThrowTypeError("Wrong number of arguments");
return;
}
if (!info[0]->IsNumber() || !info[1]->IsNumber()) {
Nan::ThrowTypeError("Wrong arguments");
return;
}
double arg0 = info[0]->NumberValue();
double arg1 = info[1]->NumberValue();
v8::Local<v8::Number> num = Nan::New(arg0 + arg1);
info.GetReturnValue().Set(num);
}
void Init(v8::Local<v8::Object> exports) {
exports->Set(Nan::New("add").ToLocalChecked(),
Nan::New<v8::FunctionTemplate>(Add)->GetFunction());
}
NODE_MODULE(addon, Init)
這個範例取自 node-addon-examples。
這邊可以看到它用了一個 void Init
取代我們的 int main
,而 Init
中的 exports->Set(...)
有點像是我們 Node.js 會寫的 module.exports
。Add
的部分則是跟一般的函式沒什麼兩樣,只是花了很多時間檢查丟進來的參數數量、參數型態,還有最後取得參數值、計算完再回傳。
回去看我們的原始程式。我們寫好了一個 int process
函式,呼叫時要傳入 char* filename
,最後回傳一個 int
。所以我們的 Code 應該要長這樣:
#include <nan.h>
void Method(const Nan::FunctionCallbackInfo<v8::Value>& info) {
v8::String::Utf8Value str(info[0]->ToString());
std::string _filename = *str;
char* filename = new char[_filename.length() + 1];
strcpy(filename, _filename.c_str());
int line = process(filename);
v8::Local<v8::Number> num = Nan::New(line);
info.GetReturnValue().Set(num);
}
void Init(v8::Local<v8::Object> exports) {
exports->Set(Nan::New("process").ToLocalChecked(),
Nan::New<v8::FunctionTemplate>(Method)->GetFunction());
}
NODE_MODULE(addon, Init)
再一起看一次這段程式碼。void Init
的部分算是在宣告 JS 中的 module.exports.process = Method
。會多這個函式而不是直接呼叫 process
,主要是因為我們不想去動原本的 C++ 程式碼,多寫一個函式來處理 v8 型態和 C++ 型態的轉換。
所以我們多寫一個 Method
函式,一開始從 info
中的第一個參數取得字串。這時的型態是 v8::String::Utf8Value
,我先把它轉成 std::string
再轉成 char*
,最後才把這個 filename
傳入 process
函式裡。
算完我們需要的值後,我們先存成 int
,再轉成 v8::Number
的型態。最後用 GetReturnValue().set()
來把 v8::Number
回傳給 JS。
編譯 C++ Addon
東西都寫的差不多後,就可以使用 node-gyp build
的語法來編譯這個模組,預設應該會放在 build/Release/hello.node
。由於我們剛剛有安裝 bindings
這個套件,因此可以直接這樣使用:
const helloAddon = require('bindings')('hello');
接著就可以開始使用這個套件,例如:
const helloAddon = require('bindings')('hello');
const num = hello.process('/path/to/somefile.txt');
console.log(num);
最後,這段程式碼應該會長這樣,供大家參考參考:
#include <nan.h>
#include <iostream>
#include <string>
#include <fstream>
int process(char* filename) {
std::ifstream infile(filename); // 開檔
std::string line;
int GPS_COUNT = 0;
while (std::getline(infile, line)) {
if (line.find("GPS") == 0) {
// 找到 GPS 資訊,開始做點正事
// do_something();
GPS_COUNT++;
}
}
return GPS_COUNT;
}
void Method(const Nan::FunctionCallbackInfo<v8::Value>& info) {
v8::String::Utf8Value str(info[0]->ToString());
std::string _filename = *str;
char* filename = new char[_filename.length() + 1];
strcpy(filename, _filename.c_str());
int line = process(filename);
v8::Local<v8::Number> num = Nan::New(line);
info.GetReturnValue().Set(num);
}
void Init(v8::Local<v8::Object> exports) {
exports->Set(Nan::New("process").ToLocalChecked(),
Nan::New<v8::FunctionTemplate>(Method)->GetFunction());
}
NODE_MODULE(addon, Init)