web-dev-qa-db-ja.com

webAssembly機能が同じJS機能よりも約300倍遅い理由

行の長さを見つける300 *遅い

最初に WebAssemblyの機能がJavaScriptの同等のものよりも遅いのはなぜですか?

しかし、それは問題にほとんど光を当てていないので、私は多くの時間を投資しました。

グローバルは使用せず、メモリも使用しません。線分セグメントの長さを見つけて、それらを単純な古いJavascriptの同じものと比較する2つの単純な関数があります。 4つのパラメーターと3つのローカル変数があり、floatまたはdoubleを返します。

On Chrome= JavaScriptはwebAssemblyより40倍速く、firefoxではwasmはほぼ300倍遅いJavascript。

jsPrefテストケース。

JsPrefにテストケースを追加しました WebAssembly V Javascript math

私は何を間違えていますか?

どちらか

  1. 明らかなバグを見逃したか、悪い習慣をしたか、コーダーの愚かさに苦しんでいます。
  2. WebAssemblyは32ビットOS用ではありません(10ラップトップi7CPUに勝つ)
  3. WebAssemblyは、すぐに使えるテクノロジーとはほど遠いものです。

オプション1にしてください。

webAssemblyのユースケース を読みました

より大きなJavaScript/HTMLアプリケーションに埋め込まれたWebAssemblyをターゲットにすることにより、既存のコードを再利用します。これは、単純なヘルパーライブラリから、計算指向のタスクオフロードまで何でもかまいません。

いくつかのジオメトリライブラリをwebAssemblyに置き換えて、パフォーマンスをさらに高めたいと思っていました。私は、それが10倍以上速くなるように、素晴らしいものになることを望んでいました。しかし、WTFは300倍遅くなります。


UPADTE

これは、JS最適化の問題ではありません。

