[GH-ISSUE #313] [optimize serialize performance]简化序列化/反序列化代码,消除宏,提升性能 #190

Closed
opened 2026-10-26 09:46:51 +00:00 by bsakaguchi · 4 comments
Owner

Originally created by @qicosmos on GitHub (Jul 16, 2025).
Original GitHub issue: https://github.com/deepseek-ai/3FS/issues/313

动机

目前3fs::serde的序列化/反序列化基于宏实现的编译期反射实现的,这种写法在新版本编译器中是可以大幅简化的,yalantinglibs的序列化库struct_pack 正是基于新的编译期反射实现思路实现的,大幅简化了序列化/反序列化的实现并且比3fs::serde的性能高了将近一个数量级,建议使用struct_pack 来替换3fs::serde来简化代码和提升性能,代码改造的代价也不大。

比较

序列化对象定义的比较

3fs::serde 定义序列化结构体:

struct person : public serde::SerdeHelper<person> {
  SERDE_STRUCT_FIELD(id, int32_t{});
  SERDE_STRUCT_FIELD(name, std::string{});
  SERDE_STRUCT_FIELD(age, int{});
  SERDE_STRUCT_FIELD(salary, double{});
};

struct_pack 定义序列化结构体:

struct person_t {
  int32_t id;
  std::string name;
  int age;
  double salary;
};

都是同样内存布局的结构体,struct_pack的定义不需要任何宏,代码更简洁。

性能比较

对person对象做10万次序列化反序列化看耗时.

测试1

person的定义,测试长字符串和大数字:

  person p;
  p.id = 432798;
  p.name = std::string(1024, 'A');
  p.age = 24;
  p.salary = 65536.42;

benchmark结果:

3fs 序列化耗时:11699562 ns

struct_pack序列化耗时:1210753 ns

3fs 反序列化耗时:3006874 ns

struct_pack反序列化耗时:1520934 ns

序列化速度struct_pack 是3fs的9.6倍;反序列化速度struct_pack是3fs 的1.9倍

测试2

person的定义, 测试短字符串和小数字:

  person p;
  p.id = 1;
  p.name = "tom";
  p.age = 24;
  p.salary = 1024;

benchmark结果:

3fs 序列化耗时:4544700 ns

struct_pack序列化耗时:524934 ns

3fs 反序列化耗时:2343792 ns

struct_pack反序列化耗时:747060 ns

序列化速度struct_pack 是3fs的8.7倍;反序列化速度struct_pack是3fs 的3.1倍

测试3

将person定义中的std::string 类型改成std::string_view 类型,测试零拷贝场景下反序列化的速度。

person初始化同上,两次测试结果如下。

长字符串和大数字:

3fs 反序列化耗时:2505920 ns

struct_pack反序列化耗时:580043 ns

反序列化速度struct_pack是3fs 的4.3倍

短字符串和小数字:

3fs 反序列化耗时:2372563 ns

struct_pack反序列化耗时:381896 ns

反序列化速度struct_pack是3fs 的6.2倍

benchmark 代码

#include <iostream>
#include <chrono>
#include <ylt/struct_pack.hpp>
#include "common/serde/Serde.h"

class ScopedTimer {
 public:
  ScopedTimer(const char *name)
      : m_name(name),
        m_beg(std::chrono::high_resolution_clock::now()) {}
  ~ScopedTimer() {
    auto end = std::chrono::high_resolution_clock::now();
    auto dur = std::chrono::duration_cast<std::chrono::nanoseconds>(end - m_beg);

    std::cout << m_name << " : " << dur.count() << " ns\n";
  }

 private:
  const char *m_name;
  std::chrono::time_point<std::chrono::high_resolution_clock> m_beg;
};

void run_3fs_bench(const person &p) {
  hf3fs::serde::Out<DownwardBytes<hf3fs::net::Allocator<>>> out0;
  serde::serialize(p, out0);
  std::cout << "3fs size: " << out0.bytes().size() << "\n";
  person p1{};
  serde::deserialize(p1, std::string_view(out0.bytes()));
  out0.bytes().reset();

  size_t count = 100000;
  {
    ScopedTimer timer("3fs serialize");
    for (size_t i = 0; i < count; i++) {
      out0.bytes().reset();
      serde::serialize(p, out0);
    }
  }

  {
    ScopedTimer timer("3fs deserialize");
    for (size_t i = 0; i < count; i++) {
      serde::deserialize(p1, std::string_view(out0.bytes()));
    }
  }
}

void run_struct_pack_bench(const person_t &p) {
  std::string buffer;
  struct_pack::serialize_to(buffer, p);
  std::cout << "struct_pack size: " << buffer.size() << "\n";
  person_t p1{};
  struct_pack::deserialize_to(p1, buffer);

  size_t count = 100000;
  {
    ScopedTimer timer("stp serialize");
    for (size_t i = 0; i < count; i++) {
      buffer.clear();
      struct_pack::serialize_to(buffer, p);
    }
  }

  {
    ScopedTimer timer("stp deserialize");
    for (size_t i = 0; i < count; i++) {
      struct_pack::deserialize_to(p1, buffer);
    }
  }
}

void bench() {
  person p;
  p.id = 432798;
  p.name = std::string(1024, 'A');
  p.age = 24;
  p.salary = 65536.42;
  run_3fs_bench(p);

  person_t p1{432798, std::string(1024, 'A'), 24, 65536.42};
  run_struct_pack_bench(p1);

  std::cout << "small object\n";
  p = {};
  p.id = 1;
  p.name = "tom";
  p.age = 24;
  p.salary = 1024;
  run_3fs_bench(p);

  p1 = {1, "tom", 24, 1024};
  run_struct_pack_bench(p1);
}

int main(int argc, char **argv) {
  bench();
  return 0;
}

替换序列化库的工作量

  1. 修改序列化对象的定义

把宏定义去掉,定义成普通结构体即可;

2.序列化和反序列化接口稍作修改即可

3.json 部分的改造
可以使用struct_json去将对象序列化成json字符串,对象还是一个普通的c++结构体即可,这个结构体即可以转换为struct_pack格式也可以转换为json或者xml格式,类似于:

person_t p1{432798, std::string(1024, 'A'), 24, 65536.42};
std::string json_str;
struct_json::to_json(p1, json_str);

person_t p2;
struct_json::from_json(p2, json_str);
Originally created by @qicosmos on GitHub (Jul 16, 2025). Original GitHub issue: https://github.com/deepseek-ai/3FS/issues/313 # 动机 目前3fs::serde的序列化/反序列化基于宏实现的编译期反射实现的,这种写法在新版本编译器中是可以大幅简化的,[yalantinglibs](https://github.com/alibaba/yalantinglibs)的序列化库struct_pack 正是基于新的编译期反射实现思路实现的,大幅简化了序列化/反序列化的实现并且比3fs::serde的性能高了将近一个数量级,建议使用struct_pack 来替换3fs::serde来简化代码和提升性能,代码改造的代价也不大。 # 比较 ## 序列化对象定义的比较 3fs::serde 定义序列化结构体: ```cpp struct person : public serde::SerdeHelper<person> { SERDE_STRUCT_FIELD(id, int32_t{}); SERDE_STRUCT_FIELD(name, std::string{}); SERDE_STRUCT_FIELD(age, int{}); SERDE_STRUCT_FIELD(salary, double{}); }; ``` struct_pack 定义序列化结构体: ```cpp struct person_t { int32_t id; std::string name; int age; double salary; }; ``` 都是同样内存布局的结构体,struct_pack的定义不需要任何宏,代码更简洁。 ## 性能比较 对person对象做10万次序列化反序列化看耗时. ### 测试1 person的定义,测试长字符串和大数字: ```cpp person p; p.id = 432798; p.name = std::string(1024, 'A'); p.age = 24; p.salary = 65536.42; ``` **benchmark结果:** 3fs 序列化耗时:11699562 ns struct_pack序列化耗时:1210753 ns 3fs 反序列化耗时:3006874 ns struct_pack反序列化耗时:1520934 ns 序列化速度struct_pack 是3fs的9.6倍;反序列化速度struct_pack是3fs 的1.9倍 ### 测试2 person的定义, 测试短字符串和小数字: ```cpp person p; p.id = 1; p.name = "tom"; p.age = 24; p.salary = 1024; ``` **benchmark结果:** 3fs 序列化耗时:4544700 ns struct_pack序列化耗时:524934 ns 3fs 反序列化耗时:2343792 ns struct_pack反序列化耗时:747060 ns 序列化速度struct_pack 是3fs的8.7倍;反序列化速度struct_pack是3fs 的3.1倍 ### 测试3 将person定义中的std::string 类型改成std::string_view 类型,测试零拷贝场景下反序列化的速度。 person初始化同上,两次测试结果如下。 **长字符串和大数字:** 3fs 反序列化耗时:2505920 ns struct_pack反序列化耗时:580043 ns 反序列化速度struct_pack是3fs 的4.3倍 **短字符串和小数字:** 3fs 反序列化耗时:2372563 ns struct_pack反序列化耗时:381896 ns 反序列化速度struct_pack是3fs 的6.2倍 ### benchmark 代码 ```cpp #include <iostream> #include <chrono> #include <ylt/struct_pack.hpp> #include "common/serde/Serde.h" class ScopedTimer { public: ScopedTimer(const char *name) : m_name(name), m_beg(std::chrono::high_resolution_clock::now()) {} ~ScopedTimer() { auto end = std::chrono::high_resolution_clock::now(); auto dur = std::chrono::duration_cast<std::chrono::nanoseconds>(end - m_beg); std::cout << m_name << " : " << dur.count() << " ns\n"; } private: const char *m_name; std::chrono::time_point<std::chrono::high_resolution_clock> m_beg; }; void run_3fs_bench(const person &p) { hf3fs::serde::Out<DownwardBytes<hf3fs::net::Allocator<>>> out0; serde::serialize(p, out0); std::cout << "3fs size: " << out0.bytes().size() << "\n"; person p1{}; serde::deserialize(p1, std::string_view(out0.bytes())); out0.bytes().reset(); size_t count = 100000; { ScopedTimer timer("3fs serialize"); for (size_t i = 0; i < count; i++) { out0.bytes().reset(); serde::serialize(p, out0); } } { ScopedTimer timer("3fs deserialize"); for (size_t i = 0; i < count; i++) { serde::deserialize(p1, std::string_view(out0.bytes())); } } } void run_struct_pack_bench(const person_t &p) { std::string buffer; struct_pack::serialize_to(buffer, p); std::cout << "struct_pack size: " << buffer.size() << "\n"; person_t p1{}; struct_pack::deserialize_to(p1, buffer); size_t count = 100000; { ScopedTimer timer("stp serialize"); for (size_t i = 0; i < count; i++) { buffer.clear(); struct_pack::serialize_to(buffer, p); } } { ScopedTimer timer("stp deserialize"); for (size_t i = 0; i < count; i++) { struct_pack::deserialize_to(p1, buffer); } } } void bench() { person p; p.id = 432798; p.name = std::string(1024, 'A'); p.age = 24; p.salary = 65536.42; run_3fs_bench(p); person_t p1{432798, std::string(1024, 'A'), 24, 65536.42}; run_struct_pack_bench(p1); std::cout << "small object\n"; p = {}; p.id = 1; p.name = "tom"; p.age = 24; p.salary = 1024; run_3fs_bench(p); p1 = {1, "tom", 24, 1024}; run_struct_pack_bench(p1); } int main(int argc, char **argv) { bench(); return 0; } ``` # 替换序列化库的工作量 1. 修改序列化对象的定义 把宏定义去掉,定义成普通结构体即可; 2.序列化和反序列化接口稍作修改即可 3.json 部分的改造 可以使用struct_json去将对象序列化成json字符串,对象还是一个普通的c++结构体即可,这个结构体即可以转换为struct_pack格式也可以转换为json或者xml格式,类似于: ```cpp person_t p1{432798, std::string(1024, 'A'), 24, 65536.42}; std::string json_str; struct_json::to_json(p1, json_str); person_t p2; struct_json::from_json(p2, json_str); ```
Author
Owner

@SF-Zhou commented on GitHub (Jul 17, 2025):

Thank you for your attention.

  1. For compatibility reasons, the serialization protocol will not be changed lightly.
  2. Serialization/deserialization is not a performance hotspot, and the benefits of optimization are very limited.
  3. 3fs::serde prioritizes reducing serialized data size (even at an additional cost) to minimize network transmission time. You can verify the actual serialized length.
  4. Additionally, I believe there are areas for improvement in the benchmarks
    • out0.bytes().reset() releases the internal buffer, while buffer.clear() does not, making this comparison unfair.
    • std::string benefits from SSO (Small String Optimization), which gives it an advantage at very small serialized lengths.
<!-- gh-comment-id:3082026466 --> @SF-Zhou commented on GitHub (Jul 17, 2025): Thank you for your attention. 1. For compatibility reasons, the serialization protocol will not be changed lightly. 2. Serialization/deserialization is not a performance hotspot, and the benefits of optimization are very limited. 3. `3fs::serde` prioritizes reducing serialized data size (even at an additional cost) to minimize network transmission time. You can verify the actual serialized length. 4. Additionally, I believe there are areas for improvement in the benchmarks - `out0.bytes().reset()` releases the internal buffer, while `buffer.clear()` does not, making this comparison unfair. - `std::string` benefits from SSO (Small String Optimization), which gives it an advantage at very small serialized lengths.
Author
Owner

@qicosmos commented on GitHub (Jul 17, 2025):

谢谢回复。

第一点我就不做评论了。

第二点我其实想说的是现在的实现方法比较繁琐复杂,可以简化实现,至少可以把宏消除掉。

第三点我测了,短字符串和长字符串,序列化长度二者是相当的:长字符串(1024)时3fs的size是1044,struct_pack是1047,相差3个字节而已;短字符串(sso),3fs是21字节,struct_pack是24字节,相差3字节。
第四点,我不知道怎么去优化这个buffer,怎么去复用这个buffer呢?你可以给一个建议。

关于sso,我实际上测了非sso的场景(字符串长度1024)。我希望可以把内存的开销完全避免掉,只测序列化反序列化的开销。

<!-- gh-comment-id:3082077846 --> @qicosmos commented on GitHub (Jul 17, 2025): 谢谢回复。 第一点我就不做评论了。 第二点我其实想说的是现在的实现方法比较繁琐复杂,可以简化实现,至少可以把宏消除掉。 第三点我测了,短字符串和长字符串,序列化长度二者是相当的:长字符串(1024)时3fs的size是1044,struct_pack是1047,相差3个字节而已;短字符串(sso),3fs是21字节,struct_pack是24字节,相差3字节。 第四点,我不知道怎么去优化这个buffer,怎么去复用这个buffer呢?你可以给一个建议。 关于sso,我实际上测了非sso的场景(字符串长度1024)。我希望可以把内存的开销完全避免掉,只测序列化反序列化的开销。
Author
Owner

@qicosmos commented on GitHub (Jul 17, 2025):

另外,我理解现在的序列化其实是没法做类型安全校验的对吧,非法二进制可能也会成功反序列化。struct_pack是做了类型校验的,不会允许这种情况发生。anyway,只是提一个建议。

<!-- gh-comment-id:3082082088 --> @qicosmos commented on GitHub (Jul 17, 2025): 另外,我理解现在的序列化其实是没法做类型安全校验的对吧,非法二进制可能也会成功反序列化。struct_pack是做了类型校验的,不会允许这种情况发生。anyway,只是提一个建议。
Author
Owner

@acking-you commented on GitHub (Jul 25, 2025):

我感觉如果扣这个的话,就没必要自己实现了,交给比较活跃的第三方库即可,同样基于编译期反射的序列化库实现,我可能会选择直接用 glaze ,不过这个得开 cpp23,

<!-- gh-comment-id:3116947552 --> @acking-you commented on GitHub (Jul 25, 2025): 我感觉如果扣这个的话,就没必要自己实现了,交给比较活跃的第三方库即可,同样基于编译期反射的序列化库实现,我可能会选择直接用 [glaze](https://github.com/stephenberry/glaze/tree/main) ,不过这个得开 cpp23,
Sign in to join this conversation.
No labels
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
deepseek-ai/3FS#190
No description provided.