RustでNode.jsを高速化





最近、ネットワークはしばしば「若くて有望な」言語であるRustに言及しています。 彼は私に好奇心と、それを多少なりとも役に立つものにしたいという欲求を喚起しました。 これは、カッコウの助けを借りて、ハリネズミとハリネズミを交わすというかなり奇妙な経験になりました。



そして、私はこれをやった。 node.jsにプロジェクトがあります。 ハッシュを必要とする機能があります。 さらに、非常に頻繁に-ほとんどすべての着信要求に対して。 このハッシュは衝突から私を保護するものではなく、通常は利便性のためにセキュリティを必要としないため、adler32アルゴリズムが使用されます。 短い出力値を提供します。



一部の不条理な点については、node.jsではそうではありません。 これがばかげている理由を説明させてください。 このアルゴリズムは通常、圧縮で使用され、特にgzipで使用されます。 Node.jsには、zlibモジュールに標準のgzip実装があります。 つまり、adler32は実際には存在しますが、暗黙的な形式です。 Pythonでは、比較のために、同様のモジュールで使用でき、使用できます。



まあまあ。 npmからサードパーティのパッケージを取得します。 私はこれを取りました: adler32-主に暗号モジュールと統合でき、他のハッシュアルゴリズムと同じ方法で使用できるためです。 これは便利です。 この場合、パフォーマンスについては特に考えませんでした。 それが何であれ、それはペニーです。 しかし、私は実験を計画していたので、このadler32は被害者によって選ばれました。



一般的に、始めましょう。 Rustの配置は簡単です。 ドキュメントは、 ロシア語英語の両方で非常にわかりやすいです。 Rustはバージョン1.15を使用しています。 楽しい事実:ロシア語のドキュメントは英語の直接翻訳ではなく、構造がわずかに異なります。 特に、ストリームの操作例が追加されました。



Rust自体に加えて、node.jsバージョン6.8.0、Visual Studio 2015、Python 2.7もあります。これらはすべて必要です。



次に、予備測定を行います。



Node.js



for (var i=0; i<5000000; i++) { var m = crypto.createHash('adler32'); m.update("-    ,      "); m.digest('hex'); }
      
      





3つの開始の平均結果: 41.601秒。 最良の結果:40,206



何かと比較するために、node.jsでのハッシュのネイティブ実装を始めましょう。 sha1と言います。 まったく同じコードを実行しましたが、アルゴリズムとしてsha1を指定すると、次の数値が得られました。

3つの開始の平均結果: 9.737秒。 最良の結果:9,321



たぶん、このアドラーは彼ですか? しかし、ちょっと、ちょっと。 Rustで何かを試してみましょう。



さび



したがって、Rustにはサードパーティの圧縮ライブラリがあり、このCargoで利用できます。 彼女はまた、gzipを知っており、adler32を読み取る機能を提供します。 次のようになります。



 for i in 0..5000_000 { let mut state = adler::State32::new(); state.feed("-    ,      ".as_bytes()) }
      
      





3つの開始の平均結果: 2,314秒。 最高スコア:2,309



悪くない!



Node.jsとFFI



RustはC互換コードにコンパイルされるため、動的ライブラリにコンパイルしてFFIを使用して接続できます。 Node.jsにはこのための特別なパッケージがあり、個別にインストールする必要があります。



 npm install ffi
      
      





すべてが順調であれば、その後、Cで記述された、または互換性のある外部ライブラリを接続できます。



つまり、この破砕機を急速にライブラリーに変換する必要があります。 要するに、コードは次のようになります。

 extern crate compress; extern crate libc; use libc::c_char; use std::ffi::CStr; use std::ffi::CString; use compress::checksum::adler; #[no_mangle] pub extern "C" fn adler(url: *const c_char) -> *mut c_char { let c_str = unsafe { CStr::from_ptr(url).to_bytes() }; let mut state = adler::State32::new(); state.feed(c_str); let s:String = format!("{:x}", state.result()); let s = CString::new(s).unwrap(); s.into_raw() }
      
      





ご覧のとおり、事態はもう少し複雑になりました。 関数は、入力でC文字列を受け取り、それをバイトに転送し、ハッシュを考慮し、16進数に変換し、再びC文字列に追いつき、その後のみ返します。



