La rapidité est constante,
Lors de ma précédente articles J'ai parlé de l'utilisation de WebAssembly vous permet d'intégrer l'écosystème de bibliothèques en C/C++ sur le Web. Une application qui fait un usage intensif des bibliothèques C/C++ est squoosh, notre qui vous permet de compresser des images à l'aide de divers codecs compilé de C++ à WebAssembly.
WebAssembly est une machine virtuelle de bas niveau qui exécute le bytecode stocké
dans les fichiers .wasm
. Ce byte code est fortement typé et structuré de telle sorte
qu'il peut être compilé et optimisé pour le système hôte bien plus rapidement que
JavaScript peut le faire. WebAssembly fournit un environnement permettant d'exécuter du code ayant
le système de bac à sable et
l'intégration de données dès le départ.
D'après mon expérience, la plupart des problèmes de performances sur le Web sont dus à des contraintes de mise en page et de peinture excessive, mais de temps en temps, une application doit une tâche coûteuse en calcul qui prend beaucoup de temps. WebAssembly peut vous aider ici.
Le chemin du chaud
Dans squoosh, nous avons écrit une fonction JavaScript qui fait pivoter un tampon d'image par multiples de 90 degrés. Alors que OffscreenCanvas convient parfaitement elle n'est pas prise en charge par les navigateurs que nous ciblions. bugs dans Chrome.
Cette fonction effectue une itération sur chaque pixel d'une image d'entrée et la copie une autre position dans l'image de sortie pour obtenir une rotation. Pour une taille de 4 094 pixels par une image de 4 096 pixels (16 mégapixels). Il faudrait plus de 16 millions d'itérations ce que nous appelons un "chemin réactif". Malgré le fait que nombre d'itérations, deux navigateurs sur trois que nous avons testés terminent la tâche en deux secondes ou moins. Durée acceptable pour ce type d'interaction.
for (let d2 = d2Start; d2 >= 0 && d2 < d2Limit; d2 += d2Advance) {
for (let d1 = d1Start; d1 >= 0 && d1 < d1Limit; d1 += d1Advance) {
const in_idx = ((d1 * d1Multiplier) + (d2 * d2Multiplier));
outBuffer[i] = inBuffer[in_idx];
i += 1;
}
}
Mais avec un navigateur, le processus prend plus de 8 secondes. Comment les navigateurs optimisent JavaScript est très compliqué, et les différents moteurs n'optimisent pas les campagnes pour différentes choses. Certaines optimisent l'exécution brute, d'autres l'interaction avec le DOM. Dans dans ce cas, nous avons trouvé un chemin non optimisé dans un navigateur.
En revanche, WebAssembly repose entièrement sur la vitesse d'exécution brute. Donc Pour obtenir des performances rapides et prévisibles sur tous les navigateurs pour un code de ce type, WebAssembly peut vous aider.
WebAssembly pour des performances prévisibles
En général, JavaScript et WebAssembly peuvent obtenir les mêmes performances maximales. Toutefois, pour JavaScript, ces performances ne peuvent être atteintes que via la méthode "Fast path", et il est souvent difficile de rester sur cette voie rapide. L'un des principaux avantages WebAssembly offre des performances prévisibles, même sur tous les navigateurs. Les règles strictes la saisie et l'architecture de bas niveau permettent au compilateur de renforcer garantit que le code WebAssembly ne doit être optimisé qu'une seule fois utilisez toujours le "chemin rapide".
Écrire pour WebAssembly
Auparavant, nous avons pris les bibliothèques C/C++ et les avons compilées sur WebAssembly pour utiliser leur sur le Web. Nous n'avons pas vraiment touché au code des bibliothèques, a écrit de petites quantités de code C/C++ pour former le pont entre le navigateur et la bibliothèque. Cette fois, notre motivation est différente: nous voulons écrire en pensant à WebAssembly. Nous pouvons ainsi utiliser et les avantages de WebAssembly.
Architecture WebAssembly
Lorsque vous écrivez pour WebAssembly, il est utile d'en savoir un peu plus ce qu'est réellement WebAssembly.
Pour citer WebAssembly.org:
Lorsque vous compilez un extrait de code C ou Rust dans WebAssembly, vous obtenez un .wasm
contenant une déclaration de module. Cette déclaration consiste en une liste
"importations" que le module attend de son environnement, une liste des exportations qu'il
met à la disposition de l'hôte (fonctions, constantes, morceaux de mémoire) et
bien sûr les instructions binaires réelles
pour les fonctions qu'elle contient.
Quelque chose que je n’avais pas réalisé avant de m’y pencher: la pile qui fait WebAssembly une "machine virtuelle basée sur des piles" n'est pas stockée dans le fragment de mémoire utilisée par les modules WebAssembly. La pile est entièrement interne à la VM les développeurs Web ne peuvent pas y accéder (sauf via les outils de développement). Il est donc possible pour écrire des modules WebAssembly qui n'ont pas besoin de mémoire supplémentaire n'utilisez que la pile interne de la VM.
Dans notre cas, nous devrons utiliser de la mémoire supplémentaire pour permettre un accès arbitraire
aux pixels de notre image et générer
une version pivotée de cette image. C'est
à quoi sert WebAssembly.Memory
.
Gestion de la mémoire
Généralement, une fois que vous utilisez de la mémoire supplémentaire,
gérer cette mémoire. Quelles parties de la mémoire sont utilisées ? Lesquelles sont sans frais ?
En C, par exemple, la fonction malloc(n)
trouve un espace mémoire
sur n
octets consécutifs. Les fonctions de ce type sont également appelées "allocateurs".
Bien entendu, l'implémentation de l'outil d'attribution utilisé doit être incluse dans votre
WebAssembly et augmentera la taille de votre fichier. Cette taille et ces performances
fonctions de gestion de la mémoire peuvent
varier considérablement selon
l'algorithme utilisé. C'est pourquoi de nombreux langages offrent plusieurs implémentations
à choisir ("dmalloc", "emmalloc", "wee_alloc", etc.).
Dans notre cas, nous connaissons les dimensions de l'image d'entrée (et donc de l'image de sortie) avant d'exécuter le module WebAssembly. Ici, nous Habituellement, nous transmettons le tampon RVBA de l'image d'entrée à une fonction WebAssembly et renvoyer l'image ayant fait l'objet d'une rotation en retour . Pour générer cette valeur renvoyée, nous devrions utiliser l'outil d'allocation. Mais comme nous connaissons la quantité totale de mémoire nécessaire (deux fois la taille de l'entrée une fois pour l'entrée et une fois pour la sortie), nous pouvons placer l'image d'entrée dans de la mémoire WebAssembly à l'aide de JavaScript, exécutez le module WebAssembly pour générer une Deuxièmement, l'image a fait l'objet d'une rotation, puis relit le résultat à l'aide de JavaScript. Nous pouvons obtenir sans utiliser de gestion de la mémoire.
C'est l'embarras du choix
Si vous avez observé la fonction JavaScript d'origine, que nous voulons que WebAssembly-fy soit, vous voyez qu'il s'agit sans API spécifique à JavaScript. Par conséquent, il doit être assez droit avant de transférer ce code vers n'importe quel langage. Nous avons évalué trois langues différentes qui s'compilent sur WebAssembly: C/C++, Rust et AssemblyScript. La seule question Pour chacune des langues, nous devons répondre à la question suivante: comment accéder à la mémoire brute sans utiliser de fonctions de gestion de la mémoire ?
C et Emscripten
Emscripten est un compilateur C pour la cible WebAssembly. L'objectif d'Emmscripten est de qui remplace directement les compilateurs C connus tels que GCC ou clang. et est pour la plupart compatible avec les indicateurs. C'est un élément essentiel de la mission d'Emmscripten. car il souhaite rendre la compilation de code C et C++ existant dans WebAssembly aussi simple que possible.
L'accès à la mémoire brute est la nature même du C et des pointeurs existent pour cette motif:
uint8_t* ptr = (uint8_t*)0x124;
ptr[0] = 0xFF;
Ici, nous transformons le nombre 0x124
en pointeur vers un format 8 bits non signé.
entiers (ou octets). Cela transforme efficacement la variable ptr
en un tableau
à partir de l'adresse mémoire 0x124
, que nous pouvons utiliser comme n'importe quel autre tableau,
ce qui nous permet d'accéder à des octets
individuels pour la lecture et l'écriture. Dans notre cas, nous
Nous consultons le tampon RVBA d'une image que nous souhaitons réorganiser
la rotation des clés. Pour déplacer un pixel, nous devons déplacer quatre octets consécutifs en même temps.
(un octet pour chaque canal: R, V, B et A). Pour vous faciliter la tâche, nous pouvons
tableau d'entiers 32 bits non signés. Par convention, l'image d'entrée commence
à l'adresse 4. L'image de sortie commencera immédiatement après l'image d'entrée.
date de fin:
int bpp = 4;
int imageSize = inputWidth * inputHeight * bpp;
uint32_t* inBuffer = (uint32_t*) 4;
uint32_t* outBuffer = (uint32_t*) (inBuffer + imageSize);
for (int d2 = d2Start; d2 >= 0 && d2 < d2Limit; d2 += d2Advance) {
for (int d1 = d1Start; d1 >= 0 && d1 < d1Limit; d1 += d1Advance) {
int in_idx = ((d1 * d1Multiplier) + (d2 * d2Multiplier));
outBuffer[i] = inBuffer[in_idx];
i += 1;
}
}
Après avoir porté l'intégralité de la fonction JavaScript vers C, nous pouvons compiler le fichier C.
avec emcc
:
$ emcc -O3 -s ALLOW_MEMORY_GROWTH=1 -o c.js rotate.c
Comme toujours, emscripten génère un fichier de code glue nommé c.js
et un module Wasm
appelée c.wasm
. Notez que le module Wasm est compressé avec seulement 260 octets environ, alors que le
le code Glue représente environ 3,5 Ko après gzip. Après quelques manipulations, nous avons pu abandonner
le code Glue et instanciez les modules WebAssembly avec les API vanilla.
C'est souvent possible avec Emscripten, tant que vous n'utilisez
de la bibliothèque standard C.
Rust
Rust est un nouveau langage de programmation moderne doté d'un système de types enrichi, sans environnement d'exécution et un modèle de propriété qui garantit la sécurité de la mémoire et des threads. Rouille prend également en charge WebAssembly en tant que fonctionnalité essentielle, et l'équipe Rust a a contribué à améliorer l'écosystème WebAssembly grâce à ses excellents outils.
L'un de ces outils est wasm-pack
, par
le groupe de travail rustwasm. wasm-pack
qui transforme votre code en un module Web
et prêts à l'emploi avec des bundles tels que webpack. wasm-pack
est un
mais ne fonctionne actuellement que pour Rust. Le groupe est
envisagez d'ajouter la prise en charge d'autres langues
de ciblage WebAssembly.
Dans Rust, les tranches correspondent à ce que contiennent les tableaux en C. Et tout comme en C, nous devons créer
tranches qui utilisent nos
adresses de départ. Cela va à l'encontre du modèle de sécurité de la mémoire.
que Rust applique. Pour comprendre comment utiliser le mot clé unsafe
,
ce qui nous permet d'écrire du code
qui n'est pas conforme à ce modèle.
let imageSize = (inputWidth * inputHeight) as usize;
let inBuffer: &mut [u32];
let outBuffer: &mut [u32];
unsafe {
inBuffer = slice::from_raw_parts_mut::<u32>(4 as *mut u32, imageSize);
outBuffer = slice::from_raw_parts_mut::<u32>((imageSize * 4 + 4) as *mut u32, imageSize);
}
for d2 in 0..d2Limit {
for d1 in 0..d1Limit {
let in_idx = (d1Start + d1 * d1Advance) * d1Multiplier + (d2Start + d2 * d2Advance) * d2Multiplier;
outBuffer[i as usize] = inBuffer[in_idx as usize];
i += 1;
}
}
La compilation des fichiers Rust à l'aide de
$ wasm-pack build
on obtient un module Wasm de 7,6 Ko contenant environ 100 octets de code glue (les deux après gzip).
AssemblyScript
AssemblyScript est un script jeune projet qui vise à être un compilateur TypeScript vers WebAssembly. Il est il est important de noter, cependant, qu'il ne consommera pas seulement n'importe quel TypeScript. AssemblyScript utilise la même syntaxe que TypeScript, mais remplace la version standard pour leur propre bibliothèque. Sa bibliothèque standard modélise les capacités WebAssembly. Cela signifie que vous ne pouvez pas simplement compiler n'importe quel TypeScript à WebAssembly, mais cela signifie que vous n'avez pas besoin d'apprendre une nouvelle pour écrire WebAssembly.
for (let d2 = d2Start; d2 >= 0 && d2 < d2Limit; d2 += d2Advance) {
for (let d1 = d1Start; d1 >= 0 && d1 < d1Limit; d1 += d1Advance) {
let in_idx = ((d1 * d1Multiplier) + (d2 * d2Multiplier));
store<u32>(offset + i * 4 + 4, load<u32>(in_idx * 4 + 4));
i += 1;
}
}
Compte tenu de la petite surface de type de notre fonction rotate()
, il s'agit
assez facile de porter ce code
vers AssemblyScript. Les fonctions load<T>(ptr:
usize)
et store<T>(ptr: usize, value: T)
sont fournies par AssemblyScript pour
pour accéder à la mémoire brute. Pour compiler notre fichier AssemblyScript, procédez comme suit :
il suffit d'installer le package npm AssemblyScript/assemblyscript
et d'exécuter
$ asc rotate.ts -b assemblyscript.wasm --validate -O3
AssemblyScript nous fournira un module Wasm d'environ 300 octets sans code glue. Le module ne fonctionne qu'avec les API WebAssembly vanilla.
Analyses forensiques WebAssembly
La taille de 7,6 Ko de Rust est étonnamment importante par rapport aux deux autres langues. Il y deux outils de l'écosystème WebAssembly peuvent vous aider à analyser vos fichiers WebAssembly (quelle que soit la langue dans laquelle ils ont été créés) et vous informer de ce qui se passe et vous aider à améliorer votre situation.
Twiggy
Twiggy est un autre outil du
L'équipe WebAssembly extrait de nombreuses données utiles d'une instance WebAssembly
de ce module. L'outil n'est pas spécifique à Rust et vous permet d'inspecter des éléments tels que le
graphique des appels du module, déterminer les sections inutilisées ou superflues
les sections qui contribuent à la taille totale du fichier de votre module. La
la deuxième méthode peut être effectuée à l'aide de la commande top
de Twiggy:
$ twiggy top rotate_bg.wasm
Dans ce cas, nous pouvons voir que la majorité de la taille de notre fichier provient du d'un outil d'allocation. C'était surprenant, car notre code n'utilise pas d'allocations dynamiques. Un autre facteur important est les « noms des fonctions » dans cette sous-section.
bande de Wasm
wasm-strip
est un outil du kit binaire WebAssembly, ou wabt. Il contient
deux outils qui vous permettent d'inspecter et de manipuler des modules WebAssembly.
wasm2wat
est un désassembleur qui transforme un module Wasm binaire en
dans un format lisible par l'humain. Wabt contient également wat2wasm
, qui vous permet d'activer ou de désactiver
ce format lisible par l'humain en
module Wasm binaire. Bien que nous ayons utilisé
ces deux outils complémentaires pour inspecter nos fichiers WebAssembly,
wasm-strip
pour qu'il soit le plus utile possible. wasm-strip
supprime les sections inutiles
et les métadonnées d'un module WebAssembly:
$ wasm-strip rotate_bg.wasm
Cela permet de réduire la taille du fichier du module rust de 7,5 Ko à 6,6 Ko (après gzip).
wasm-opt
wasm-opt
est un outil de Binaryen.
Il prend un module WebAssembly et tente de l'optimiser à la fois en termes de taille
des performances basées uniquement
sur le bytecode. Certains outils comme Emscripten fonctionnent déjà
cet outil, d'autres non. C'est généralement
une bonne idée d'essayer d'économiser
d'octets supplémentaires à l'aide de ces outils.
wasm-opt -O3 -o rotate_bg_opt.wasm rotate_bg.wasm
Avec wasm-opt
, nous pouvons gagner une autre poignée d'octets et obtenir un total de
6,2 Ko après gzip.
#![no_std]
Après consultation et recherche, nous avons réécrit notre code Rust sans utiliser
bibliothèque standard de Rust, en utilisant le
#![no_std]
. Cette opération désactive aussi complètement les allocations de mémoire dynamique, ce qui supprime
du code d'allocation de notre module. Compiler ce fichier Rust
par
$ rustc --target=wasm32-unknown-unknown -C opt-level=3 -o rust.wasm rotate.rs
a généré un module Wasm de 1,6 Ko après wasm-opt
, wasm-strip
et gzip. Bien qu'il soit
plus volumineux que les modules générés par C et AssemblyScript,
pour être considéré
comme un poids léger.
Performances
Avant de tirer des conclusions hâtives basées uniquement sur la taille du fichier, nous sommes partis de cette aventure. pour optimiser les performances, et non la taille du fichier. Comment avons-nous mesuré les performances quels ont été les résultats ?
Effectuer une analyse comparative
Bien que WebAssembly soit un format de bytecode de bas niveau, il doit tout de même être envoyé via un compilateur pour générer du code machine spécifique à l'hôte. Tout comme JavaScript, le compilateur fonctionne en plusieurs étapes. On dit simplement que la première étape la compilation plus rapide, mais a tendance à générer du code plus lent. Une fois que le module a commencé le navigateur détecte les composants fréquemment utilisés et les envoie via un compilateur plus optimisé, mais plus lent.
Notre cas d'utilisation est intéressant, dans la mesure où le code de rotation d'une image une fois, peut-être deux fois. Dans la grande majorité des cas, nous n'obtiendrons jamais les avantages du compilateur d'optimisation. Ceci est important à garder à l’esprit lorsque l'analyse comparative. Exécuter nos modules WebAssembly 10 000 fois dans une boucle nous donnerait des résultats irréalistes. Pour obtenir des chiffres réalistes, nous devons exécuter le module une fois et prendre des décisions en fonction des chiffres de cette seule course.
Comparaison des performances
Ces deux graphiques représentent des vues différentes des mêmes données. Dans le premier graphique, nous par navigateur ; dans le second, nous comparons par langue. Veuillez notez que j'ai choisi une échelle de temps logarithmique. Il est également important que tous utilisaient la même image de test de 16 mégapixels et le même hôte à l'exception d'un navigateur, qui ne peut pas être exécuté sur la même machine.
Sans trop analyser ces graphiques, il est évident que nous avons résolu le problème problème de performances: tous les modules WebAssembly s'exécutent en 500 ms maximum. Ce confirme ce que nous avons vu au départ: WebAssembly offre des fonctions prévisibles des performances. Quelle que soit la langue choisie, les différences entre les navigateurs et les langages de programmation est minime. Pour être exact: l'écart type de JavaScript dans tous les navigateurs est d'environ 400 ms, alors que l'écart-type de tous nos L'utilisation des modules WebAssembly est d'environ 80 ms pour tous les navigateurs.
Efforts à fournir
Autre métrique : les efforts que nous avons dû fournir pour créer et intégrer notre module WebAssembly dans squoosh. Il est difficile d'attribuer une valeur numérique à je ne vais pas créer de graphiques, mais j'aimerais avoir quelques éléments souligner:
AssemblyScript était fluide. Il vous permet non seulement d'utiliser TypeScript pour d'écrire WebAssembly, ce qui facilite grandement la révision du code pour mes collègues. produit des modules WebAssembly sans colle, de très petits éléments, des performances. Les outils de l'écosystème TypeScript, comme Prettier et Tslint, fonctionnera probablement.
L'utilisation de Rust avec wasm-pack
est également extrêmement pratique, mais elle offre d'excellents résultats.
Dans les projets WebAssembly de grande envergure, les liaisons et la gestion de la mémoire
nécessaires. Nous avons dû nous écarter un peu du parcours du bonheur pour parvenir à une
la taille du fichier.
C et Emscripten ont créé un module WebAssembly très petit et très performant. prêt à l'emploi, mais sans avoir le courage de vous lancer dans le code de la colle et de le réduire à mais la taille totale (module WebAssembly + code glue) se termine. étant assez grandes.
Conclusion
Alors, quel langage devez-vous utiliser si vous avez un chemin chaud JS et que vous voulez plus rapides et plus cohérents avec WebAssembly. Comme toujours pour les performances questions, la réponse est: cela dépend. Alors, qu'avons-nous livré ?
<ph type="x-smartling-placeholder">Comparaison au niveau de la taille du module et des performances des différents langages que nous avons utilisé, le meilleur choix semble être C ou AssemblyScript. Nous avons décidé d'expédier Rust. Il y plusieurs raisons justifient cette décision: tous les codecs livrés sur Squoosh jusqu'à présent sont compilés avec Emscripten. Nous souhaitions élargir nos connaissances WebAssembly et utiliser un autre langage en production. AssemblyScript est une bonne alternative, mais le projet est relativement jeune et le compilateur n'est pas aussi mature que le compilateur Rust.
Bien que la différence de taille de fichier entre Rust et les autres langues le graphique à nuage de points est plutôt radical, mais ce n'est pas si grave en réalité: Le chargement de 500 octets ou 1,6 Ko, même en 2G, prend moins d'un dixième de seconde. Et Nous espérons que Rust comblera bientôt l'écart en termes de taille de module.
En termes de performances d'exécution, Rust a une moyenne plus rapide sur tous les navigateurs que AssemblyScript. Surtout pour les projets plus importants, Rust sera plus susceptible générer du code plus rapidement sans avoir besoin d'optimiser le code manuellement. Mais cela cela ne devrait pas vous empêcher d’utiliser ce avec quoi vous êtes le plus à l’aise.
Cela étant dit, AssemblyScript a été une grande découverte. Elle permet aux applications aux développeurs de créer des modules WebAssembly sans avoir à apprendre langue. L'équipe AssemblyScript a été très réactive et active à améliorer leur chaîne d'outils. Nous garderons un œil sur AssemblyScript.
Mise à jour: Rust
Après la publication de cet article, Nick Fitzgerald
de l'équipe Rust nous a indiqué son excellent livre Rust Wasm, qui contient
une section sur l'optimisation de la taille des fichiers. Suivez le
des instructions spécifiques (surtout l'optimisation de la durée de l'association
gestion de la panique) nous a permis d'écrire du code Rust « normal » et de revenir à l'utilisation
Cargo
(npm
de Rust) sans augmenter la taille du fichier. Le module Rust se termine
avec 370 octets après gzip. Pour en savoir plus, consultez le rapport de presse que j'ai ouvert sur Squoosh.
Un grand merci à Ashley Williams, Steve Klabnik, Nick Fitzgerald et Max Graey pour leur aide dans cette aventure.