web-dev-qa-db-fra.com

Pourquoi la fonction webAssembly est-elle presque 300 fois plus lente que la même fonction JS

Trouver la longueur de la ligne 300 * plus lentement

J'ai d'abord lu la réponse à Pourquoi ma fonction WebAssembly est-elle plus lente que l'équivalent JavaScript?

Mais cela a peu éclairé le problème, et j'ai investi beaucoup de temps qui pourrait bien être ce truc jaune contre le mur.

Je n'utilise pas de globaux, je n'utilise pas de mémoire. J'ai deux fonctions simples qui trouvent la longueur d'un segment de ligne et les comparent à la même chose dans le vieux Javascript. J'ai 4 params 3 autres habitants et retourne un flotteur ou double.

Sur Chrome le Javascript est 40 fois plus rapide que le webAssembly et sur firefox le wasm est presque 300 fois plus lent que le Javascript.

cas de test jsPref.

J'ai ajouté un cas de test à jsPref WebAssembly V Javascript math

Qu'est-ce que je fais mal?

Soit

  1. J'ai raté un bug évident, une mauvaise pratique ou je souffre de stupidité de codeur.
  2. WebAssembly n'est pas pour un système d'exploitation 32 bits (gagner 10 ordinateurs portables i7CPU)
  3. WebAssembly est loin d'être une technologie prête.

Veuillez être l'option 1.

J'ai lu le cas d'utilisation de webAssembly

Réutilisez le code existant en ciblant WebAssembly, intégré dans une application JavaScript/HTML plus grande. Cela peut aller de simples bibliothèques d'aide au déchargement de tâches orienté calcul.

J'espérais pouvoir remplacer certaines bibliothèques de géométrie par webAssembly pour obtenir des performances supplémentaires. J'espérais que ce serait génial, comme 10 fois ou plus plus vite. MAIS 300 fois plus lent WTF.


UPADTE

Ce n'est pas un problème d'optimisation JS.

Pour m'assurer que l'optimisation a le moins d'effet possible, j'ai testé en utilisant les méthodes suivantes pour réduire ou éliminer tout biais d'optimisation.

  • compteur c += length(... pour s'assurer que tout le code est exécuté.
  • bigCount += c pour garantir que la fonction entière est exécutée. Pas besoin
  • 4 lignes pour chaque fonction afin de réduire un biais en ligne. Pas besoin
  • toutes les valeurs sont des doubles générés aléatoirement
  • chaque appel de fonction renvoie un résultat différent.
  • ajouter un calcul de longueur plus lent dans JS en utilisant Math.hypot pour prouver que le code est en cours d'exécution.
  • ajout d'un appel vide qui renvoie le premier paramètre JS pour voir les frais généraux
// 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",
    }
],

Résultats de la mise à jour.

Parce qu'il y a beaucoup plus de frais généraux dans le test, les résultats sont plus proches, mais le code JS est toujours plus rapide de deux ordres de grandeur.

Notez la lenteur de la fonction Math.hypot est. Si l'optimisation était en vigueur, cette fonction serait proche de la fonction len plus rapide.

  • 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.

*/

Quel est l'intérêt de WebAssambly s'il n'optimise pas

Fin de la mise à jour


Tous les trucs liés au problème.

Trouvez la longueur d'une ligne.

Source originale dans un langage personnalisé

   
// 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;
}

Le code se compile en Wat pour la relecture

(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
)
)

Comme wasm compilé dans une chaîne hexadécimale (la note n'inclut pas la section de nom) et chargé à l'aide de WebAssembly.compile Les fonctions exportées s'exécutent ensuite avec la fonction Javascript len ​​(dans l'extrait ci-dessous)

    // 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);
    }

Et le code de test est le même pour les 3 fonctions et s'exécute en mode strict.

 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",
    }
]

Les résultats des tests sur FireFox sont

 /*
=======================================
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

Andreas décrit un certain nombre de bonnes raisons pour lesquelles l'implémentation JavaScript a été initialement observée être 300 fois plus rapide . Cependant, il existe un certain nombre d'autres problèmes avec votre code.

  1. Il s'agit d'un `` micro-benchmark '' classique, c'est-à-dire que le code que vous testez est si petit que les autres frais généraux de votre boucle de test sont un facteur important. Par exemple, il existe un surcoût lors de l'appel de WebAssembly à partir de JavaScript, qui prendra en compte vos résultats. Qu'essayez-vous de mesurer? vitesse de traitement brute? ou les frais généraux de la frontière linguistique?
  2. Vos résultats varient énormément, de x300 à x2, en raison de petits changements dans votre code de test. Encore une fois, il s'agit d'un problème de micro-référence. D'autres ont vu la même chose en utilisant cette approche pour mesurer les performances, par exemple ce post prétend que le wasm est x84 plus rapide , ce qui est clairement faux!
  3. Le WebAssembly actuel VM est très nouveau, et un MVP. Il deviendra plus rapide. Votre JavaScript VM a eu 20 ans pour atteindre sa vitesse actuelle. Les performances de la frontière du wasm JS <=> est en cours et optimisé en ce moment .

Pour une réponse plus définitive, consultez le document conjoint de l'équipe WebAssembly, qui décrit un gain de performances d'exécution attendu d'environ 30%

Enfin, pour répondre à votre argument:

Quel est l'intérêt de WebAssembly s'il n'optimise pas

Je pense que vous avez des idées fausses sur ce que WebAssembly fera pour vous. Sur la base du document ci-dessus, les optimisations des performances d'exécution sont assez modestes. Cependant, il existe encore un certain nombre d'avantages en termes de performances:

  1. Son format binaire compact signifie et sa nature de bas niveau signifie que le navigateur peut charger, analyser et compiler le code beaucoup plus rapidement que JavaScript. Il est prévu que WebAssembly peut être compilé plus rapidement que votre navigateur ne peut le télécharger.
  2. WebAssembly a des performances d'exécution prévisibles. Avec JavaScript, les performances augmentent généralement à chaque itération car elles sont encore optimisées. Il peut également diminuer en raison de la se-optimisation.

Il existe également un certain nombre d'avantages non liés aux performances.

Pour une mesure des performances plus réaliste, consultez:

Les deux sont des bases de code de production pratiques.

7
ColinE

Le moteur JS peut appliquer de nombreuses optimisations dynamiques à cet exemple:

  1. Effectuez tous les calculs avec des nombres entiers et convertissez uniquement en double pour le dernier appel à Math.sqrt.

  2. Inline l'appel à la fonction len.

  3. Hissez le calcul hors de la boucle, car il calcule toujours la même chose.

  4. Reconnaissez que la boucle est laissée vide et éliminez-la entièrement.

  5. Reconnaissez que le résultat n'est jamais renvoyé par la fonction de test et supprimez donc tout le corps de la fonction de test.

Tous sauf (4) s'appliquent même si vous ajoutez le résultat de chaque appel. Avec (5) le résultat final est une fonction vide de toute façon.

Avec Wasm, un moteur ne peut pas effectuer la plupart de ces étapes, car il ne peut pas s'aligner au-delà des frontières linguistiques (du moins aucun moteur ne le fait aujourd'hui, AFAICT). De plus, pour Wasm, on suppose que le compilateur producteur (hors ligne) a déjà effectué des optimisations pertinentes, donc un Wasm JIT a tendance à être moins agressif que celui pour JavaScript, où l'optimisation statique est impossible.

4
Andreas Rossberg