さらに、Cargo.tomlファイルで、動的ライブラリにコンパイルするものを指定する必要があります。 また、依存関係も示します。



 [package] name = "adler" version = "0.1.0" authors = ["juralis"] [lib] name = "adler" crate-type = ["dylib"] [dependencies] compress = "*" libc = "*"
      
      







こっち これで、ライブラリにコンパイルされます。 どのタイプがターゲットプラットフォームに依存します。 これはすべてWindowsから行い、適切なコンパイルオプションを指定したためです。



 cargo build --release --target x86_64-pc-windows-msvc
      
      





じゃあ この非常にdllを取得し、node.jsのプロジェクトに近い場所に配置して、コードに何かを追加します。



 var ffi = require('ffi'); var lib = ffi.Library('adler.dll', { adler: ['string', ['string']] }) for (var i=0; i<5000000; i++) { lib.adler("-    ,      ") }
      
      





3つの開始の平均結果: 27.882秒。 最良の結果:26,642



まあ...何かがどういうわけか私が望むものではありません。 どうやら、外部の課題とこれらの喜びはすべて非常に高価です。 ただし、それでも高速に動作します。 しかし、それはさらに速くできますか? できます。



Node.jsおよびC ++アドオン



node.jsでは、ご存じのとおり、いわゆるアドオンがサポートされています。 試してみませんか? 私がC ++で一般的に言う唯一の問題は、歯の蹴りではありません。 しかし、少し助けてくれた親切な人がいます。 ここでは、その仕組みについて説明します。 結局のところ、私はこの方法で楽しむことを決めた最初の人ではありません。 ただし、フィボナッチ数の計算にはかなり些細な例があり、したがって、そこには多くの不明な点が残っています。 そして、私はC ++を知らないので、これはもちろん問題でした。



しかし、人類はあらゆる種類の歪みを発明するという点ではるかに進んでおり、親切な人がRustライブラリ用 Cppラッパーの小さなジェネレータを作成したことが判明しました。 Rustのソースコードを分析し、基準に一致する関数を使用して、プロで何らかのコードを生成します。 そして、上記のRust-codeについて、このようなC ++コードを取得しました



 //Header //This could go into separate header file defining interface: #ifndef NATIVE_EXTENSION_GRAB_H #define NATIVE_EXTENSION_GRAB_H #include <nan.h> #include <string> #include <iostream> #include <node.h> #include <stdio.h> using namespace std; using namespace v8; using v8::Function; using v8::Local; using v8::Number; using v8::Value; using Nan::AsyncQueueWorker; using Nan::AsyncWorker; using Nan::Callback; using Nan::New; using Nan::Null; using Nan::To; #endif /* extern interface for Rust functions */ extern "C" { extern "C" char * adler(char * url); } NAN_METHOD(adler) { Nan::HandleScope scope; String::Utf8Value cmd_url(info[0]); string s_url = string(*cmd_url); char *url = (char*) malloc (s_url.length() + 1); strcpy(url, s_url.c_str()); char * result = adler(url); info.GetReturnValue().Set(Nan::New<String>(result).ToLocalChecked()); free(result); free(url); } NAN_MODULE_INIT(InitAll) { Nan::Set( target, New("adler").ToLocalChecked(), Nan::GetFunction(New<FunctionTemplate>(adler)).ToLocalChecked() ); } NODE_MODULE(addon, InitAll)
      
      





さらに、以前の友人からサンプルのbindings.gypファイルを取得しました。



 { "targets": [{ "target_name": "adler", "sources": ["adler.cc" ], "libraries": [ "/path/to/lib/adler.dll" ] }] }
      
      





それでも強迫観念を持つindex.jsファイルが必要です:



 module.exports = require('./build/Release/addon');
      
      





ここで、node-gypを使用してこの喜びをすべて収集する必要があります。 しかし、私はそれを簡単にコンパイルすることを拒否しました。 そこで何が起こっているのか少し理解しなければなりませんでした。



最初に、nanパッケージを配置する必要があります(Node.jsのネイティブアブストラクション):

npm install nan -g



そして、bindings.gypにパスを追加します(ライブラリと同じレベルのどこかに):



 "include_dirs" : [ "<!(node -e \"require('nan')\")" ]
      
      





