【问题标题】:Why is protobuf 5-10x slower than memcpy for a list of bytes?为什么字节列表的 protobuf 比 memcpy 慢 5-10 倍?
【发布时间】:2026-01-15 12:00:01
【问题描述】:

我有两个性能截然不同的简单代码块:

void testProto() {
  demo::Person* person = new demo::Person();
  person->set_data(data, BUFFER_LEN);
}

void testMemcpy() {
  demo::Person* person = new demo::Person();
  memcpy(memcpy_dest, data, BUFFER_LEN);
}

proto 文件如下所示:

message Person {
  bytes data = 1;
}

根据Protobuf encoding docs,设置长度分隔数据看起来就像复制带有几个标题字节的数据一样简单。为什么第一个函数比第二个函数花费的时间多 5-10 倍?

我制作了一个完整的、易于运行的示例here

附加说明/上下文:

  • Flatbuffers 和 protobufs 的替代品没有这个问题
  • Here's my attempt 使用调试器。我不能低于Set 方法。
  • 这种性能对我来说很重要的原因是我正在将一些高吞吐量/低延迟的网络代码转换为 protobuf。由于我在每个数据包中多次运行上述代码,因此 protobufs 会严重影响性能。
  • 我在 -O3 下运行,但即使在 -O0 下,仍然存在巨大的性能差异
  • 函数调用开销不是问题,因为性能不佳会随着数据的大小而变化。函数调用只是一个恒定的开销。
  • 我尝试了多种方法来确保 memcpy 不会被优化掉(-O0,使用数组)。我非常有信心 memcpy 没有被优化掉。
  • 我在testMemcpy 中尝试了malloc。这让事情变慢了一点,但仍然至少差了 5 倍。
  • 我在 Macbook M1 和 Ubuntu Intel 机器上试过这个

【问题讨论】:

  • 你在使用优化代码吗?
  • 是的,您可以在我链接的示例中看到。我正在运行 -O3。
  • 所有相关细节都需要在问题本身中,而不是在外部链接中
  • memcpy 复制是什么以及复制到哪里?这显然不是demo::Person。也许memcpy 只是因为没有人查看结果而被优化掉了?
  • Protobuf 不仅仅是字节复制。例如,您的 memcpy 已经有一个缓冲区(静态!)要复制到。如果我修改你的基准,那么 memcpy 也必须分配一个存储字节的位置,就像 protobuf 一样,那么差异就会变得更小。尽管如此,proto 还是比 memcpy 慢约 2 倍——或者具体而言,每次迭代慢约 127 微秒。鉴于它还管理一个分配器领域,varint 编码长度,并跟踪其他消息头,这似乎有点合理。如果你的瓶颈是复制单字节缓冲区,protobuf 不是最快的。

标签: c++ protocol-buffers protobuf-c


【解决方案1】:

您的基准测试中的代码无效。程序格式不正确。
如果不是,[as-if rule] 将适用。
调用testMemcpy() 函数和什么都不做之间没有可观察到的 行为差异。 (除了分配无法释放的内存;可以忽略,这是未定义的行为)。

【讨论】:

  • 我们在 cmets 中讨论了这个问题。这是真的,但即使它的格式正确,我们确保testMemcpy 没有被优化,性能差异仍然存在。还有其他事情发生。
  • 我在问题中也提到了这一点:“我尝试了多种方法来确保 memcpy 不会被优化掉(-O0,使用数组)。我非常有信心memcpy 没有被优化掉。”
  • @ViralTaco_ 给定问题中未定义的行为在哪里?发布一个答案说,没有演示,“代码无效”是没有用的。
  • -O0 不会改变标准。行为上没有明显的差异。即使编译器生成程序集并且它确实被执行,并且该程序集是 x86-64,处理器也将能够优化无用的指令。 (但是,这在很大程度上是题外话)。我的观点是:您不能将该代码用作基准。请:0)要么释放内存,要么不动态分配它。 1)实现可观察的行为,这样你就可以测量一些东西。 @GManNickG 的例子是什么?我在最后的括号中解释了原因。这与问题无关。
  • “程序格式错误。”并且“这是未定义的行为”具有实际意义。这个问题对我(和我的编译器)来说是格式正确的,并且不包含我看到的未定义行为。
最近更新 更多