如何提高Rust序列化性能?- 1

466次阅读  |  发布于4月以前

在这篇文章中,我们将看到如何提高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