Karmm est un format de sérialisation binaire rapide. La priorité de Karmm est d'être facile à utiliser tout en étant rapide que possible. Il est optimisé pour prendre les performances maximales de Golang et Tinygo et est efficace pour les lectures reproductibles, lisant un contenu différent du même type. Karmmem démontre dix fois plus vite que Google Flatbuffers, avec les frais généraux supplémentaires de vérification des limites inclus.
️ Karmm toujours en cours de développement, l'API n'est pas stable. Cependant, le format de sérialisation lui-même est différent de changer et doit rester en arrière compatible avec les versions plus anciennes.
Karmm a été créé pour résoudre un seul problème: rendre les données faciles à transférer entre l'hôte WebAssembly et l'invité. Bien que toujours portable pour les langues non WebSembly. Nous expérimentons un "modèle de commande d'événements" entre Wasm-Host et WasM-Gest dans un projet, mais le partage de données est très cher, et les appels FFI ne sont pas bon marché non plus. Karmm encode une fois et partage le même contenu avec plusieurs invités, quelle que soit la langue, ce qui le rend très efficace. De plus, même en utilisant l'objet-API pour décoder, il est assez rapide et Karmm a été conçu pour profiter de ce modèle, éviter les allocations et réutiliser la même structure pour plusieurs données.
Pourquoi ne pas utiliser Witx? Il s'agit d'un bon projet et visant à WASM, mais il semble plus complexe et définit non seulement la structure des données, mais les fonctions, que j'essaie d'éviter. De plus, il n'est pas destiné à être portable en non-pas. Pourquoi ne pas utiliser Flatbuffers? Nous avons essayé, mais ce n'est pas assez rapide et provoque également des paniques en raison du manque de vérification liée. Pourquoi ne pas utiliser Cap'n'proto? C'est une bonne alternative mais manque d'implémentation pour le zig et l'assemblage, qui est la priorité supérieure, il a également plus d'allocations et l'API générée est plus difficile à utiliser, par rapport à Karmm.
C'est un petit exemple de la façon dont l'utilisation de Karmm.
karmem app @ packed ( true ) @ golang . package ( `app` );
enum SocialNetwork uint8 { Unknown ; Facebook ; Instagram ; Twitter ; TikTok ; }
struct ProfileData table {
Network SocialNetwork ;
Username [] char ;
ID uint64 ;
}
struct Profile inline {
Data ProfileData ;
}
struct AccountData table {
ID uint64 ;
Email [] char ;
Profiles [] Profile ;
} Générez le code à l'aide de go run karmem.org/cmd/karmem build --golang -o "km" app.km
Pour encoder, l'utilisation doit créer une structure native puis la coder.
var writerPool = sync. Pool { New : func () any { return karmem . NewWriter ( 1024 ) }}
func main () {
writer := writerPool . Get ().( * karmem. Writer )
content := app. AccountData {
ID : 42 ,
Email : "[email protected]" ,
Profiles : []app. Profile {
{ Data : app. ProfileData {
Network : app . SocialNetworkFacebook ,
Username : "inkeliz" ,
ID : 123 ,
}},
{ Data : app. ProfileData {
Network : app . SocialNetworkFacebook ,
Username : "karmem" ,
ID : 231 ,
}},
{ Data : app. ProfileData {
Network : app . SocialNetworkInstagram ,
Username : "inkeliz" ,
ID : 312 ,
}},
},
}
if _ , err := content . WriteAsRoot ( writer ); err != nil {
panic ( err )
}
encoded := writer . Bytes ()
_ = encoded // Do something with encoded data
writer . Reset ()
writerPool . Put ( writer )
}Au lieu de le décoder à une autre structure, vous pouvez lire directement certains champs, sans aucun décodage supplémentaire. Dans cet exemple, nous n'avons besoin que du nom d'utilisateur de chaque profil.
func decodes ( encoded [] byte ) {
reader := karmem . NewReader ( encoded )
account := app . NewAccountDataViewer ( reader , 0 )
profiles := account . Profiles ( reader )
for i := range profiles {
fmt . Println ( profiles [ i ]. Data ( reader ). Username ( reader ))
}
} AVIS: Nous utilisons NewAccountDataViewer , toute Viewer n'est qu'une visionneuse et ne copie pas les données backend. Certaines langues (C #, assemblyScript) utilisent UTF-16, tandis que Karmm utilise UTF-8, dans ces cas, vous avez une pénalité de performance.
Vous pouvez également le décoder vers une structure existante. Dans certains cas, il vaut mieux que vous réutilisez la même structure pour les lectures de multiples.
var accountPool = sync. Pool { New : func () any { return new (app. AccountData ) }}
func decodes ( encoded [] byte ) {
account := accountPool . Get ().( * app. AccountData )
account . ReadAsRoot ( karmem . NewReader ( encoded ))
profiles := account . Profiles
for i := range profiles {
fmt . Println ( profiles [ i ]. Data . Username )
}
accountPool . Put ( account )
}En utilisant un schéma similaire avec desbuffers et du karmm. Karmm est presque 10 fois plus rapide que Google Flatbuffers.
Natif (macOS / ARM64 - M1):
name old time/op new time/op delta
EncodeObjectAPI-8 2.54ms ± 0% 0.51ms ± 0% -79.85% (p=0.008 n=5+5)
DecodeObjectAPI-8 3.57ms ± 0% 0.20ms ± 0% -94.30% (p=0.008 n=5+5)
DecodeSumVec3-8 1.44ms ± 0% 0.16ms ± 0% -88.86% (p=0.008 n=5+5)
name old alloc/op new alloc/op delta
EncodeObjectAPI-8 12.1kB ± 0% 0.0kB -100.00% (p=0.008 n=5+5)
DecodeObjectAPI-8 2.87MB ± 0% 0.00MB -100.00% (p=0.008 n=5+5)
DecodeSumVec3-8 0.00B 0.00B ~ (all equal)
name old allocs/op new allocs/op delta
EncodeObjectAPI-8 1.00k ± 0% 0.00k -100.00% (p=0.008 n=5+5)
DecodeObjectAPI-8 110k ± 0% 0k -100.00% (p=0.008 n=5+5)
DecodeSumVec3-8 0.00 0.00 ~ (all equal)
WebAssembly sur Wazero (macOS / ARM64 - M1):
name old time/op new time/op delta
EncodeObjectAPI-8 17.2ms ± 0% 4.0ms ± 0% -76.51% (p=0.008 n=5+5)
DecodeObjectAPI-8 50.7ms ± 2% 1.9ms ± 0% -96.18% (p=0.008 n=5+5)
DecodeSumVec3-8 5.74ms ± 0% 0.75ms ± 0% -86.87% (p=0.008 n=5+5)
name old alloc/op new alloc/op delta
EncodeObjectAPI-8 3.28kB ± 0% 3.02kB ± 0% -7.80% (p=0.008 n=5+5)
DecodeObjectAPI-8 3.47MB ± 2% 0.02MB ± 0% -99.56% (p=0.008 n=5+5)
DecodeSumVec3-8 1.25kB ± 0% 1.25kB ± 0% ~ (all equal)
name old allocs/op new allocs/op delta
EncodeObjectAPI-8 4.00 ± 0% 4.00 ± 0% ~ (all equal)
DecodeObjectAPI-8 5.00 ± 0% 4.00 ± 0% -20.00% (p=0.008 n=5+5)
DecodeSumVec3-8 5.00 ± 0% 5.00 ± 0% ~ (all equal)
Les performances sont presque les mêmes lors de la comparaison de la lecture de données non sérialisées à partir d'une structure native et de la lecture à partir de données sérialisées de Karmm.
Natif (macOS / ARM64 - M1):
name old time/op new time/op delta
DecodeSumVec3-8 154µs ± 0% 160µs ± 0% +4.36% (p=0.008 n=5+5)
name old alloc/op new alloc/op delta
DecodeSumVec3-8 0.00B 0.00B ~ (all equal)
name old allocs/op new allocs/op delta
DecodeSumVec3-8 0.00 0.00 ~ (all equal)
C'est une comparaison avec toutes les langues prises en charge.
WebAssembly sur Wazero (macOS / ARM64 - M1):
name time/op result/wasi-go-km.out result/wasi-as-km.out result/wasi-zig-km.out result/wasi-swift-km.out result/wasi-c-km.out result/wasi-odin-km.out result/wasi-dotnet-km.out
DecodeSumVec3-8 757µs ± 0% 1651µs ± 0% 369µs ± 0% 9145µs ± 6% 368µs ± 0% 1330µs ± 0% 75671µs ± 0%
DecodeObjectAPI-8 1.59ms ± 0% 6.13ms ± 0% 1.04ms ± 0% 30.59ms ±34% 0.90ms ± 1% 4.06ms ± 0% 231.72ms ± 0%
EncodeObjectAPI-8 3.96ms ± 0% 4.51ms ± 1% 1.20ms ± 0% 8.26ms ± 0% 1.03ms ± 0% 5.19ms ± 0% 237.99ms ± 0%
name alloc/op result/wasi-go-km.out result/wasi-as-km.out result/wasi-zig-km.out result/wasi-swift-km.out result/wasi-c-km.out result/wasi-odin-km.out result/wasi-dotnet-km.out
DecodeSumVec3-8 1.25kB ± 0% 21.75kB ± 0% 1.25kB ± 0% 1.82kB ± 0% 1.25kB ± 0% 5.34kB ± 0% 321.65kB ± 0%
DecodeObjectAPI-8 15.0kB ± 0% 122.3kB ± 1% 280.8kB ± 1% 108.6kB ± 3% 1.2kB ± 0% 23.8kB ± 0% 386.5kB ± 0%
EncodeObjectAPI-8 3.02kB ± 0% 58.00kB ± 1% 1.23kB ± 0% 1.82kB ± 0% 1.23kB ± 0% 8.91kB ± 0% 375.82kB ± 0%
name allocs/op result/wasi-go-km.out result/wasi-as-km.out result/wasi-zig-km.out result/wasi-swift-km.out result/wasi-c-km.out result/wasi-odin-km.out result/wasi-dotnet-km.out
DecodeSumVec3-8 5.00 ± 0% 5.00 ± 0% 5.00 ± 0% 32.00 ± 0% 5.00 ± 0% 6.00 ± 0% 11.00 ± 0%
DecodeObjectAPI-8 5.00 ± 0% 4.00 ± 0% 4.00 ± 0% 32.00 ± 0% 4.00 ± 0% 6.00 ± 0% 340.00 ± 0%
EncodeObjectAPI-8 4.00 ± 0% 3.00 ± 0% 3.00 ± 0% 30.00 ± 0% 3.00 ± 0% 5.00 ± 0% 40.00 ± 0%
Actuellement, nous nous concentrons sur WebAssembly, et à cause de cela, ce sont les langues prises en charge:
Certaines langues sont toujours en cours de développement et n'ont aucune promesse de compatibilité en arrière. Nous essaierons de suivre la dernière version. Actuellement, l'API générée et les bibliothèques ne devraient pas considérer stable.
| Caractéristiques | Aller / tinygo | Zigou | Assemblance | Rapide | C | C # /. Net | Odin |
|---|---|---|---|---|---|---|---|
| Performance | Bien | Excellent | Bien | Pauvre | Excellent | Horrible | Bien |
| Priorité | Haut | Haut | Haut | Faible | Haut | Moyen | Faible |
| Codage | |||||||
| Encodage d'objets | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
| Codage brut | |||||||
| Copie zéro | |||||||
| Décodage | |||||||
| Décodage d'objet | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
| Réutilisation d'objets | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | |
| Accès aléatoire | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
| Copie zéro | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | |
| Corde à copie zéro | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ||
| Tableau natif | ✔️ | ✔️ | ✔️ | ️ | ✔️ |
Karmmem utilise un langage de schéma personnalisé, qui définit les structures, les énumériques et les types.
Le schéma est très simple à comprendre et à définir:
karmem game @ packed ( true ) @golang. package ( `km` ) @ assemblyscript . import ( `../../assemblyscript/karmem` );
enum Team uint8 { Humans ; Orcs ; Zombies ; Robots ; Aliens ;}
struct Vec3 inline {
X float32 ;
Y float32 ;
Z float32 ;
}
struct MonsterData table {
Pos Vec3 ;
Mana int16 ;
Health int16 ;
Name [] char ;
Team Team ;
Inventory [ < 128 ] byte ;
Hitbox [ 4 ] float64 ;
Status [] int32 ;
Path [ < 128 ] Vec3 ;
}
struct Monster inline {
Data MonsterData ;
}
struct State table {
Monsters [ < 2000 ] Monster ;
} Chaque fichier doit commencer par: karmem {name} [@tag()]; . D'autres balises facultatives peuvent être définies, comme indiqué ci-dessus, il est recommandé d'utiliser l'option @packed(true) .
Primitives :
uint8 , uint16 , uint32 , uint64int8 , int16 , int32, int64float32 , float64boolbyte , charIl n'est pas possible de définir des types facultatifs ou nullables.
Arris :
[{Length}]{Type} (Exemple: [123]uint16 , [3]float32 )[]{Type} (exemple: []char , []uint64 )[<{Length}]{Type} (exemple: [<512]float64 , [<42]byte )Il n'est pas possible d'avoir une tranche de tables ou des tranches d'énumériques ou des tranches de tranches. Cependant, il est possible d'envelopper ces types à l'intérieur d'une structure en ligne.
Actuellement, Karmm a deux types de structures: en ligne et en table.
En ligne: les structures en ligne, comme son nom l'indique, sont inclinées lorsqu'elles sont utilisées. Cela réduit la taille et peut améliorer les performances. Cependant, il ne peut pas faire changer leur définition. Dans l'ordre: vous ne pouvez pas modifier le champ d'une structure en ligne sans casser la compatibilité.
struct Vec3 inline {
X float32 ;
Y float32 ;
Z float32 ;
} Cette structure est exactement la même de [3]float32 et aura le même résultat de sérialisation. À cause de cela, tout changement de cette structure (par exemple, le modifie en float64 ou l'ajout de nouveaux champs) rompra la compatibilité.
Tables: les tables peuvent être utilisées lorsque la compatibilité arrière compte. Par exemple, les tables peuvent avoir de nouveaux champs ajouter en bas sans casser la compatibilité.
struct User table {
Name [] char ;
Email [] char ;
Password [] char ;
}Considérons que vous avez besoin d'un autre champ ... pour les tables, ce n'est pas un problème:
struct User table {
Name [] char ;
Email [] char ;
Password [] char ;
Telephone [] char ;
}Comme il s'agit d'un tableau, vous pouvez ajouter de nouveaux champs au bas de la structure, et les deux versions sont compatibles entre elles. Si le message est envoyé à un client qui ne comprend pas le nouveau champ, il sera ignoré. Si un client obsolète envoie un message à un client plus récent, le nouveau champ aura la valeur par défaut (0, fausse, chaîne vide, etc.).
Les énumérations peuvent être utilisées comme un alias pour les entiers, comme uint8 .
enum Team uint8 {
Unknown ;
Humans ;
Orcs ;
Zombies = 255 ;
}Les énumérations doivent commencer par une valeur zéro, la valeur par défaut dans tous les cas. Si la valeur d'une énumération est omise, elle utilisera l'ordre de l'énumération comme valeur.
Une fois que vous avez un schéma défini, vous pouvez générer le code. Tout d'abord, vous devez être installé karmem , le faire à partir de la page des versions ou l'exécuter avec Go.
karmem build --assemblyscript -o "output-folder" your-schema.km
Si vous avez déjà installé Golang, vous pouvez utiliser go karmem.org/cmd/karmem build --zig -o "output-folder" your-schema.km à la place.
Commandes:
build
--zig : Activer la génération pour zig--swift : Activer la génération pour Swift / Swiftwasm--odin : Activer la génération pour Odin--golang : Activer la génération pour Golang / Tinygo--dotnet : Activer la génération pour .net--c : Activer la génération pour c--assemblyscript : Activer la génération pour assemblyscript-o <dir> : définit le dossier de sortie<input-file> : définit le schéma d'entréeKarmm est rapide et vise également à être sécurisé et stable pour l'utilisation générale.
Hors des limites
Karmmem comprend la vérification des limites pour éviter la lecture hors limites et éviter les accidents et les paniques. C'est quelque chose que Google Flatbuffers n'a pas, et le contenu mal formé provoquera la panique. Cependant, cela ne corrige pas toutes les vulnérabilités possibles.
Épuisement des ressources
Karmem permet à un pointeur / décalage puisse être réutilisé plusieurs fois dans le même message. Malheureusement, ce comportement permet à un court message de générer des tableaux plus étendus que la taille du message. Actuellement, la seule atténuation de ce problème consiste à utiliser des terrains limités au lieu des tableaux et à éviter le décodage d'objet-API.
Fuite de données
Karmm n'efface pas la mémoire avant le codage, ce qui peut fuir des informations du message précédent ou de la mémoire du système lui-même. Cela peut être résolu à l'aide de la balise @packed(true) , comme décrit précédemment. La balise packed supprimera le rembourrage du message, ce qui empêchera la fuite. Alternativement, vous pouvez effacer la mémoire avant le codage, manuellement.
Karmm a certaines limites par rapport à d'autres bibliothèques de sérialisation, telles que:
Taille maximale
Semblable à Google Protobuf et Google Flatbuffers, Karmm a une taille maximale de 2 Go. C'est la taille maximale de l'ensemble du message, pas la taille maximale de chaque tableau. Cette limitation est due au fait que WASM est conçu pour être 32 bits, et la taille maximale de 2 Go semble adéquate pour les besoins actuels. L'écrivain actuel n'applique pas cette limitation, mais la lecture d'un message supérieur à 2 Go provoquera un comportement non défini.
Tableaux de tableaux / tables
Karmm ne prend pas en charge les tableaux de tableaux ou de tableaux de tables. Cependant, il est possible d'envelopper ces types à l'intérieur d'une structure en ligne, comme mentionné ci-dessus. Cette limitation a été imposée pour profiter des tableaux / tranches indigènes de la langue. La plupart des langues résument le pointeur et la taille du réseau à l'intérieur d'une structure, qui nécessite que la taille de chaque élément soit connu, empêchant par conséquent des tableaux d'articles avec une taille / des progrès variables.
UTF-8
Karmm ne prend en charge que l'UTF-8 et ne prend pas en charge d'autres encodages.