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標籤將從消息中刪除填充物,這將防止洩漏。另外,您可以在編碼之前先清除內存。
與其他序列化庫相比,Karmem有一些局限性,例如:
最大尺寸
與Google Protobuf和Google Flatbuffers類似,Karmem的最大尺寸為2GB。這是整個消息的最大大小,而不是每個數組的最大大小。這種限制是由於WASM設計為32位的事實,並且2GB的最大尺寸似乎足以滿足當前需求。當前的作者沒有執行此限制,但是閱讀一條大於2GB的消息將導致不確定的行為。
數組/表格的數組
Karmem不支持陣列或表格數組的數組。但是,如上所述,可以將這些類型包裹在一個內聯架構中。該限制是為了利用語言中的本地陣列/切片。大多數語言都封裝了指針和陣列的大小類似於結構的指針,這需要每個元素的大小才能知道,從而防止了具有可變大小/步幅的項目數組。
UTF-8
Karmem僅支持UTF-8,並且不支持其他編碼。