在这篇文章中,我们将看到如何提高Rust序列化性能。我们将看一个简单的示例,并将其性能提高2.25倍。
在我们的例子中,假设有这样一个结构体:
struct Name {
first_name: String,
last_name: String,
}
我们希望在格式化时使用全名表示(用空格分隔姓和名)。让我们实现Display trait来定义这种表示:
use std::fmt::{self, Display};
impl Display for Name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} {}", self.first_name, self.last_name)
}
}
这个实现允许我们使用println!。例如,打印这个结构体的实例:
fn main() {
let name = Name {
first_name: "Max".to_string(),
last_name: "Mustermann".to_string(),
};
println!("Hello {name}");
// Output: Hello Max Mustermann
}
假设我们有一个Name的向量或切片作为输入(例如,数据库查询的结果)。我们的任务是将其序列化为一个包含全名的JSON向量。
暂停阅读,思考一分钟。你将如何实现这个目标?
简单的方法是将Name转换为全名字符串,然后对其进行序列化:
fn naive(names: &[Name]) -> serde_json::Result<String> {
let full_names = names
.iter()
.map(|name| name.to_string())
.collect::<Vec<_>>();
serde_json::to_string(&full_names)
}
我们迭代Name切片,并使用to_string()方法将Name转换成Display字符串表示形式。然后,然后使用.collect::<Vec<_>>()方法转换成向量,最后序列化它。
这里的问题是我们为每个Name执行to_string()方法时,必须在堆上分配的String,堆分配是昂贵的!
其实,不必在序列化之前创建字符串。我们可以在序列化过程中创建它们,并直接将它们附加到序列化器的缓冲区中,而不是先分配我们自己的缓冲区。
我们可以通过在结构体上面添加#[derived(Serialize)]派生出Serialize for Name的默认实现。
use serde::Serialize;
#[derive(Serialize)]
struct Name {
first_name: String,
last_name: String,
}
但是派生的默认实现会将Name实例序列化为以下JSON对象:
{ "first_name": "Max", "last_name": "Mustermann" }
而我们实际上想要的是,类似于下面的经过序列化的字符串:
"Max Mustermann"
这意味着我们必须手动实现Serialize trait:
use serde::{Serialize, Serializer};
impl Serialize for Name {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.collect_str(&self)
}
}
我们告诉序列化器从实例中“收集”一个字符串。collect_str接受一个参数&T,其中T实现Display并将该Display表示的字符串追加到序列化器。
我们在Display和Serialize trait之间建立了一个桥梁,现在Name类型实现了Serialize,我们可以直接将它的切片传递给Json序列化器:
fn manual_serialize(names: &[Name]) -> serde_json::Result<String> {
serde_json::to_string(names)
}
下面显示了naive与manual_serialize序列化N个Name所花费的时间对比:
serialization fastest │ slowest │ median │ mean │ samples │ iters
├─ ser_manual_serialize │ │ │ │ │
│ ├─ 0 169.3 ms │ 381.3 ms │ 174.7 ms │ 190.7 ms │ 100 │ 100
...........
│ ╰─ 20 97.45 ms │ 182.7 ms │ 102.7 ms │ 109.7 ms │ 100 │ 100
╰─ ser_naive │ │ │ │ │
├─ 0 448.8 ms │ 729.4 ms │ 496.1 ms │ 507.6 ms │ 100 │ 100
...........
╰─ 20 337.3 ms │ 697.9 ms │ 353.1 ms │ 367.5 ms │ 100 │ 100
我们得到了1.25到2.25倍的加速。加速取决于Name的数量N。N的数量越多,加速的趋势越高。
通过在类型Name上直接手动实现Serialize trait, 提高了序列化的性能。但是我们失去了拥有默认派生的能力,即不能使用#[derive(Serialize)]。如果在另一个上下文中需要发送以下JSON对象,我们该怎么办?
{ "first_name": "Max", "last_name": "Mustermann" }
Copyright© 2013-2020
All Rights Reserved 京ICP备2023019179号-8