Karmem es un formato de serialización binario rápido. La prioridad de Karmem es ser fácil de usar mientras ha sido lo más rápido posible. Está optimizado para tomar el rendimiento máximo de Golang y Tinygo y es eficiente para lecturas repetibles, leyendo contenido diferente del mismo tipo. Karmem demuestra ser diez veces más rápido que Google Flatbuffers, con la sobrecarga adicional de la verificación de los límites incluidos.
️ Karmem todavía en desarrollo, la API no es estable. Sin embargo, el formato de serialización en sí es diferente al cambiar y debe permanecer compatible con versiones anteriores con versiones anteriores.
Karmem fue creado para resolver un solo problema: facilitar la transferencia de datos entre el host de websembly y el invitado. Mientras aún es portátil para idiomas que no son de webassembly. Estamos experimentando con un "patrón de comando de eventos" entre WASM-Host y WASM-Guest en un proyecto, pero compartir datos es muy costoso, y las llamadas FFI tampoco son baratas. Karmem codifica una vez y comparte el mismo contenido con múltiples invitados, independientemente del idioma, lo que lo hace muy eficiente. Además, incluso usando Object-API para decodificar, es lo suficientemente rápido, y Karmem fue diseñado para aprovechar ese patrón, evitar las asignaciones y reutilizar la misma estructura para múltiples datos.
¿Por qué no usar WITX? Es un buen proyecto y tiene como objetivo WASM, sin embargo, parece más complejo y define no solo la estructura de datos, sino las funciones, que estoy tratando de evitar. Además, no está destinado a ser portátil a no WASM. ¿Por qué no usar FlatBuffers? Lo intentamos, pero no es lo suficientemente rápido y también causa pánico debido a la falta de verificación límite. ¿Por qué no usar Cap'n'Proto? Es una buena alternativa, pero carece de implementación para ZIG y Assemblyscript, que es la máxima prioridad, también tiene más asignaciones y la API generada es más difícil de usar, en comparación con Karmem.
Ese es un pequeño ejemplo de cómo usar karmem.
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 ;
} Genere el código usando go run karmem.org/cmd/karmem build --golang -o "km" app.km
Para codificar, el uso debe crear una estructura nativa y luego codificarla.
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 )
}En lugar de decodificarlo a otra estructura, puede leer algunos campos directamente, sin ninguna decodificación adicional. En este ejemplo, solo necesitamos el nombre de usuario de cada perfil.
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 ))
}
} Aviso: usamos NewAccountDataViewer , cualquier Viewer es solo un espectador y no copia los datos de backend. Algunos idiomas (C#, AssemblyScript) usan UTF-16, mientras que Karmem usa UTF-8, en esos casos tiene alguna penalización de rendimiento.
También puede decodificarlo a una estructura existente. En algunos casos, es mejor si reutiliza la misma estructura para las lecturas múltiples.
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 )
}Usando un esquema similar con Flatbuffers y Karmem. Karmem es casi 10 veces más rápido que Google Flatbuffers.
Nativo (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 en 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)
El rendimiento es casi el mismo cuando se compara la lectura de datos no serializados de una estructura nativa y lo lee a partir de datos de karmem.
Nativo (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)
Esa es una comparación con todos los idiomas compatibles.
WebAssembly en 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%
Actualmente, nos centramos en WebAssembly, y por eso esos son los idiomas admitidos:
Algunos idiomas aún están en desarrollo, y no tienen ninguna promesa de compatibilidad hacia atrás. Intentaremos mantenernos al día con la última versión. Actualmente, la API generada y las bibliotecas no deben considerarse estables.
| Características | Ir/tinygo | Zigza | Ensamblaje | Rápido | do | C#/. NET | Odín |
|---|---|---|---|---|---|---|---|
| Actuación | Bien | Excelente | Bien | Pobre | Excelente | Horrible | Bien |
| Prioridad | Alto | Alto | Alto | Bajo | Alto | Medio | Bajo |
| Codificación | |||||||
| Codificación de objetos | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
| Codificación en bruto | |||||||
| Copia cero | |||||||
| Descodificación | |||||||
| Decodificación de objetos | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
| Reutilización de objetos | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | |
| Acceso al azar | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
| Copia cero | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | |
| Cuerda cero | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ||
| Matriz nativa | ✔️ | ✔️ | ✔️ | ️ | ✔️ |
Karmem utiliza un lenguaje de esquema personalizado, que define estructuras, enumines y tipos.
El esquema es muy simple de entender y definir:
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 ;
} Cada archivo debe comenzar con: karmem {name} [@tag()]; . Se pueden definir otras etiquetas opcionales, como se muestra anteriormente, se recomienda usar la opción @packed(true) .
Primitivas :
uint8 , uint16 , uint32 , uint64int8 , int16 , int32, int64float32 , float64boolbyte , charNo es posible definir tipos opcionales o anulables.
Matrices :
[{Length}]{Type} (ejemplo: [123]uint16 , [3]float32 )[]{Type} (ejemplo: []char , []uint64 )[<{Length}]{Type} (ejemplo: [<512]float64 , [<42]byte )No es posible tener una rebanada de tablas o rebanadas de enumnos o rebanadas de rodajas. Sin embargo, es posible envolver esos tipos dentro de un Estructura en línea.
Actualmente, Karmem tiene dos tipos de estructuras: en línea y tabla.
En línea: las estructuras en línea, como su nombre indica, se incluyen cuando se usan. Eso reduce el tamaño y puede mejorar el rendimiento. Sin embargo, no puede cambiar su definición. En orden: no puede editar el campo de una estructura en línea sin romper la compatibilidad.
struct Vec3 inline {
X float32 ;
Y float32 ;
Z float32 ;
} Esa estructura es exactamente la misma de [3]float32 y tendrá el mismo resultado de serialización. Debido a eso, cualquier cambio de esta estructura (por ejemplo, cambiarlo a float64 o agregar nuevos campos) romperá la compatibilidad.
Tablas: las tablas se pueden usar cuando es importante que la compatibilidad hacia atrás. Por ejemplo, las tablas pueden tener nuevos campos adjuntar en la parte inferior sin romper la compatibilidad.
struct User table {
Name [] char ;
Email [] char ;
Password [] char ;
}Consideremos que necesita otro campo ... para tablas, no es un problema:
struct User table {
Name [] char ;
Email [] char ;
Password [] char ;
Telephone [] char ;
}Como es una tabla, puede agregar nuevos campos en la parte inferior de la estructura, y ambas versiones son compatibles entre ellos. Si el mensaje se envía a un cliente que no comprende el nuevo campo, se ignorará. Si un cliente obsoleto envía un mensaje a un cliente más nuevo, el nuevo campo tendrá el valor predeterminado (0, falso, cadena vacía, etc.).
Las enumeras se pueden usar como un alias para el tipo de enteros, como uint8 .
enum Team uint8 {
Unknown ;
Humans ;
Orcs ;
Zombies = 255 ;
}Los enums deben comenzar con un valor cero, el valor predeterminado en todos los casos. Si se omite el valor de cualquier enum, utilizará el orden de enum como valor.
Una vez que tenga un esquema definido, puede generar el código. Primero, debe instalar karmem , obtenerlo de la página de versiones o ejecutarlo con Go.
karmem build --assemblyscript -o "output-folder" your-schema.km
Si ya tiene Golang instalado, puede usar go karmem.org/cmd/karmem build --zig -o "output-folder" your-schema.km en su lugar.
Comandos:
build
--zig : habilitar la generación para zig--swift : habilitar la generación para swift/swiftwasm--odin : habilitar la generación para Odin--golang : habilitar la generación para Golang/Tinygo--dotnet : habilitar la generación para .net--c : habilitar la generación para c--assemblyscript : Habilitar generación para ensamblyscript-o <dir> : Define la carpeta de salida<input-file> : Define el esquema de entradaKarmem es rápido y también tiene como objetivo ser seguro y estable para el uso general.
Fuera de los límites
Karmem incluye verificación de límites para evitar la lectura fuera de los límites y evitar accidentes y pánico. Eso es algo que Google Flatbuffers no tiene, y el contenido malformado causará pánico. Sin embargo, no arregla todas las vulnerabilidades posibles.
Agotamiento de recursos
Karmem permite que un puntero/desplazamiento se pueda reutilizar varias veces en el mismo mensaje. Desafortunadamente, ese comportamiento hace posible que un mensaje corto genere matrices más extensas que el tamaño del mensaje. Actualmente, la única mitigación para ese problema es utilizar matrices limitadas en lugar de matrices y evitar la decodificación de objeto-API.
Filtración de datos
Karmem no borra la memoria antes de codificar, que puede filtrar información del mensaje anterior o de la memoria del sistema en sí. Eso se puede resolver usando la etiqueta @packed(true) , como se describió anteriormente. La etiqueta packed eliminará el relleno del mensaje, lo que evitará la fuga. Alternativamente, puede borrar la memoria antes de codificar, manualmente.
Karmem tiene algunas limitaciones en comparación con otras bibliotecas de serialización, como:
Tamaño máximo
Similar a Google ProtoBuf y Google Flatbuffers, Karmem tiene un tamaño máximo de 2 GB. Ese es el tamaño máximo de todo el mensaje, no el tamaño máximo de cada matriz. Esta limitación se debe al hecho de que WASM está diseñado para ser de 32 bits, y el tamaño máximo de 2 GB parece adecuado para las necesidades actuales. El escritor actual no hace cumplir esta limitación, pero leer un mensaje que sea más grande que 2GB causará un comportamiento indefinido.
Matrices de matrices/mesas
Karmem no admite matrices de matrices o matrices de tablas. Sin embargo, es posible envolver esos tipos dentro de un Estructura en línea, como se mencionó anteriormente. Esa limitación se impuso para aprovechar las matrices/rebanadas nativas del idioma. La mayoría de los idiomas encapsula el puntero y el tamaño de la matriz dentro de una estructura, que requiere que se conozca el tamaño de cada elemento, lo que evita la matrices de elementos con tamaño/zancadas variables.
UTF-8
Karmem solo admite UTF-8 y no admite otras codificaciones.