Karmem é um formato de serialização binária rápida. A prioridade do karmem é fácil de usar, enquanto foi rápida possível. É otimizado para tomar o desempenho máximo de Golang e Tinygo e é eficiente para leituras repetíveis, lendo conteúdo diferente do mesmo tipo. Karmem demonstra ser dez vezes mais rápido que o Google Flatbuffers, com a sobrecarga adicional da verificação dos limites incluídos.
️ Karmem ainda em desenvolvimento, a API não é estável. No entanto, o próprio formato de serialização é diferente de mudar e deve permanecer compatível com versões mais antigas.
O Karmem foi criado para resolver um único problema: facilite a transferência de dados entre o host WebAssembly e o convidado. Embora ainda portátil para idiomas que não sejam de Webassembly. Estamos experimentando um "padrão de comando de eventos" entre o WASM-HOST e o WASM-Guest em um projeto, mas o compartilhamento de dados é muito caro, e as chamadas de FFI também não são baratas. Karmem codifica uma vez e compartilha o mesmo conteúdo com vários convidados, independentemente do idioma, tornando -o muito eficiente. Além disso, mesmo usando o objeto-api para decodificar, é rápido o suficiente, e o Karmem foi projetado para tirar proveito desse padrão, evitar alocações e reutilizar a mesma estrutura para vários dados.
Por que não usar o Witx? É um bom projeto e teve como objetivo o WASM, no entanto, parece mais complexo e define não apenas a estrutura de dados, mas também as funções, que estou tentando evitar. Além disso, não se destina a ser portátil para não-wasm. Por que não usar plakingbuffers? Tentamos, mas não é rápido o suficiente e também causa pânico devido à falta de verificação de limites. Por que não usar Cap'n'proto? É uma boa alternativa, mas carece de implementação para o ZIG e o AssemblyScript, que é a prioridade superior, também possui mais alocações e a API gerada é mais difícil de usar, em comparação que o Karmem.
Esse é um pequeno exemplo de como o uso do 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 ;
} Gere o código usando go run karmem.org/cmd/karmem build --golang -o "km" app.km
Para codificar, o uso deve criar uma estrutura nativa e depois codificá -la.
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 )
}Em vez de decodificá -lo para outra estrutura, você pode ler alguns campos diretamente, sem qualquer decodificação adicional. Neste exemplo, precisamos apenas do nome de usuário 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 , qualquer Viewer é apenas um visualizador e não copia os dados do back -end. Alguns idiomas (C#, Assemblyscript) usam o UTF-16, enquanto o Karmem usa o UTF-8, nesses casos, você tem alguma penalidade de desempenho.
Você também pode decodificá -lo para uma estrutura existente. Em alguns casos, é melhor se você reutilizar a mesma estrutura para leituras múltiplas.
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 esquema semelhante com planilha e karmem. Karmem é quase 10 vezes mais rápido que o 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 em 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)
O desempenho é quase o mesmo ao comparar os dados não serializados de uma estrutura nativa e a leitura de dados serializados por 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)
Essa é uma comparação com todos os idiomas suportados.
WebAssembly em 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%
Atualmente, nos concentramos no WebAssembly e, por isso, esses são os idiomas suportados:
Alguns idiomas ainda em desenvolvimento e não têm nenhuma promessa de compatibilidade com versões anteriores. Vamos tentar acompanhar a versão mais recente. Atualmente, a API gerada e as bibliotecas não devem considerar estáveis.
| Características | Vá/tinygo | Zig | AssemblyScript | Swift | C | C#/. Net | Odin |
|---|---|---|---|---|---|---|---|
| Desempenho | Bom | Excelente | Bom | Pobre | Excelente | Horrível | Bom |
| Prioridade | Alto | Alto | Alto | Baixo | Alto | Médio | Baixo |
| Codificação | |||||||
| Codificação de objetos | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
| Codificação crua | |||||||
| Zero cópia | |||||||
| Decodificação | |||||||
| Decodificação de objetos | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
| Reutilização de objetos | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | |
| Acesso aleatório | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
| Zero cópia | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | |
| Cosca zero-cópia | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ||
| Array nativo | ✔️ | ✔️ | ✔️ | ️ | ✔️ |
Karmem usa uma linguagem de esquema personalizada, que define estruturas, enumes e tipos.
O esquema é muito simples de entender e 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 arquivo deve começar com: karmem {name} [@tag()]; . Outras tags opcionais podem ser definidas, como mostrado acima, é recomendável usar a opção @packed(true) .
Primitivos :
uint8 , uint16 , uint32 , uint64int8 , int16 , int32, int64float32 , float64boolbyte , charNão é possível definir tipos opcionais ou anuláveis.
Matrizes :
[{Length}]{Type} (exemplo: [123]uint16 , [3]float32 )[]{Type} (exemplo: []char , []uint64 )[<{Length}]{Type} (exemplo: [<512]float64 , [<42]byte )Não é possível ter fatia de mesas ou fatias de enumes ou fatias de fatias. No entanto, é possível envolver esses tipos dentro de uma estrutura embutida.
Atualmente, o Karmem possui dois tipos de estruturas: embutida e tabela.
Inline: as estruturas embutidas, como o nome sugerem, são inlinadas quando usadas. Isso reduz o tamanho e pode melhorar o desempenho. No entanto, não pode mudar sua definição. Em ordem de ordem: você não pode editar o campo de uma estrutura embutida sem quebrar a compatibilidade.
struct Vec3 inline {
X float32 ;
Y float32 ;
Z float32 ;
} Essa estrutura é exatamente a mesma de [3]float32 e terá o mesmo resultado de serialização. Por esse motivo, qualquer alteração dessa estrutura (por exemplo, altere -a para float64 ou adicionar novos campos) quebrará a compatibilidade.
Tabelas: as tabelas podem ser usadas quando a compatibilidade com versões anteriores. Por exemplo, as tabelas podem ter novos campos anexos na parte inferior sem quebrar a compatibilidade.
struct User table {
Name [] char ;
Email [] char ;
Password [] char ;
}Vamos considerar que você precisa de outro campo ... para tabelas, não é um problema:
struct User table {
Name [] char ;
Email [] char ;
Password [] char ;
Telephone [] char ;
}Como é uma tabela, você pode adicionar novos campos na parte inferior da estrutura, e ambas as versões são compatíveis entre elas. Se a mensagem for enviada a um cliente que não entende o novo campo, ela será ignorada. Se um cliente desatualizado enviar uma mensagem para um cliente mais novo, o novo campo terá o valor padrão (0, false, string vazia, etc.).
As enumes podem ser usadas como um alias para o tipo inteiro, como uint8 .
enum Team uint8 {
Unknown ;
Humans ;
Orcs ;
Zombies = 255 ;
}As enumes devem começar com um valor zero, o valor padrão em todos os casos. Se o valor de qualquer enumeração for omitido, ele usará a ordem de enum como valor.
Depois de definir um esquema, você pode gerar o código. Primeiro, você precisa instalar karmem , obtê -lo na página de lançamentos ou executá -lo com Go.
karmem build --assemblyscript -o "output-folder" your-schema.km
If you already have Golang installed, you can use go karmem.org/cmd/karmem build --zig -o "output-folder" your-schema.km instead.
Comandos:
build
--zig : Ativar geração para ZIG--swift : Ativar geração para Swift/Swiftwasm--odin : Ativar geração para Odin--golang : Ativar geração para Golang/Tinygo--dotnet : Ativar geração para .NET--c : Ativar geração para C--assemblyscript : Ativar geração para AssemblyScript-o <dir> : define a pasta de saída<input-file> : define o esquema de entradaKarmem é rápido e também tem como objetivo ser seguro e estável para o uso geral.
Fora dos limites
O Karmem inclui a verificação dos limites para impedir a leitura fora dos limites e evitar acidentes e pânico. Isso é algo que o Google Flatbuffers não possui, e o conteúdo malformado causará pânico. No entanto, isso não corrige todas as vulnerabilidades possíveis.
Exaustão de recursos
O Karmem permite que um ponteiro/deslocamento possa ser reutilizado várias vezes na mesma mensagem. Infelizmente, esse comportamento possibilita uma mensagem curta para gerar matrizes mais extensas do que o tamanho da mensagem. Atualmente, a única mitigação para esse problema é o uso de manchas limitadas em vez de matrizes e evitar decodificar objetos-api.
Vazamento de dados
Karmem não limpa a memória antes de codificar, que pode vazar informações da mensagem anterior ou da própria memória do sistema. Isso pode ser resolvido usando a tag @packed(true) , conforme descrito anteriormente. A tag packed removerá o preenchimento da mensagem, o que impedirá o vazamento. Como alternativa, você pode limpar a memória antes de codificar, manualmente.
Karmem tem algumas limitações em comparação com outras bibliotecas de serialização, como:
Tamanho máximo
Semelhante ao Google Protobuf e Google Flatbuffers, o Karmem tem um tamanho máximo de 2 GB. Esse é o tamanho máximo de toda a mensagem, não o tamanho máximo de cada matriz. Essa limitação se deve ao fato de que o WASM foi projetado para ser de 32 bits e o tamanho máximo de 2 GB parece adequado para as necessidades atuais. O escritor atual não aplica essa limitação, mas a leitura de uma mensagem maior que 2 GB causará comportamento indefinido.
Matrizes de matrizes/tabelas
Karmem não suporta matrizes de matrizes ou matrizes de tabelas. No entanto, é possível envolver esses tipos dentro de uma estrutura embutida, como mencionado acima. Essa limitação foi imposta a aproveitar as matrizes/fatias nativas do idioma. A maioria das línguas encapsula o ponteiro e o tamanho da matriz dentro de uma estrutura, que exige que o tamanho de cada elemento seja conhecido, consequentemente impedindo matrizes de itens com tamanho/avanço variável.
UTF-8
O Karmem suporta apenas o UTF-8 e não suporta outras codificações.