
先说结论在同等编译优化水平下C的运行速度并不比C慢很多场景下C甚至更快。这个结论可能和很多人脑子里的刻板印象完全相反但如果你愿意把“语言特性”和“实际生成的机器码”分开看就容易理解了。我见过不少从C转C或者刚接触C的开发者都会有类似的疑虑“C那么多特性类、虚函数、模板、异常一个个听上去都是运行时开销怎么可能不慢”但实测做完之后往往发现差距不在语言本身而在写代码的人怎么用。这篇文章就把“C相比C的运行速度”这个问题彻底拆开讲一遍包含原理、实测方法、常见误区以及我在实际项目中总结出来的几条性能经验。1. 先立靶子为什么大家总觉得C比C慢1.1 刻板印象从哪来——语法层面的“重”C的语法在视觉上确实比C重。C语言里一个函数调用的样子大概就是foo(x, y)但C里你可能看到obj.method(x, y)、templatetypename T、std::vectorint这类写法。人在评估性能时天然会受“看得见的复杂度”影响——你看到的东西越多越觉得它慢。再加上早期的C编译器并不成熟确实存在过“类封装之后性能下降”的时代。那时候的C编译器对对象模型的处理还很粗糙构造函数、析构函数、拷贝操作动不动就内联出大量额外指令对比C的裸结构体加函数差距确实存在。这个历史背景让“C慢”的观念一直传到现在。另一个因素是从其他语言迁移过来的视角问题。如果你刚从Python、Java这类带有大量动态分派和运行时托管语义的语言切到C会觉得C“已经很底层了”但如果是从C切过来会觉得C“包裹了一层东西”。底层开发者天然更信任裸指针、裸内存和直白的函数调用。1.2 真正决定性能的不是语言是生成的代码语言本身没有速度编译器生成的机器码才有速度。CPU执行的是二进制指令不是C代码也不是C代码。所以讨论“C相比C的运行速度”本质上要讨论的是同样的逻辑用两种语言描述之后编译器各自生成什么样的汇编。C相比C增加了大量“语法糖”和“编译期机制”但关键点在于除了少数必须付出运行时代价的特性之外大多数C特性在生成机器码时会被编译器剥掉。比如std::vectorint在开启优化之后底层操作和手写的int *arrsizecapacity结构体操作几乎没有区别。模板类本质上就是编译器在编译期生成的一块定制代码和使用手写C代码在内存布局上没有差异。所以我认为在评估速度之前首先要建立正确的模型C和C是“同一层”的语言都直接编译为本地代码中间没有解释器、虚拟机或者运行时垃圾收集。它们之间的性能差异不可能是数量级的差异只可能是具体使用方式带来的微小差距。这里的“微小”在个别热路径上会被放大但在整体系统层面通常可以忽略。2. 零成本抽象到底是不是真的零成本2.1 没代价的抽象模板、常量表达式、引用C的设计哲学里有一条著名的“零成本抽象”zero-overhead abstraction意思是你不用某个特性就不用为它付费你用了某个特性它生成的代码不会比你手动写底层的版本更差。虽然这个口号在细节上不完全成立但大部分核心特性确实做到了。模板就是典型的“编译期替你写代码”机制。模板实例化发生在编译期编译完成之后模板类/函数已经被展开成针对你传入类型的具体代码。这相当于你手动为每种类型写了一份专用实现但编译器替你自动完成。比如std::sort在实例化之后就是一个针对特定类型和特定比较逻辑的定制函数和手写一个专门的排序函数没有性能差别。引用则本质上就是指针的语法替身在生成的代码里就是一个地址和一个C指针的成本完全相同。常量表达式constexpr更极端它直接在编译期把结果算好运行时连计算都省了。这在C语言里你得靠宏或者外部脚本做到。很多C用户担心C的类和构造函数会带来额外开销但如果你看开启-O2之后的汇编代码一个简单的结构体加构造函数和C语言里的结构体初始化生成的东西一模一样——就是几条mov指令。你为抽象写的代码在优化之后被折叠掉了。2.2 有代价的特性虚函数、继承、异常诚实地说有一部分C特性确实有运行时成本且无法通过优化完全消除。最典型的就是虚函数。只要有虚函数每个对象里就要存一个虚表指针每次虚调用都需要“取指针 → 查虚表 → 间接跳转”三步。相比普通函数调用的直接跳转这多了一次内存访问和一次间接跳转在热循环里会明显影响指令流水线和分支预测。但注意虚函数的开销只在“运行时多态调用”的场景出现。如果你不需要多态完全可以用final标记类、或者干脆用模板替代虚函数编译器就能把调用去虚拟化devirtualize直接静态绑定这时候成本又归零了。异常处理也常被认为是C慢的主要原因之一。在大多数主流编译器实现里异常采用“零成本异常处理”zero-cost EH模型正常执行路径上不插入任何检查代码只在函数表里记录可能抛异常的位置和对应的处理代码。也就是说只要你不抛出异常正常运行路径上一分钱都不花一旦真正抛异常运行时才会去查表做栈展开那是另一套相対较慢的路径但它只在出错时触发。所以异常对“正常速度”几乎没有影响影响的是“错误处理速度”和代码体积。2.3 编译器优化如何改变“成本结构”C相对C的一个隐藏优势是类型信息更丰富编译器能做更激进的优化。C语言在很多场景下只能通过指针和基本类型传递数据优化器对数据的流动方式约束较少反而导致一些优化无法安全执行。C的类型系统和访问控制加上引用语义和const限定给优化器提供了更多“可以假定不变”的条件。举一个我实际遇到的例子一段图像处理代码用C写时因为到处都是指针编译器无法确定某个函数是否修改了外部变量于是每次循环都会重新读内存用C写时通过引用传递并且加上const修饰编译器确定数据不会变把关键计算全部放进寄存器。同样逻辑C版本在特定编译器版本上跑出了接近一排的速度差距。虽然这不是必然结果但说明了C在工程实践中由于表达更精确优化器更容易找到优化路径。前提是你别用一堆让优化器晕头转向的动态特性和复杂对象图。3. 用基准测试和数据说话3.1 测试的姿势不对结论就是错的讨论C和C性能时如果只拿“某个群友的某某程序”当证据基本没有参考价值。因为性能比较里最容易被忽略的是编译选项和测试方法的差异。我见过很多人拿C程序开-O2拿C程序不开优化就加std::vector然后得出“C慢了三倍”的结论这属于测试姿势错误不能代表语言本身。正确的对比应该满足这么几个条件同一套算法逻辑两边不能一边用高度优化的手写循环一边用朴素STL容器。相同的编译选项至少都用-O2或者都用-O3才公平。相同的输入规模和运行环境在同一个机器上跑多次取中位数。排除启动开销把核心计算循环跑足够多次不要测进程启动、IO这些东西。我在自己的Linux环境上大量做过这类对比。常见的结果是两者最终性能差异在 ±5% 以内在噪声范围内个别C场景因为底层容器的缓存友好性和编译期内联比C手写版本快10%~20%反过来也有C版本因为用了虚函数或者频繁shared_ptr分配而比C慢20%但那是特性误用不是语言本身的锅。3.2 典型对照组测试与结论我列几个具有代表性的对照场景供参考测试场景C实现思路C实现思路实测结论数组累加/循环计算裸指针 forstd::vector 范围for性能几乎一致开启优化后汇编接近排序大量整数qsort 函数指针std::sort lambdaC通常领先20%~30%因为函数指针比较无法内联而lambda可以直接内联动态字符串拼接手动reallocmemcpystd::string/appendC略快因为std::string会按容量增长策略减少realloc次数多态图形计算结构体 类型字段 if虚函数 继承开启优化后可持平保守场景虚函数稍慢3%~8%频繁小对象创建malloc/freeshared_ptrC明显慢但这是因为shared_ptr有引用计数原子操作正确做法是换unique_ptr或对象池其中排序场景最值得展开说说。C的qsort接收函数指针每次比较都要通过函数指针间接跳转编译器无法把比较逻辑内联进排序内部C 的std::sort接收的是模板参数比较函数尤其是lambda在编译期就内联进排序函数每趟比较都是一条简单的指令没有跳转、没有间接寻址。这一点在几百万个整数时差距非常明显。这个例子充分说明C在“表达比较逻辑”这件事上可以让编译器生成更高效的代码。3.3 记住这张对照表我把结论浓缩成一张对照表方便你在日常讨论或者技术评审时直接引用维度CC性能影响函数调用直接调用/函数指针直接调用/inline/lambdaC模板lambda常优于C函数指针容器手写数组结构体STL容器正确使用时接近C少写bug且不慢多态手动switch/if虚函数虚函数略有开销但可优化掉内存分配malloc/freenew/delete基本一致异常分配复杂场景C有额外信息字符串处理手动管理缓冲区std::string大多数场景C更安全且不慢错误处理errno/返回码异常正常路径性能不受影响这张表的结论是语言能力更丰富的C在“正确使用前置条件”下性能和C处于同一档位真正拉开差距的是用法而不是语言。4. 工具链优化与C特定性能技巧4.1 编译器选项与LTO其实是第一级优化很多人对比性能只关心语言写法忘了编译器才是“性能放大器”。同样的代码-O0和-O3的差距可以到几倍甚至几十倍比C和C之间的差距大太多了。所以第一步先把编译选项拉满。具体到C有几个非常关键的工具链选项值得提-O3启用更多循环优化和向量化适合计算密集场景。-marchnative让编译器针对当前CPU的指令集生成代码自动启用AVX2、NEON这些向量指令。-flto链接时优化允许跨编译单元做内联和常量传播。这个对C特别有帮助因为C大量依赖模板和inline跨文件优化能大幅减少冗余代码。-fno-exceptions和-fno-rtti如果你确定用不到异常和运行时类型信息关掉它们可以减少代码体积配合跳转表优化有时能带来微小的速度提升。但这属于“不用则免费”的收尾优化不要依赖它获得巨大收益。在我实际项目里C代码开-O3 -marchnative -flto之后和同样算法逻辑的C版本基本没有差距甚至在向量化和循环展开上C反而更容易命中优化器喜欢的模式。4.2 六条C高性能编码经验抛开语言之争用C写出能和C代码一样快甚至更快的代码还是有一些具体技巧的。我把这些年沉淀下来的经验整理成六条第一尽量使用“值语义 移动语义”而不是“指针 手动拷贝”。现代C的移动构造在函数返回、容器扩容时可以做到零复制把数据从一个内存块“搬”到另一个内存块只需要改几个指针。相比之下C风格的显式拷贝函数一旦拷贝逻辑写得粗心很容易多复制一两轮缓冲区。第二优先用模板而非虚函数。如果多态的类层级是固定的编译期就知道实现那么模板是你的最佳选择。模板没有虚表指针没有间接跳转编译器天然知道调用的具体函数能做深度内联。第三注意容器选型。std::vector是缓存友好的多数场景碾压std::list和std::map。C语言里手写链表和红黑树很容易在缓存命中率上输给std::vector线性扫描。C的代码如果只是“用结构体指针串起来”不一定就高效。第四减少不必要的内存分配。C写起来太方便了容易不知不觉在循环里创建临时std::string或std::vector每一次都可能触发堆分配。解决方法是复用缓冲区或者使用reserve预分配容量。这一点在C里大家天生会注意但C的语法糖容易让人大意。第五善用编译期计算。constexpr、模板元编程、if constexpr这些能力让C可以在编译期完成大量决策。运行时少算速度自然快。C语言在这一点上通常只能依赖宏宏又容易破坏调试体验和类型安全。第六热路径上避免shared_ptr。这不是说智能指针不好而是引用计数有原子变量的读取和递增/递减操作在单线程热循环里完全是多余的。单线程用unique_ptr或裸指针真正需要共享所有权时再考虑shared_ptr而且尽量用make_shared减少二次分配。4.3 从C到C的性能“搬运清单”如果你手上有一段C代码想用C重写但不希望变慢可以参考我整理的“搬运清单”结构体定义保留struct本身C兼容不过可以添加构造函数和成员函数。内存分配malloc/free换成new/delete如果对象生命周期简单直接换成栈对象或者std::vector。数组操作C风格裸数组换成std::array定长或std::vector变长在开启优化后性能一致但更安全。函数指针换成模板或者lambda编译器能内联。显式类型分支如果逻辑固定用模板或重载替换如果必须运行时分派优先考虑std::variant替代裸switch。字符串缓冲区char buf[1024]换成std::string注意提前reserve。错误路径把if (ret ! 0)改成异常抛出正常路径少一条分支判断。做完这七步之后用相同编译选项跑一遍基准绝大多数项目不仅不会变慢还会因为容器管理更精细、比较逻辑内联等机制变得更快。当然前提是别中途引入不必要的虚函数和复杂对象图。5. 常见误区、反例与排查实录5.1 “C慢”的经典误判性能讨论里最容易翻车的是把“误用”当成“慢”。我举几个最常见的误判第一个是把std::string的所有操作都当成慢。实际上std::string的短字符串优化SSO机制使得很短的字符串根本不做堆分配直接存在对象内部。而C语言里大多数char*操作其实都在处理动态内存反而容易触发堆分配。误判往往来自“看到封装就觉得慢”的心理预设。第二个是认为“面向对象慢”。其实类的成员函数和C结构体的外部函数编译后没有区别数据成员就是结构体字段。只要不做虚函数类的调用开销和C完全相同。第三个是“模板会让代码膨胀所以慢”。模板多实例化确实会增加代码体积但现代CPU的指令缓存足够大代码体积增大通常不会带来明显的性能损失。而且内联带来的好处往往远远大于指令缓存压力。我在测试中发现模板版的排序、搜索代码在大多数场景都比C的函数指针版本快。第四个是“C的异常太慢所以必须禁用”。前面说过零成本异常处理在正常路径上不插入检查代码真正产生成本的是抛异常的过程。如果你的程序正常路径上状态稳定几乎没有异常路径被触发那么禁用异常并不会带来明显收益。反而为了禁用异常改写出标志位判断可能增加额外分支。5.2 排查性能问题的方法论当你真的遇到一段C代码明显比预期慢时不要急着“换回C”而是按以下步骤排查第一步先用perf或者系统性能分析工具看热点在哪里。通常你会发现慢的不是某个语言特性而是某个具体函数、某个内存分配点或者某条连锁缓存缺失。第二步检查是不是频繁堆分配。常见线索是malloc和free的调用次数异常高。用-pg或者性能计数器统计一下分配次数如果一次循环里分配了成百上千次那就是分配模式的问题不是语言的问题。第三步检查是不是虚函数导致分支预测失败。如果热点是一个间接调用看看它能不能改成直接调用或者通过模板消除多态。第四步检查缓存友好性。遍历方式是否跳跃数据结构是否过于分散std::vector的连续内存比std::list的散列节点更利于缓存这一条在大型数据上尤其明显。第五步检查编译选项。会不会某个文件忘了开-O2或者某个接口被编译器拒绝内联C可以通过__attribute__((always_inline))强制内联但一般不用这么激进。我在多次调优中反复验证的一条经验是90%以上的“C变慢”问题最后定位出来都是内存分配次数太高或者虚函数误用而这些用C重写也避免不了——因为C手写同样的对象生命周期管理只会更容易出错不会更高效。5.3 快速自查清单最后给你一份可以直接贴在工位上的自查清单写代码的时候对照一下[ ] 所有热路径上的对象是否都尽量放在栈上或使用std::vector[ ] 循环内是否避免了不必要的堆分配和临时对象[ ] 多态调用是否真的需要虚函数能否用模板或std::variant替代[ ] 深层拷贝是否可以用移动语义或者引用/指针替代[ ] 已开启-O3和-flto[ ] 是否按输入规模提前reserve了容器容量[ ] 大数据量的遍历是否是连续内存而不是散列节点[ ] 原子操作和锁的使用是否只出现在真正需要并发同步的地方如果这些问题都过了你的C代码性能基本不会低于同逻辑的C代码。剩下的差距多半是优化水平问题不是语言问题。回到最初的问题“C相比C的运行速度”谁快谁慢——我在实际项目中反复验证的结论是同一水平的代码在相同编译选项下C基本可以做到和C持平甚至在算法、容器、模板的合理组合下更容易榨出性能。这也和C标准库花了大量精力做性能优化有关。C是一种简洁强大的语言但把“简洁”等同于“快”往往会错过C在编译期帮你消除运行时开销的潜力。我个人现在写性能敏感代码时反而倾向于用C不是因为它语法花哨而是因为它能让“意图”更明确容器明确、生命周期明确、多态明确编译器也因此更容易理解你的代码、生成更优的指令。最后再分享一个小技巧做性能对比时把自己的C和C代码都丢到godbolt上看一下汇编很多时候你亲眼看到两份汇编长得几乎一样心里就不会再被“语言慢”这种说法牵着走了。