function isPalindrome(str) {
return str === str.split('').reverse().join('');
}
끝났다. 이제 코드를 프로덕션에 디플로이하고 잠이나 자러 가면 된다.
하지만, 시간이 흐른뒤에 위 알고리즘은 보기만큼 좋지 않고 실제로 엄청 느리다는 것을 깨닫게 된다.
그리곤 다른 방법을 찾아 해매게 된다.
노드 애드온 생태계
애드온을 만들기 위해서는 아래의 툴들이 필요하다.
- node-gyp: 네이티브 애드온을 컴파일 하기 위한 크로스 플랫폼 cli
- node-bindings: 작성한 네이티브 애드온 .node 파일을 불러오기 위한 헬퍼 모듈
- nan: 노드버전별 애드온 개발을 쉽게 하기 위한 도구
아래의 명령으로 한번에 모두 설치할 수 있다.
$ npm i node-gyp -g && npm i node-bindings nan --save
이후 "gypfile": true
옵션을 package파일에 추가하고 binding.gyp 파일을 만든다.
binding.gyp
{
“targets”: [
{
“target_name”: “palindrome”,
“sources”: [ “palindrome.cc” ],
“include_dirs”: [ “<!(node -e \”require(‘nan’)\”)” ]
}
]
}
이제 C++로 palindrome 메소드를 작성하기 위한 준비가 끝났다. 아래의 코드를 살펴 보자.
#include <nan.h>
using namespace v8;
void IsPalindrome(const FunctionCallbackInfo<Value>& info) {
String::Utf8Value sentence(info[0]->ToString());
std::string str = std::string(*sentence);
int len = str.length();
int half = len / 2;
int start = 0;
int end = len - 1;
int space = 32;
bool isPal = true;
while (half > 0 && isPal) {
bool startSpace = str.at(start) == space;
bool endSpace = str.at(end) == space;
if (str.at(start) == str.at(end)) {
start++;
end--;
} else if (startSpace || endSpace) {
startSpace && start++;
endSpace && end--;
} else {
isPal = false;
}
half--;
}
info.GetReturnValue().Set(isPal);
}
void Init(Local<Object> exports, Local<Object> module) {
NODE_SET_METHOD(module, "exports", IsPalindrome);
}
NODE_MODULE(addon, Init);
아마 최상의 구현은 아닐지 모른다. 하지만 *O(n)*의 시간 복잡도를 가지고 있어 지금으론 충분하다. 이제 코드를 확인해보자. 우선 아래의 코드부터 시작한다.
void Init(Local<Object> exports, Local<Object> module) {
NODE_SET_METHOD(module, “exports”, IsPalindrome);
}
NODE_MODULE(addon, Init);
위 코드는 추후 노드에서 사용될 수 있도록 노출해준다. NODE_MODULE과 NODE_SET_METHOD 메소드의 소스코드는 한 번 살펴보도록 하자.
다음으로 확인해야 할 코드는 아래와 같다.
void IsPalindrome(const FunctionCallbackInfo<Value>& info) {
String::Utf8Value sentence(info[0]->ToString());
std::string str = std::string(*sentence);
여기서 우린 v8의 클래스를 사용하게 된다. FunctionCallbackInfo는 호출하는 컨텍스트에 대한 정보에 접근할 수 있게 해 주는데, 리시버 그리고 인자들의 개수와 값, 함수의 홀더에 대한 정보를 포함한다.
마침내 Utf8Value 클래스를 사용해 인자를 std string으로 변환하게 되고 이것들이 우리의 알고리즘 안에서 사용될 것이다.
두 가지의 구현의 퍼포먼스를 측정하기 위해서 benchmarkjs를 사용하는데 jsperf.com 내부에서 사용하는 라이브러리이다.
var Benchmark = require('benchmark');
var palindromeC = require('bindings')('palindrome.node');
var palindromeJs = require('./palindrome.js');
var suite = new Benchmark.Suite;
var str = 'a man a plan a cat a ham a yak a yam a hat a canal panama';
suite
.add('Javascript palindrome', function() {
palindromeJs(str);
})
.add('C palindrome', function() {
palindromeC(str);
})
.on('cycle', cycle)
.on('complete', complete)
.run({ 'async': true });
function cycle(event) {
console.log(String(event.target));
}
function complete(a,b) {
console.log('Fastest: ' + this.filter('fastest').map('name'));
console.log('Slowest: ' + this.filter('slowest').map('name'));
}
C palindrome x 1,353,176 ops/sec ±1.98% (80 runs sampled)
Javascript palindrome x 293,383 ops/sec ±1.34% (87 runs sampled)
Fastest: C palindrome
Slowest: Javascript palindrome
C palindrome이 자바스크립트 구현보다 460% 빠르다.
미친 성능 향상이 아닌가? 하지만 여기엔 한가지 놓친 부분이 있다. 비교된 코드의 구현이 다르다. 자바스크립트의 구현이 C++의 구현보다 조금 더 비용이 비싼 구현이다.
C++과 정확하게 같은 구현으로 자바스크립트 코드를 다시 만들어보자.
function isPalindrome(str) {
var half = Math.round(str.length / 2);
var start = 0;
var end = str.length - 1;
var palindrome = true;
var SPACE = 32;
var COMMA = 44;
var startSpace, endSpace;
while (half && palindrome) {
startSpace = str.charCodeAt(start) === SPACE || str.charCodeAt(start) === COMMA;
endSpace = str.charCodeAt(end) === SPACE || str.charCodeAt(end) === COMMA;
if (str[start] == str[end]) {
start++;
end--;
} else if (startSpace || endSpace) {
startSpace && start++;
endSpace && end--;
} else {
palindrome = false;
}
half--;
}
return palindrome;
}
C palindrome x 1,370,148 ops/sec ±1.32% (80 runs sampled)
Javascript palindrome x 3,326,042 ops/sec ±0.98% (82 runs sampled)
Fastest: Javascript palindrome
Slowest: C palindrome
놀랍게도 이제는 오히려 자바스크립트 구현이 240% 더 빨라졌다. 이게 말이 되나?
다행스럽게도 이를 해결할 방법이 있다.
nan 소개
노드의 네이티브 추상화
V8(그리고 약간의 노드코어)의 미친듯한 변경 사항 때문에 네이티브 애드온을 버전별로 컴파일을 해야한다.
nan의 목적은 매번 NODE_MODULE_VERSION을 따로 조회하지 않고 네이티브 노드 애드온에는 필요한 로직만 담아 개발하게 하는 것이다.
우리의 네이티브 구현이 자바스크립트의 구현보다 느렸던 이유는 바로 아래의 코드 때문이다.
String::Utf8Value sentence(info[0]->ToString());
std::string str = std::string(*sentence);
여기서는 첫번째 인자를 std string으로 캐스팅하는것 외엔 아무것도 하지 않는다.
하지만 Utf8Value의 특별한 내부 동작 때문에 이 변경의 비용이 높아지게 된다.
자 이제 nan을 이용하면 어떻게 되는지 살펴보자.
Nan::Utf8String arg0(info[0]);
char *str = *arg0;
이제 v8의 Utf8Value대신 nan의 Uft8String을 사용하고 이후에 char의 array로 캐스팅할 것이다.
그외 char array와 동작하도록 코드의 몇몇 작은 부분을 수정했다.
#include <nan.h>
using namespace v8;
void IsPalindrome(const FunctionCallbackInfo<Value>& info) {
Nan::Utf8String arg0(info[0]);
char *str = *arg0;
size_t len = arg0.length();
int half = len / 2;
int start = 0;
int end = len - 1;
int space = 32;
int comma = 44;
bool isPal = true;
bool startSpace;
bool endSpace;
while (half > 0 && isPal) {
startSpace = str[start] == space || str[start] == comma;
endSpace = str[end] == space || str[end] == comma;
if (str[start] == str[end]) {
start++;
end--;
} else if (startSpace || endSpace) {
startSpace && start++;
endSpace && end--;
} else {
isPal = false;
}
half--;
}
info.GetReturnValue().Set(isPal);
}
void Init(Local<Object> exports, Local<Object> module) {
NODE_SET_METHOD(module, "exports", IsPalindrome);
}
NODE_MODULE(addon, Init);
C palindrome x 5,753,415 ops/sec ±1.40% (84 runs sampled)
Javascript palindrome x 3,307,899 ops/sec ±1.28% (84 runs sampled)
Fastest: C palindrome
Slowest: Javascript palindrome
이제 자바스크립트보다 C++이 170% 나아졌다. thanks to nan :)
가장 큰 회문(palindrome)
이제 큰 문자열에서도 성능의 향상되었는지 아니면 달라진 게 없는지를 체크해 볼 수 있도록 가장 큰 회문으로 테스트를 해보자.
(17,826 Word Palindrome)[http://norvig.com/pal17txt.html]으로 테스트를 해본뒤의 결과는 아래와 같다.
C palindrome x 4,636 ops/sec ±1.10% (83 runs sampled)
Javascript palindrome x 1,712 ops/sec ±1.22% (83 runs sampled)
Fastest: C palindrome
Slowest: Javascript palindrome
또다시 C++이 더 나은 퍼포먼스를 보여줬고 이번엔 270% 정도이다. 아마도 그 정도는 아닐지 몰라도 큰 문자열에서 얼마나 나아졌는지를 알 수가 있다.
결론
지금까지 노드 네이티브 애드온을 만드는 방법과 어떻게 벤치마크하는지를 알아봤다. 어쩌면 위의 예제들은 단지 스트링과의 작업만을 다루기 때문에 충분한 퍼포먼스의 향상을 얻지 못했을지도 모른다.
하지만 적어도 결과를 보여줬고 어떻게 하는지의 예제도 볼 수 있었다.
plaindrome 애드온의 모든 코드는 여기에서 확인 가능하고 사용된 벤치마크 예제는 여기에서 확인할 수 있다.
마지막으로 노드 애드온에 대한 예제나 적절한 문서를 찾는 것은 정말 어려웠다는 점을 밝히고 싶다. 아래에 내가 학습하면서 참고한 자료들을 공유한다.