Karmem是一种快速的二元序列化格式。 Karmem的优先级是在尽可能快的同时易于使用。它优化了golang和Tinygo的最高性能,并且可以有效地读取相同类型的不同内容。 Karmem证明比Google Flatbuffers快十倍,其中包括范围检查的其他开销。
配x Karmem仍在开发中,API不稳定。但是,序列化格式本身与变化不同,应与较旧版本保持向后兼容。
Karmem是为了解决一个问题而创建的:在WebAssembly主机和来宾之间易于传输数据。虽然仍然适用于非网络隔板语言。我们正在一个项目中的WASM-HOST和WASM-GUEST之间尝试“事件命令”模式,但是共享数据非常昂贵,而FFI呼叫也不便宜。 Karmem一次编码一次,并与多位客人共享相同的内容,而不论语言如何,因此非常有效。同样,即使使用对象-API进行解码,它也足够快,而Karmem旨在利用该模式,避免分配,并重新使用相同的结构以获取多个数据。
为什么不使用witx?这是一个好的项目,目标是WASM,但是它似乎更复杂,不仅定义了我要避免的数据结构,还定义了功能。同样,它不打算便携到非浪费。为什么不使用Flatbuffers?我们尝试了,但是它不够快,并且由于缺乏界限检查而引起恐慌。为什么不使用cap'n'proto?这是一个很好的替代方法,但缺乏对高优先级的ZIG和汇编的实现,它也具有更多的分配,而生成的API比Karmem更难使用。
这是如何使用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 ;
}使用go run karmem.org/cmd/karmem build --golang -o "km" app.km生成代码。
为了编码,使用应创建本机结构然后对其进行编码。
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 )
}您可以直接读取某些字段,而无需任何其他解码,而不是将其解码为另一个结构。在此示例中,我们只需要每个配置文件的用户名。
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 ))
}
}注意:我们使用NewAccountDataViewer ,任何Viewer都是查看器,并且不复制后端数据。某些语言(C#,汇编)使用UTF-16,而Karmem使用UTF-8,在这种情况下,您会受到一些性能惩罚。
您也可以将其解码为存在的结构。在某些情况下,如果您将相同的结构重新使用以读取相同的结构,那就更好了。
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 )
}使用类似的模式与FlatBuffer和Karmem。 Karmem的速度比Google Flatbuffers快10倍。
本机(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在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)
在比较从本机结构中读取非序列化数据并从Karmem-Serialized数据读取它时,该性能几乎相同。
本机(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)
这是与所有受支持的语言的比较。
WebAssembly在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%
目前,我们专注于WebAssembly,因此这些是所支持的语言:
一些语言仍在开发中,并且没有任何向后兼容的承诺。我们将尝试跟上最新版本。当前,生成的API和库不应考虑稳定。
| 特征 | 去/tinygo | ZIG | 汇编 | 迅速 | c | C#/。网 | 奥丁 |
|---|---|---|---|---|---|---|---|
| 表现 | 好的 | 出色的 | 好的 | 贫穷的 | 出色的 | 可怕 | 好的 |
| 优先事项 | 高的 | 高的 | 高的 | 低的 | 高的 | 中等的 | 低的 |
| 编码 | |||||||
| 对象编码 | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
| 原始编码 | |||||||
| 零拷贝 | |||||||
| 解码 | |||||||
| 对象解码 | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
| 对象重复使用 | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | |
| 随机访问 | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
| 零拷贝 | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | |
| 零拷贝 | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ||
| 本机数组 | ✔️ | ✔️ | ✔️ | 配x | ✔️ |
Karmem使用一种自定义模式语言,该语言定义结构,枚举和类型。
模式非常容易理解和定义:
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 ;
}每个文件都必须以: karmem {name} [@tag()]; 。可以定义其他可选标签,如上所示,建议使用@packed(true)选项。
原始人:
uint8 , uint16 , uint32 , uint64int8 , int16 , int32, int64float32 , float64boolbyte , char无法定义可选类型。
数组:
[{Length}]{Type} (示例: [123]uint16 , [3]float32 )[]{Type} (示例: []char , []uint64 )[<{Length}]{Type} (示例: [<512]float64 , [<42]byte )不可能拥有一小部分桌子或枚举或切片切片。但是,可以将这些类型包装在一个内联架构中。
当前,Karmem有两种结构类型:内联和表。
内联:顾名思义,在使用时,内联构造是在使用的。这可以降低大小并可以提高性能。但是,它不能改变他们的定义。单词:您不能在不兼容的情况下编辑一个内联构造的字段。
struct Vec3 inline {
X float32 ;
Y float32 ;
Z float32 ;
}该结构在[3]float32中完全相同,并且将具有相同的序列化结果。因此,此结构的任何更改(例如,将其更改为float64或添加新字段)都会破坏兼容性。
表:向后兼容性很重要时可以使用表。例如,表可以在底部附加新字段而不会破坏兼容性。
struct User table {
Name [] char ;
Email [] char ;
Password [] char ;
}让我们考虑一下您需要另一个字段...对于桌子,这不是问题:
struct User table {
Name [] char ;
Email [] char ;
Password [] char ;
Telephone [] char ;
}由于它是一个表,您可以在结构的底部添加新字段,并且两个版本之间都兼容。如果消息发送给不了解新字段的客户端,则将被忽略。如果一个过时的客户端向新客户端发送消息,则新字段将具有默认值(0,false,空字符串等)。
枚举可以用作整数类型的别名,例如uint8 。
enum Team uint8 {
Unknown ;
Humans ;
Orcs ;
Zombies = 255 ;
}枚举必须从零值开始,在所有情况下,默认值。如果省略任何枚举的价值,它将以枚举顺序为价值。
定义模式后,您可以生成代码。首先,您需要安装karmem ,从发布页面中获取它或使用GO运行。
karmem build --assemblyscript -o "output-folder" your-schema.km
如果您已经安装了Golang,则可以使用go karmem.org/cmd/karmem build --zig -o "output-folder" your-schema.km 。
命令:
build
--zig :启用Zig的生成--swift :启用Swift/Swiftwasm的生成--odin :启用Odin的生成--golang :启用Golang/Tinygo的生成--dotnet :启用.net的生成--c :启用C--assemblyscript :启用汇编的生成-o <dir> :定义输出文件夹<input-file> :定义输入模式Karmem很快,也旨在安全和稳定以用于一般使用。
范围
Karmem包括检查界限,以防止越野读数并避免崩溃和恐慌。这是Google Flatbuffers没有的东西,并且畸形的内容会引起恐慌。但是,它并不能解决所有可能的漏洞。
资源耗尽
Karmem允许在同一消息中多次重复使用一个指针/偏移。不幸的是,这种行为使一个简短的消息可以生成比消息大小更广泛的数组。当前,该问题的唯一缓解措施是使用有限阵列而不是数组,而避免对象 - API解码。
数据泄漏
Karmem在编码之前没有清除内存,这可能会从上一个消息或系统内存本身中泄漏信息。如前所述,可以使用@packed(true)标签来解决。 packed标签将从消息中删除填充物,这将防止泄漏。 Alternatively, you can clear the memory before encoding, manually.
与其他序列化库相比,Karmem有一些局限性,例如:
最大尺寸
与Google Protobuf和Google Flatbuffers类似,Karmem的最大尺寸为2GB。这是整个消息的最大大小,而不是每个数组的最大大小。这种限制是由于WASM设计为32位的事实,并且2GB的最大尺寸似乎足以满足当前需求。当前的作者没有执行此限制,但是阅读一条大于2GB的消息将导致不确定的行为。
数组/表格的数组
Karmem不支持阵列或表格数组的数组。但是,如上所述,可以将这些类型包裹在一个内联架构中。该限制是为了利用语言中的本地阵列/切片。大多数语言都封装了指针和阵列的大小类似于结构的指针,这需要每个元素的大小才能知道,从而防止了具有可变大小/步幅的项目数组。
UTF-8
Karmem仅支持UTF-8,并且不支持其他编码。