绝大多数项目不该手写二进制序列化框架,因protobuf等成熟方案已覆盖95%高性能场景;手写需自行处理字节序、对齐、变长编码、跨平台浮点一致性等复杂问题,易触发未定义行为。

为什么不该手写二进制序列化框架
绝大多数项目不需要手写——protobuf、flatbuffers、capnproto 已经覆盖了 95% 的高性能场景,且经过大规模验证。手写意味着你要自己处理字节序、对齐填充、变长整数编码(如 zigzag + varint)、嵌套结构的偏移管理、版本兼容性、内存安全边界检查,以及最关键的:跨平台浮点数二进制表示一致性(std::numeric_limits<float>::is_iec559</float> 不是所有平台都为 true)。一旦出现 memcpy(&f, bytes + offset, sizeof(float)) 这类操作,在非 IEEE754 环境或开启严格别名检查(-fstrict-aliasing)时会触发未定义行为。
如果真要手写,必须绕开的三个陷阱
绕不开就等于放弃性能和可维护性:
-
别直接 reinterpret_cast 指针写入:用
std::bit_cast(C++20)或std::memcpy替代*reinterpret_cast<uint32_t>(ptr)</uint32_t>,否则触发 strict aliasing 优化错误 -
别硬编码字段偏移:结构体加
#pragma pack(1)后仍可能因编译器扩展(如__attribute__((aligned)))或调试宏改变布局;应统一用offsetof+ 手动计算,或生成代码(如用 Python 脚本解析 IDL) -
别忽略网络字节序的粒度:TCP 是字节流,不是消息流;
htonl/ntohl只解决 4 字节整数,但double、int128_t、变长字符串长度字段都需要独立字节序处理,且必须与反序列化端完全对称
最小可行骨架:只支持 POD 结构的 flat buffer 写法
假设你只处理无指针、无虚函数、无 padding 缝隙的结构(如 struct Msg { uint32_t id; float x; uint8_t flags; }),可这样组织:
class BinaryWriter {
std::vector<uint8_t> buf_;
public:
template<typename T>
void write(const T& v) {
static_assert(std::is_trivially_copyable_v<T>);
const size_t old_size = buf_.size();
buf_.resize(old_size + sizeof(T));
std::memcpy(buf_.data() + old_size, &v, sizeof(T));
}
// 注意:此处不进行任何字节序转换!
// 调用方必须保证传入的是已调用 htonl/htons 的值
};
关键点:
立即学习“C++免费学习笔记(深入)”;
- 不自动处理字节序 —— 让业务层决定何时转(例如只对网络协议字段转,内部计数器不转)
- 不提供 read
() 泛型反序列化 —— 因为 float和uint32_t占用相同字节数但语义不同,强制 reinterpret 会掩盖类型错误 - 预留
reserve()接口,避免 vector 频繁 realloc 导致缓存失效
真正影响性能的其实是 IO 层,不是序列化本身
实测表明,在千兆网卡 + epoll 模式下,protobuf 序列化耗时通常不到整个请求周期的 3%,瓶颈在 writev 系统调用次数、socket send buffer 大小、Nagle 算法开关(TCP_NODELAY)、以及是否启用零拷贝(如 sendfile 或 io_uring)。所以与其花两周手写序列化,不如确认:SO_SNDBUF 是否足够大、是否批量合并小包、是否用 iovec 减少系统调用、buffer 是否 lock-free 且 cache line 对齐。一个 std::vector<uint8_t></uint8_t> 的 push_back 就可能比 memcpy 多出 2~3 倍 cycles。



















