CGO 开销与原生 RDMA 的差距
「Go 经 cgo 调用 libibverbs,所以一定比 C 慢」——这是个流传很广、但被实测推翻的直觉。 本节用同机同口径的实测数据说明:cgo 确实有每次调用的固定开销,但它既不大、也不是 gordma 与原生 perftest 差距的主因;Go 写的工具在状态好时能打满线速、追平 C。
实测:Go 能追平 C
64KB 消息、-n 1000000,三个 Send/Recv 带宽工具换算到同一单位后的最好成绩:
| 工具 | 语言/路径 | 峰值吞吐 | 峰值消息率 |
|---|---|---|---|
ib_send_bw | C(原生 verbs) | ~392 Gb/s | 0.747 Mpps |
go_send_bw | Go + cgo(perftest 包) | ~392 Gb/s | 0.747 Mpps |
go_rdmanet_bw --raw | Go + cgo(RawConn) | ~232 Gb/s | 0.443 Mpps |
go_send_bw 在状态好时跑出 0.747 Mpps,和 C 的 ib_send_bw 完全一致。
这直接说明:Go + cgo 不存在「追不上 C」的固有差距。早先看到的「go_send_bw 只有 ~314」是机器状态差时的数,不是 cgo 的锅。
ib_send_bw 是 MiB/s(2²⁰)、
go_send_bw 是 MB/s(10⁶)、旧版 --raw 曾用 Gb/s(10⁹ bit)。直接比原始数会得出完全错误的结论
(现在 --raw 已统一成 MiB/s,详见 go_rdmanet_bw 详解)。
cgo 开销有多大:实测拆解
用 GORDMA_PROBE=1 把发送循环的时间拆成提交 WR(post)和忙等完成(poll)两段。
同机交替跑,两个 Go 工具的 post 成本几乎一模一样:
| 工具 | post 占比 | post 每 WR | poll 占比 |
|---|---|---|---|
go_send_bw | ~15% | ~300 ns | ~58% |
RawConn | ~13% | ~310 ns | ~59% |
一次 C.ibv_post_send(...) 含 cgo 跨界约 300ns(栈切换 + 调度器协调 + 调用本身)。
但它只占总时间 ~15%,而且两个工具完全相同。换句话说:
- cgo 提交开销是真实存在的(每 WR ~300ns),但占比小;
- 它不是 RawConn 慢于
go_send_bw的原因——两者的提交成本一样。
那差距到底在哪:poll(完成到达)
把 poll 段换成每 WR 耗时,真相就出来了:
go_send_bw poll : 0.75 ~ 1.33 µs/WR # 剧烈波动
RawConn poll : ~1.40 µs/WR(三次纹丝不动)
poll 是忙轮询等网卡把完成写回 CQ,poll 时间短=网卡实际吞吐快。关键观察:
go_send_bw的 poll 在 0.75~1.33µs 间大幅跳:状态好时 0.75µs→打满线速 0.747 Mpps,状态差时 1.33µs→掉到 0.449 Mpps。RawConn的 poll 死死钉在 ~1.40µs:三次几乎不动,消息率稳定封顶在 ~0.42 Mpps。
所以「RawConn 比 perftest 慢 1.7 倍」不是一个固定结论:同窗交替实测中,两者差距在
1.05× ~ 1.76× 之间晃动,某些轮次(go_send 状态差时)几乎持平。
差距大小取决于 go_send_bw 那次能不能让网卡跑满,而非一个稳定的库缺陷。
结论:cgo 不是主角
- Go vs C:没有固有差距。
go_send_bw实测能追平ib_send_bw(同为 0.747 Mpps)。 - 提交路径(post):cgo 每 WR ~300ns、占比 ~15%,两个 Go 工具相同——不解释任何差距。
- 曾经待查、现已查明:RawConn 的完成到达(poll)被稳定压在 ~1.40µs/WR,而
go_send_bw偶尔能降到 0.75µs。 两者 QP/CQ 创建、txDepth、MTU、收发循环结构逐行核对一致——真凶不在代码里,而是 Go 的信号式异步抢占 周期性切碎忙轮询自旋,详见下一节 异步抢占与抖动。 - 机器抖动 vs 抢占:共享 GPU 主机的 CPU 调频确有噪声,但「忽高忽低」的主因其实是上面那个异步抢占——
下一节用
GORDMA_PROBE证伪了「锁核 / NUMA」假设(taskset到任意核都一样),并给出一行环境变量就能稳定打满线速的修复。
cgo 仍值得优化的地方
虽然 cgo 不是这里的瓶颈,但「减少穿越次数」在小包高频场景仍然重要——那时每 WR 搬的字节少, ~300ns 的固定提交开销占比会飙升:
- 批量提交(
PostSendBatch/PipelineBatch):C 侧 WR 链表把 N 个请求一次ibv_post_send提交,把过路费从「每 WR」降到「每批」。小包消息率可提升约一个数量级。 - 忙轮询(
PollBusy):避免事件驱动每次唤醒一次系统调用,热路径直接读 CQ 内存。 - 复用 scratch / 预注册内存:
PostSend用每 QP 预分配的 C 侧 SGE 数组、零拷贝SendBuffer省 bounce 拷贝,压低每 WR 的 CPU 成本。
libffi 行不行
不行,libffi 只会更慢。它在运行时读类型描述符、动态拼调用栈帧(反射式调用),比 cgo 编译期
生成的静态 wrapper 慢;而且「栈切换 + 调度器协调」那道栅栏它一样省不掉。libffi/purego 的价值是
免 C 工具链、纯 Go 交叉编译,不是性能。而且如上所述——提交路径本就不是瓶颈,换它也没用。
GORDMA_PROBE=1 看 post/poll 拆分;小包场景用批量 + 忙轮询把
穿越次数压下去即可。