最適化による影響ができるだけ少ないことを確認するために、次の方法を使用して、最適化バイアスを削減または排除するためのテストを実施しました。

  • counter c += length(...を使用して、すべてのコードが実行されるようにします。
  • bigCount += cは、関数全体が確実に実行されるようにします。必要ありません
  • インラインスキューを減らすために、各関数に4行。必要ありません
  • すべての値はランダムに生成されたdoubleです
  • 各関数呼び出しは異なる結果を返します。
  • コードが実行されていることを証明するために、Math.hypotを使用してJSでより遅い長さの計算を追加します。
  • 最初のパラメーターJSを返す空の呼び出しを追加して、オーバーヘッドを確認しました
// setup and associated functions
    const setOf = (count, callback) => {var a = [],i = 0; while (i < count) { a.Push(callback(i ++)) } return a };
    const Rand  = (min = 1, max = min + (min = 0)) => Math.random() * (max - min) + min;
    const a = setOf(100009,i=>Rand(-100000,100000));
    var bigCount = 0;




    function len(x,y,x1,y1){
        var nx = x1 - x;
        var ny = y1 - y;
        return Math.sqrt(nx * nx + ny * ny);
    }
    function lenSlow(x,y,x1,y1){
        var nx = x1 - x;
        var ny = y1 - y;
        return Math.hypot(nx,ny);
    }
    function lenEmpty(x,y,x1,y1){
        return x;
    }


// Test functions in same scope as above. None is in global scope
// Each function is copied 4 time and tests are performed randomly.
// c += length(...  to ensure all code is executed. 
// bigCount += c to ensure whole function is executed.
// 4 lines for each function to reduce a inlining skew
// all values are randomly generated doubles 
// each function call returns a different result.

tests : [{
        func : function (){
            var i,c=0,a1,a2,a3,a4;
            for (i = 0; i < 10000; i += 1) {
                a1 = a[i];
                a2 = a[i+1];
                a3 = a[i+2];
                a4 = a[i+3];
                c += length(a1,a2,a3,a4);
                c += length(a2,a3,a4,a1);
                c += length(a3,a4,a1,a2);
                c += length(a4,a1,a2,a3);
            }
            bigCount = (bigCount + c) % 1000;
        },
        name : "length64",
    },{
        func : function (){
            var i,c=0,a1,a2,a3,a4;
            for (i = 0; i < 10000; i += 1) {
                a1 = a[i];
                a2 = a[i+1];
                a3 = a[i+2];
                a4 = a[i+3];
                c += lengthF(a1,a2,a3,a4);
                c += lengthF(a2,a3,a4,a1);
                c += lengthF(a3,a4,a1,a2);
                c += lengthF(a4,a1,a2,a3);
            }
            bigCount = (bigCount + c) % 1000;
        },
        name : "length32",
    },{
        func : function (){
            var i,c=0,a1,a2,a3,a4;
            for (i = 0; i < 10000; i += 1) {
                a1 = a[i];
                a2 = a[i+1];
                a3 = a[i+2];
                a4 = a[i+3];                    
                c += len(a1,a2,a3,a4);
                c += len(a2,a3,a4,a1);
                c += len(a3,a4,a1,a2);
                c += len(a4,a1,a2,a3);
            }
            bigCount = (bigCount + c) % 1000;
        },
        name : "length JS",
    },{
        func : function (){
            var i,c=0,a1,a2,a3,a4;
            for (i = 0; i < 10000; i += 1) {
                a1 = a[i];
                a2 = a[i+1];
                a3 = a[i+2];
                a4 = a[i+3];                    
                c += lenSlow(a1,a2,a3,a4);
                c += lenSlow(a2,a3,a4,a1);
                c += lenSlow(a3,a4,a1,a2);
                c += lenSlow(a4,a1,a2,a3);
            }
            bigCount = (bigCount + c) % 1000;
        },
        name : "Length JS Slow",
    },{
        func : function (){
            var i,c=0,a1,a2,a3,a4;
            for (i = 0; i < 10000; i += 1) {
                a1 = a[i];
                a2 = a[i+1];
                a3 = a[i+2];
                a4 = a[i+3];                    
                c += lenEmpty(a1,a2,a3,a4);
                c += lenEmpty(a2,a3,a4,a1);
                c += lenEmpty(a3,a4,a1,a2);
                c += lenEmpty(a4,a1,a2,a3);
            }
            bigCount = (bigCount + c) % 1000;
        },
        name : "Empty",
    }
],

更新の結果。

テストには多くのオーバーヘッドがあるため、結果はより近くなりますが、JSコードは2桁高速です。

関数Math.hypotがどれほど遅いかに注意してください。最適化が有効な場合、その関数はより高速なlen関数に近くなります。

  • WebAssembly 13389µs
  • Javascript 728µs
/*
=======================================
Performance test. : WebAssm V Javascript
Use strict....... : true
Data view........ : false
Duplicates....... : 4
Cycles........... : 147
Samples per cycle : 100
Tests per Sample. : undefined
---------------------------------------------
Test : 'length64'
Mean : 12736µs ±69µs (*) 3013 samples
---------------------------------------------
Test : 'length32'
Mean : 13389µs ±94µs (*) 2914 samples
---------------------------------------------
Test : 'length JS'
Mean : 728µs ±6µs (*) 2906 samples
---------------------------------------------
Test : 'Length JS Slow'
Mean : 23374µs ±191µs (*) 2939 samples   << This function use Math.hypot 
                                            rather than Math.sqrt
---------------------------------------------
Test : 'Empty'
Mean : 79µs ±2µs (*) 2928 samples
-All ----------------------------------------
Mean : 10.097ms Totals time : 148431.200ms 14700 samples
(*) Error rate approximation does not represent the variance.

*/

最適化しない場合のWebAssamblyのポイント

更新の終了


問題に関連するすべてのもの。

行の長さを見つけます。

カスタム言語の元のソース

   
// declare func the < indicates export name, the param with types and return type
func <lengthF(float x, float y, float x1, float y1) float {
    float nx, ny, dist;  // declare locals float is f32
    nx = x1 - x;
    ny = y1 - y;
    dist = sqrt(ny * ny + nx * nx);
    return dist;
}
// and as double
func <length(double x, double y, double x1, double y1) double {
    double nx, ny, dist;
    nx = x1 - x;
    ny = y1 - y;
    dist = sqrt(ny * ny + nx * nx);
    return dist;
}

コードをワットにコンパイルして校正読み取り

(module
(func 
    (export "lengthF")
    (param f32 f32 f32 f32)
    (result f32)
    (local f32 f32 f32)
    get_local 2
    get_local 0
    f32.sub
    set_local 4
    get_local 3
    get_local 1
    f32.sub
    tee_local 5
    get_local 5
    f32.mul
    get_local 4
    get_local 4
    f32.mul
    f32.add
    f32.sqrt
)
(func 
    (export "length")
    (param f64 f64 f64 f64)
    (result f64)
    (local f64 f64 f64)
    get_local 2
    get_local 0
    f64.sub
    set_local 4
    get_local 3
    get_local 1
    f64.sub
    tee_local 5
    get_local 5
    f64.mul
    get_local 4
    get_local 4
    f64.mul
    f64.add
    f64.sqrt
)
)

16進数文字列でコンパイルされたwasm(注には名前セクションは含まれません)として、WebAssembly.compileを使用してロードされます。エクスポートされた関数は、Javascript関数lenに対して実行されます(以下のスニペット)

    // hex of above without the name section
    const asm = `0061736d0100000001110260047d7d7d7d017d60047c7c7c7c017c0303020001071402076c656e677468460000066c656e67746800010a3b021c01037d2002200093210420032001932205200594200420049492910b1c01037c20022000a1210420032001a122052005a220042004a2a09f0b`
    const bin = new Uint8Array(asm.length >> 1);
    for(var i = 0; i < asm.length; i+= 2){ bin[i>>1] = parseInt(asm.substr(i,2),16) }
    var length,lengthF;

    WebAssembly.compile(bin).then(module => {
        const wasmInstance = new WebAssembly.Instance(module, {});
        lengthF = wasmInstance.exports.lengthF;
        length = wasmInstance.exports.length;
    });
    // test values are const (same result if from array or literals)
    const a1 = Rand(-100000,100000);
    const a2 = Rand(-100000,100000);
    const a3 = Rand(-100000,100000);
    const a4 = Rand(-100000,100000);

    // javascript version of function
    function len(x,y,x1,y1){
        var nx = x1 - x;
        var ny = y1 - y;
        return Math.sqrt(nx * nx + ny * ny);
    }

また、テストコードは3つの関数すべてで同じであり、厳格モードで実行されます。

 tests : [{
        func : function (){
            var i;
            for (i = 0; i < 100000; i += 1) {
               length(a1,a2,a3,a4);

            }
        },
        name : "length64",
    },{
        func : function (){
            var i;
            for (i = 0; i < 100000; i += 1) {
                lengthF(a1,a2,a3,a4);
             
            }
        },
        name : "length32",
    },{
        func : function (){
            var i;
            for (i = 0; i < 100000; i += 1) {
                len(a1,a2,a3,a4);
             
            }
        },
        name : "lengthNative",
    }
]

FireFoxのテスト結果は次のとおりです。

 /*
=======================================
Performance test. : WebAssm V Javascript
Use strict....... : true
Data view........ : false
Duplicates....... : 4
Cycles........... : 34
Samples per cycle : 100
Tests per Sample. : undefined
---------------------------------------------
Test : 'length64'
Mean : 26359µs ±128µs (*) 1128 samples
---------------------------------------------
Test : 'length32'
Mean : 27456µs ±109µs (*) 1144 samples
---------------------------------------------
Test : 'lengthNative'
Mean : 106µs ±2µs (*) 1128 samples
-All ----------------------------------------
Mean : 18.018ms Totals time : 61262.240ms 3400 samples
(*) Error rate approximation does not represent the variance.
*/
16
Blindman67

アンドレアスは、JavaScriptの実装が 最初はx300で高速であると観察された であったいくつかの正当な理由を説明しています。ただし、コードには他にも多くの問題があります。

  1. これは古典的な「マイクロベンチマーク」です。つまり、テストするコードが非常に小さいため、テストループ内の他のオーバーヘッドが重要な要素になります。たとえば、JavaScriptからWebAssemblyを呼び出すとオーバーヘッドが発生し、結果が考慮されます。何を測定しようとしていますか?生の処理速度?または言語境界のオーバーヘッド?
  2. テストコードのわずかな変更により、結果はx300からx2まで大きく異なります。繰り返しますが、これはマイクロベンチマークの問題です。他の人は、このアプローチを使用してパフォーマンスを測定するときに同じことを見てきました。たとえば、 この投稿はwasmがx84より高速であると主張しています 、これは明らかに間違っています!
  3. 現在のWebAssembly VMは非常に新しく、MVPです。高速になります。JavaScriptVMは現在の速度に達するまでに20年かかりました。パフォーマンスJS <=> wasm境界の 現在作業中および最適化済み です。

より明確な回答については、WebAssemblyチームの共同論文を参照してください。これには、予想される概要が記載されています 実行時のパフォーマンスの約30%の向上

最後に、あなたの主張に答えるために:

最適化しない場合のWebAssemblyのポイント

WebAssemblyがあなたに何をするかについて誤解があると思います。上記の論文に基づくと、実行時のパフォーマンスの最適化はかなり控えめです。ただし、パフォーマンスにはまだ多くの利点があります。

  1. そのコンパクトなバイナリ形式は、低レベルの性質を意味するため、ブラウザはJavaScriptよりもはるかに高速にコードをロード、解析、コンパイルできます。 WebAssemblyは、ブラウザがダウンロードするよりも速くコンパイルできると予想されます。
  2. WebAssemblyには、予測可能な実行時パフォーマンスがあります。 JavaScriptを使用すると、一般的にパフォーマンスがさらに最適化されるため、反復ごとにパフォーマンスが向上します。 se-optimisationにより減少することもあります。

また、パフォーマンスに関連しない多くの利点もあります。

より現実的なパフォーマンス測定については、以下をご覧ください。

どちらも実用的な量産コードベースです。

7
ColinE

JSエンジンは、この例に多くの動的最適化を適用できます。

  1. 整数を使用してすべての計算を実行し、Math.sqrtの最後の呼び出しでのみdoubleに変換します。

  2. len関数の呼び出しをインライン化します。

  3. 常に同じことを計算するため、ループから計算を引き上げます。

  4. ループが空のままであることを認識し、完全に削除します。

  5. テスト関数から結果が返されないことを認識し、テスト関数の本体全体を削除します。

すべての呼び出しの結果を追加しても、(4)以外はすべて適用されます。 (5)では、いずれの場合も最終結果は空の関数になります。

Wasmを使用すると、エンジンは言語の境界を越えてインライン化できないため、これらの手順のほとんどを実行できません(少なくとも、今日はエンジンがそれを行いません、AFAICT)。また、Wasmでは、プロデューサー(オフライン)コンパイラーが関連する最適化をすでに実行していると想定されているため、Wasm JITは、静的最適化が不可能なJavaScriptのJITよりも攻撃的ではない傾向があります。

4