「C++ 比 Java 快,把循环全换成 C++ 岂不起飞?」
这台后端服务里真的挂了一段 C++ 共享库。别猜了,发车实测。
四条车道依次跑:纯过路费 → Java JIT → C++ 单次过境 → C++ 攒批。每条车道预热后测 5 轮取中位数;车道跑完再把 13 档批量逐档实测成曲线。现场测,不背答案。
| 车道 | 总耗时 | 每次操作 | 对比 Java |
|---|---|---|---|
| 纯过路费(noop) | — | — | — |
| Java JIT 循环 | — | — | 基准 |
| C++ 单次过境 | — | — | — |
| C++ 攒批过境 | — | — | — |
横轴回答的就是「一批是多少」:从一批 1 次、3 次、10 次……一直到整轮一批, 每一档都是后端真测的,不是公式画出来的。纵轴是摊到每次操作的耗时。 发车时那轮「攒批过境」用的是最大档——整轮一批。
编译成共享库挂在后端进程里,Java 25 的 FFM(Foreign Function & Memory API)直接调用。没有任何 mock——你测的就是这段机器码。
#include <cstdint>
extern "C" {
int32_t nb_get_at(const int32_t* arr, int32_t index) {
return arr[index]; // 单次过境:每个元素都要调一次
}
int64_t nb_sum_range(const int32_t* arr, int32_t start, int32_t end) {
int64_t acc = 0;
for (int32_t i = start; i < end; i++) {
acc += arr[i]; // 攒批过境:一次调用,循环在 C++ 里干完
}
return acc;
}
void nb_noop() { // 纯过路费:什么都不干,专量边界开销
}
}
知乎上的问题是:Java 没有值类型和指针,容器天生写不好,为什么不用 C++ 写一套、 JNI 调用?这张实测台就是答案的第一行:数组读取这种纳秒级操作,JIT 编译后的 Java 每次只要不到 1ns,而跨一次语言边界要几十 ns——单次过境慢几十倍。 C++ 的速度优势要先挣回过路费才轮得到赢。
第二行在攒批车道:把 N 次操作攒成一次过境,过路费摊到接近零, C++ 纯计算的优势才释放出来。交叉点 N* = b / (j − v),b 是过路费、 j 和 v 是两侧每次操作的耗时——全部来自你刚才那次实测。 曲线图上 13 档批量逐档真测,直接检验这个公式:实测交叉点应该落在 公式预言的位置附近;如果整条曲线都压不下去,那就是 JIT 连推演的机会都没给。
所以 Java 标准库的选择说得通:集合操作是纳秒粒度,JVM 的正确姿势是让 JIT 把 Java 代码本身编译成机器码(内联、标量替换)、让 intrinsic 把 arraycopy 这类底层操作直接换成向量化指令——代码不过境,才不用交过路费。 真正粗粒度的活(压缩一块 buffer、读写 socket)才值得过境, java.util.zip 和 NIO 就是这么干的。后来 Panama FFM 把过路费降了下来, Valhalla 在给 Java 补值类型——账本的两端都在改,方向依然是语言自己演化。
你看到的数字跑在后端上, 换台机器重跑数字会变,结论的形状不会:单次过境输,攒批反超, 交叉点之前的 C++ 只是台昂贵的跷跷板。