如何提高 Rust 序列化性能?- 2
在上一篇文章中,我們手動實現 Serde 庫中的 Serialize trait, 提高了序列化的性能。但是不能使用默認的#[derive(Serialize)] 功能,在這一篇文章中,我們來解決這個問題,使這兩種情景可以兼容。
格式化器
我們不要在數據類型上直接手動實現 Serialize trait。相反,應該在類似格式化器的封裝類型上實現它。
這裏有一個例子:
struct DisplayFormatter<T: Display>(T);
impl<T: Display> Serialize for DisplayFormatter<T> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.collect_str(&self.0)
}
}
此泛型格式器封裝了實現 Display 的類型 T,並使用該表示對 T 進行序列化。你不僅可以在 Name 類型上使用它,還可以在任何實現 Display 的類型上使用它。
但是,有時你的類型並不一定要實現 Display,或者你想要不同的 Display 和 Serialize 實現。在這種情況下,可以使用一個封裝具體類型的格式化器:
struct FullNameFormatter<'a>(&'a Name);
impl<'a> Serialize for FullNameFormatter<'a> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.collect_str(&format_args!("{} {}", self.0.first_name, self.0.last_name))
}
}
這裏,我們直接使用 format_args! 宏,像 println! 宏和 format! 宏在底層使用的就是 format_args! 宏。它返回 Arguments,重要的是 Arguments 實現了 collect_str() 方法所需的 Display。
現在,當我們想要序列化 Name 時,我們可以使用該格式化器:
fn formatter(names: &[Name]) -> serde_json::Result<String> {
let full_names = names.iter().map(FullNameFormatter).collect::<Vec<_>>();
serde_json::to_string(&full_names)
}
我們將每個 & Name 映射到 FullNameFormatter(&Name),並將映射收集到一個向量中,然後將該向量傳遞給 serde_json 進行序列化。
查看基準測試結果,可以看到該方法與之前的方法幾乎具有相同的性能:
serialization fastest │ slowest │ median │ mean │ samples │ iters
├─ ser_formatter │ │ │ │ │
│ ├─ 0 282.2 ms │ 502 ms │ 294 ms │ 312.4 ms │ 100 │ 100
..........
│ ╰─ 20 99.15 ms │ 222.2 ms │ 110.4 ms │ 116.2 ms │ 100 │ 100
╰─ ser_manual_serialize │ │ │ │ │
├─ 0 172 ms │ 299.8 ms │ 175.6 ms │ 184 ms │ 100 │ 100
..........
╰─ 20 95.45 ms │ 127.9 ms │ 99.34 ms │ 100.7 ms │ 100 │ 100
幾乎沒性能差異,因爲封裝器類型在 Rust 中是零成本抽象。
但實際上,應該有一個與封裝器類型本身無關的額外成本。在上面的測試結果中應該會看到,對於低 N 值,此方法的性能略低於 manual_serialize
這是因爲有一次收集 map 數據並進行 Vec 的分配!這種分配幾乎沒有出現在基準測試結果中,特別是對於較高的 N 值。這是因爲它只執行一次,與序列化本身相比,它的開銷可以忽略不計。
接下來,我們將取消 Vec 分配,雖然這似乎是一個不必要的優化,但至少可以看到另一個格式化程序示例。
序列格式化器
我們不能跳過收集 map 數據並將 Iterator 傳遞給 serde_json 序列化器的過程,因爲 Serde 庫不直接支持 Iterator 的序列化。Serialize trait 沒有被 Iterator 實現,但是 Serde 的 Serializer 提供了 collect_seq 方法來收集 Iterator。
我們需要一個封裝器類型,它接受 slice 並通過將 map 後的數據傳遞給 collect_seq 來進行序列化:
struct FullNameSequenceFormatter<'a>(&'a [Name]);
impl<'a> Serialize for FullNameSequenceFormatter<'a> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.collect_seq(self.0.iter().map(FullNameFormatter))
}
}
現在我們可以將序列格式化器傳遞給 serde_json:
fn sequence_formatter(names: &[Name]) -> serde_json::Result<String> {
serde_json::to_string(&FullNameSequenceFormatter(names))
}
我們再來看看基準測試結果:
serialization fastest │ slowest │ median │ mean │ samples │ iters
├─ ser_manual_serialize │ │ │ │ │
│ ├─ 0 170.6 ms │ 358.9 ms │ 185.8 ms │ 193.5 ms │ 100 │ 100
............
│ ╰─ 20 97.34 ms │ 143.5 ms │ 101.2 ms │ 106.2 ms │ 100 │ 100
╰─ ser_sequence_formatter │ │ │ │ │
├─ 0 172.8 ms │ 225.5 ms │ 183.3 ms │ 186.7 ms │ 100 │ 100
............
╰─ 20 97.89 ms │ 152.6 ms │ 99.4 ms │ 102 ms │ 100 │ 100
這纔是真正的沒有性能差異!
總結
“避免分配” 是這兩篇文章的真正結論,更具體地說,應該是避免在序列化之前分配數據的中間狀態。
我們已經看到,Serialize Trait 的簡單手工實現可以帶來很大的性能改進。但這並不意味着應該總是手動實現 Serialize Trait,只有需要自定義序列化時才應該手動實現。否則,只需在類型上使用#[derive(Serialize)] 即可。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/v0wy72Li5UT60OtJw3ozZg