I'm currently implementing an existing network protocol in rust. Enums seemed like a perfect fit to represent the possible packet types. To correctly serialize them, serde has to be told to serialize them untagged.
So far so good, serializing and deserializing works even with other implementations, the actual protocol requires cbor encoding but for debugging purposes I can easily switch to json, thanks to serde this works with only very minimal code changes.
While benchmarking my implementation I realized that deserialization was painfully slow. After some fiddling with my source I learned that the order within the enum is responsible for the slowdown. Hence, I sorted them by probability to speed up everything 6x in the average cases.
This still doesn't feel right and probably is not very idiomatic.
A simple rust program that behaves similarly is the following:
use serde::{Deserialize, Serialize};
use std::io::stdout;
use std::io::Write;
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(untagged)] // Order of probable occurence, serde tries decoding in untagged enums in this order
enum PacketVariants {
Hello(u32, u8, String),
Bye(u32, String),
Bla(String),
Blub(u32),
}
fn to_cbor(p: &PacketVariants) -> Vec<u8> {
serde_cbor::to_vec(p).expect("Error serializing packet as cbor.")
}
fn to_json(p: &PacketVariants) -> String {
serde_json::to_string(p).expect("Error serializing packet as json.")
}
fn deserialize_packet_cbor(runs: i64, buf: Vec<u8>) {
print!("Loading {} packets: \t", runs);
stdout().flush().unwrap();
use std::time::Instant;
let bench_now = Instant::now();
for _x in 0..runs {
let p: PacketVariants = serde_cbor::from_slice(&buf).expect("Decoding packet failed");
}
let elapsed = bench_now.elapsed();
let sec = (elapsed.as_secs() as f64) + (f64::from(elapsed.subsec_nanos()) / 1_000_000_000.0);
println!("{} packets/second", (runs as f64 / sec) as i64);
}
fn deserialize_packet_json(runs: i64, buf: String) {
print!("Loading {} packets: \t", runs);
stdout().flush().unwrap();
use std::time::Instant;
let bench_now = Instant::now();
for _x in 0..runs {
let p: PacketVariants = serde_json::from_str(&buf).expect("Decoding packet failed");
}
let elapsed = bench_now.elapsed();
let sec = (elapsed.as_secs() as f64) + (f64::from(elapsed.subsec_nanos()) / 1_000_000_000.0);
println!("{} packets/second", (runs as f64 / sec) as i64);
}
fn main() {
let hello = PacketVariants::Hello(1, 3, "AAA".into());
let bye = PacketVariants::Bye(2, "AAA".into());
let bla = PacketVariants::Bla("AAA".into());
let blub = PacketVariants::Blub(3);
// CBOR tests
let p1 = to_cbor(&hello);
let p2 = to_cbor(&bye);
let p3 = to_cbor(&bla);
let p4 = to_cbor(&blub);
println!("{:02x?}", p1);
println!("{:02x?}", p2);
println!("{:02x?}", p3);
println!("{:02x?}", p4);
deserialize_packet_cbor(100_000, p1);
deserialize_packet_cbor(100_000, p2);
deserialize_packet_cbor(100_000, p3);
deserialize_packet_cbor(100_000, p4);
// JSON tests
let p1 = to_json(&hello);
let p2 = to_json(&bye);
let p3 = to_json(&bla);
let p4 = to_json(&blub);
println!("{:?}", p1);
println!("{:?}", p2);
println!("{:?}", p3);
println!("{:?}", p4);
deserialize_packet_json(100_000, p1);
deserialize_packet_json(100_000, p2);
deserialize_packet_json(100_000, p3);
deserialize_packet_json(100_000, p4);
}
These dependencies are required:
serde = { version = "1.0", features = ["derive"] }
serde_derive = "1"
serde_cbor = "0.9"
serde_json = "1.0"
And here the cbor benchmark output:
Loading 100000 packets: 3192034 packets/second
Loading 100000 packets: 1094580 packets/second
Loading 100000 packets: 693023 packets/second
Loading 100000 packets: 594142 packets/second
Each line corresponds to one of the packet variants.
Is my only alternative writing a manual packet parser and not using serde? Is there a way to make serde parsing more intelligent so that it looks ahead if another field is coming or if the type fits one of the expected ones? I guess the biggest problem for serde is that, for example, json doesn't provide any useful information regarding array lengths or type information whereas cbor or msgpack usually provide vague type information or the length of an array. Should solutions for this be implemented in say serde_cbor
?
On the other hand, maybe I am just too new to rust and missing something really obvious to speed up my program...