そこで、コンパイラはこのnan自体からヘッダーファイルを探します。 その後、もう少しポジティブなファイルを掘る必要がありました。 ここに最終バージョンがありますが、私はまだコンパイルすることを考えていました。



 #include <nan.h> #include <string> #include <node.h> #pragma comment(lib,"Ws2_32.lib") #pragma comment(lib,"userenv.lib") using std::string; using v8::String; using Nan::New; extern "C" { extern "C" char * adler(char * url); } NAN_METHOD(adler) { Nan::HandleScope scope; String::Utf8Value cmd_url(info[0]); string s_url = string(*cmd_url); char *url = (char*) malloc (s_url.length() + 1); strcpy(url, s_url.c_str()); char * result = adler(url); info.GetReturnValue().Set(Nan::New<String>(result).ToLocalChecked()); free(result); free(url); } NAN_MODULE_INIT(InitAll) { Nan::Set( target, New("adler").ToLocalChecked(), Nan::GetFunction(New<FunctionTemplate>(adler)).ToLocalChecked() ); } NODE_MODULE(addon, InitAll)
      
      





しかし、これが起こる前に、別のことが発見されました。 私のライブラリは動的としてコンパイルされ、node-gypには静的が必要でした。 したがって、Cargo.tomlでは、次の行を変更する必要があります。



crate-type = ["dylib"]

これについて:

crate-type = ["staticlib"]



それから再びコンパイルする必要があります:



 cargo build --release --target x86_64-pc-windows-msvc
      
      





さらに、bindings.gypのライブラリへのパスをlibバージョンに変更することを忘れないでください。



  "libraries": [ "/path/to/lib/adler.lib" ]
      
      





そして、すべてが一緒になって、切望されているadler.nodeファイルを取得する必要があります。



再びノードで、コードを変更してハッシュを生成します。



 var adler = require('/path/to/adler.node'); for (var i=0; i<5000000; i++) { adler.adler("-    ,      "); }
      
      





3つの開始の平均結果: 7,802秒。 最良の結果:7,658



ああ、それはすでにsha1を計算するネイティブの方法よりも数秒速いです! とても素敵ですね!



原則として、計算し、それに40秒を費やす500万回のハッシュとは何ですか? 1秒で10万件弱のリクエストが届き、アプリケーションはこの1秒をすべてハッシュのカウントに費やします。 つまり、他に何もする時間がありません。 そして、そのような加速により、ハッシュ以外の何かをする時間はすでにあります。 このプロジェクトが毎秒10万件のリクエストを受け取ることはないと思いますが、それでもこの経験は非常に役立つと思います。



ところで、pythonには何がありますか?



記事の冒頭でpythonについて説明しましたが、まだ手元にあるので試してみませんか? そこで、私が言ったように、adler32はすぐにカウントできます。 コードは次のようになります。



 # -*- coding: utf-8 -*- import zlib st = b'-    ,      ' for i in range(5000000): hex(zlib.adler32(st))[2:]
      
      





3つの開始の平均結果: 2,100秒。 最良の結果:2,072



いいえ、これは間違いではなく、コンマはどこでも混同されません。 どうやら、これは標準ライブラリの一部であり、本質的にはGNU zip C-shnomの単なるラッパーであるため、速度に利点があるということです。 言い換えれば、これはPythonとRustの間ではなく、CとRustの間で比較されます。 また、Cは少し高速です。



UPD

PythonにはFFIを使用する機能もあるので、 ynlvkoの要求に応じて、このテーマについて少し追加します。



Pythonの32ビットバージョンがあるため、win32でライブラリを再コンパイルする必要がありました。



 cargo build --release --target i686-pc-windows-msvc
      
      





コード:



 from ctypes import cdll lib = cdll.LoadLibrary("adler32.dll") for i in range(5000000): lib.adler(b'-    ,      ')
      
      



3つの開始の平均結果: 6.398秒。 最高スコア:6.393

つまり、pythonium FFIはnode-ffiよりも数倍、ネイティブアドオンよりもさらに効率的に動作することがわかりました



結論



テクノロジー 平均時間、s とのベストタイム
Node.js 41,601 40,206
Node.js + ffi + Rust 27,882 26,642
Node.js(sha1) 9,737 9,321
Node.js + C ++ Rust 7,802 7,658
Python + ffi + Rust 6,398 6,393
さび 2,314 2,309
C / Python(zlib) 2,100 2,072



All Articles