一位从事物联网开发的程序员,在浏览某自动驾驶企业技术分享时注意到评论区的一场争论。感知系统搭建的话题下,讨论最终聚集到一个疑问上:推理代码既然要用C++落地,此前用Python反复打磨又是为了什么?
从直觉看,这条路径显得绕远:先用Python完成模型训练,再设法转化为C++可执行的形态,中途还要经历导出、量化、验证等环节。有网友直言,Python不过是玩具,真正投产还得依靠C。
恰好那段时间他在优化一个检测盒子,于是借机把"Python慢在何处"逐层拆开实测。这台设备环境为Windows 11,运行Python 3.13.12配合numpy 2.5.3,全部数据均为本机跑出。结论显示,这个问题的答案与运行快慢的关联并不大。
同样的模式也出现在推荐系统、风控模型、搜索排序以及量化策略中。大型企业的AI项目几乎遵循同一套流程:训练阶段放在Python里推进,随后把模型取出,嵌入其他语言上线。造成这种分工的,是两个性质完全不同的约束——迭代速度与确定性。
一、拆解“Python慢”:三层性质不同的损耗
笼统地说Python慢,其实混淆了三层彼此独立的机制,作者用探针分别做了测量。
第一层:损耗来自逐行解释,而非运算能力不足
以200万个数值做归一化为例,即减均值再除以标准差,三种写法的结果完全一致,但耗时差异明显:数据存为普通列表并用Python逐行循环;数据放进numpy数组但仍由Python逐个索引;整个循环交给numpy、底层由C完成。
测量中出现一个反常识现象,第二种写法竟比第一种更慢。把数据结构从列表换成numpy数组,不仅没有提速反而拖慢,原因是numpy单元素访问要经过Python协议(__getitem__),开销高于直接读取整数。
真正的瓶颈不在数据形式,而在那个留在Python里的for循环。当循环下沉到C层执行后,速度提升约7倍。
为单独衡量"解释一行"的成本,作者让空循环跑200万次,耗时0.0337秒,折算到每次约16.8纳秒,这被视为Python的成本下限。写一个a+b时,它需要先查明类型、找到对应方法、完成执行并把结果装箱,实际动作远多于表面的相加。
第二层:均值尚可,尾部会失控
更隐蔽的是GIL。让主线程承担固定工作,同时开启N个后台线程争抢GIL,观察主线程单次调用耗时分布可发现:p50从13.32攀升到75.93,涨幅5.7倍,尚在容忍范围;而p99从24.58飙升至983.49,提升幅度达40倍。
这便是GIL的实际表现:整体速度不会显著下滑,却会使个别调用突然卡顿接近一秒。对业务代码作者来说,多等一秒无伤大雅;但对须每秒处理30帧画面的系统而言,某一帧耽误一秒,这段时间内车辆等于盲行。
这正是解读整个问题的要点——均值会骗人,尾部延迟才决定成败。
第三层:跨语言调用需交“入场费”
第三层成本来自Python调用C。用ctypes调用一个极小的C函数并执行200万次,其开销是直接调用纯Python本地函数的6.19倍。

这是一笔与数据量无关的固定成本,合理做法是一次性传入成批数据以摊薄开销,而非在循环中逐项调用。后者会把0.24微秒乘以调用次数,轻易吞掉全部收益。
把三层并列后可以看出:逐行解释是主要损耗,但它只作用于循环仍留在Python的部分;GIL抖动不改变平均水平,却令延迟丧失确定性;跨语言调用存在入场费,可通过批处理分摊。因此Python的慢是可以改善的——把重计算的循环下沉到C,让Python退居调度角色。大公司惯常使用的正是这个思路,并且贯彻得更彻底,连模型上线都交给其他语言完成。
二、训练环节为何离不开Python
回到训练一侧,问题就不再是"Python够不够快",而是"训练究竟在优化什么"。
调整一次模型的流程大致是:修改损失函数、跑一轮、查看指标、不满意再改,中间还夹杂数据清洗、可视化与切片分析。这一循环中,"改一下再跑一遍"所需的时间就是全部代价。若单次训练耗时6小时,那么在动笔前减少几次错误,比把训练本身提速10%更有价值。
Python在此的真正优势与速度无关,体现在三个方面:
其一,深度学习生态只在Python上拥有完整入口。PyTorch、TensorFlow、JAX以及各类数据处理和可视化库,均把Python当作第一等公民。改用C++并非不能实现,而是需要自行搭建整套工具链。
其二,动态执行使调试成为可能。这正是PyTorch取代早期define-then-run框架的核心原因:能在前向传播途中设置断点,观察某个张量变化成了什么形态。静态图结构难以提供这种手段。
其三,数据侧的工作强度被严重低估。最典型的例子来自自动驾驶车队:搭载6到8个摄像头,分辨率从1080p到4K,再加上激光雷达与毫米波雷达,每小时生成的原始数据达到TB级别。清洗、标注、切片以及挖掘罕见场景,这些活全都由Python承担。
推荐系统、风控和搜索领域同样如此,模型那几行代码只是冰山一角,水下部分是庞大的数据处理。有经验的团队给出的配置比例为:每1名负责模型架构的人,就对应配备1名做数据基建、1名做部署验证的人。若只招架构方向的人,最终收获的是一堆漂亮的notebook,而非能够上线的模型。
训练侧之所以选Python,本质在于把工程师的时间视为最昂贵的资源。机器算力不足可以增添显卡,而人的迭代每慢一天都是实打实的损失。
关于"是否要转成C++",存在一个常见误读。PyTorch联合开发者Soumith Chintala曾专门澄清:这并非要求工程师手动重写Python代码,而是自动转换为低层运行时代码,实际工作量远没有想象中那么大。
三、部署环节为何落在C头上
到了上线阶段,评判标准与训练期完全不同。人们不再关心编写速度,只关注三件事。
要求一:延迟必须确定
仍以第一组数据为例。一个模型平均耗时12毫秒,偶尔尖峰达到90毫秒,这种表现要比恒定25毫秒更差。原因是下游以固定节拍消费结果——车载摄像头的帧率是固定的,范围在10到30赫兹,高端传感器组可到36赫兹;线上服务也存在明确的P99承诺。推理必须准时返回结果,而不是平均意义上准时。
Python自带三个抖动来源:垃圾回收、GIL、动态内存分配。在业务系统里它们意味着便利和省心,在延迟敏感的场景里则全部构成风险。因此上线推理几乎清一色放在C++进程中,通过调用TensorRT、ONNX Runtime或厂商SDK来完成。
要求二:部署产物必须可签名、可追溯
上线内容需具备追溯能力,需要一个经过编译、可复现、可签名的二进制产物,而不是依赖某台机器Python环境的脚本。编译产物能计算哈希、进入版本库并接受审计;单个.py文件加一堆依赖则很难做到这一点。

NVIDIA官方文档的表述相当明确:TensorRT的C++ API效率更高,并且更能满足某些合规要求,例如汽车行业。更关键的是,Python API并非在所有平台都可用,比如QNX——最常见的车载实时系统之一。也就是说,在你的目标平台上车,Python这条路有可能根本无法通行。
要求三:内存必须可控
C++里内存分配时机由开发者决定,Python里则由运行时决定。这一差异在量化之后更加突出:训练采用FP32或BF16,上线通常压缩到FP16或INT8级别。量化过程会让模型的置信度分数整体偏移,有可能正好把某个样本推过判定阈值。更棘手的是,假如0.4个点的指标下降全部集中在小目标那一档,那么意味着模型恰好在"行人检测"这类最致命的场景上出现了退步,而总体指标看上去只掉了0.4。
所以说量化不是导出一下就能收工,量化感知训练、逐通道校准等方法正是为此存在。
四、真正昂贵的是中间那道“导出缝”
照上面的推理,结论似乎是"各方干好各自的事情就行"。但在实际项目里,开销几乎从不产生于两端,而是出现在中间那条缝隙上。大公司里为此专设了一个岗位来负责,叫做模型部署。
作者把它称为导出缝,由三个具体的坑构成。
其一,自定义算子找不到等价实现。为提速而编写了自定义CUDA核,或者使用冷门的deformable attention、特殊padding的grid sample,而TensorRT与ONNX Runtime的算子集有限,很可能缺少对应实现。导出直接失败,只能重写,或者额外编写插件。
其二,数值漂移。由FP32训得的模型转为INT8,行为会发生改变。这不是理论风险,而是必然出现的情况,前文提到的0.4个点问题便由此而来。
其三,性能只在开发机上达标。在工作站显卡上测出8毫秒让人十分轻松,然而一旦换成车上的嵌入式SoC,或者一批架构不同的线上服务器,内存带宽、缓存层次和驱动栈全都不同,指标便会直接超标。
成熟团队的做法是把这条缝当成独立工程来看待:算法团队每周提交模型候选,同时通过一条导出验证流水线,负责在一天之内把候选变成量化后的引擎;上线团队拿到的是带版本的引擎文件,而不是训练阶段产出的checkpoint。
这一分工带来的直接结果是,团队必须按角色把人员配齐,不能只招单一种类的人。
五、这套逻辑对普通开发者意味着什么
大公司的分工方式看似遥远,但其思路可以原样照搬过来。
第一步:判断自己优化的是迭代速度还是运行速度。
编写业务代码、做数据分析、跑实验、搭建内部工具,所追求的都是迭代速度,选用Python本就理所应当,无需因"Python慢"而纠结。此时的瓶颈并非CPU,而是改一次代码到看见结果之间需要多久。
只有当进入延迟敏感的领域,比如实时控制、嵌入式、视频处理流水线和大规模在线服务时,才需要考虑向下层转移。而且不必全部重写,只把那个热点循环单独提取出来就够了。
第二步:把性能问题先定位到具体层级,再动手处理。
第一部分的三个实测其实构成一套诊断流程:

• 如果某个函数吃掉了80%的时间,先确认它是否为纯Python循环;若是,第一反应应是"能否向量化",而不是"该换语言了吗"。
• 若压力下频繁出现偶发高延迟,则应怀疑GIL和GC,此时应当关注p99而非平均值。
• 若大量时间消耗在跨语言调用上,应检查是否在循环中逐项调用;改为批量传参通常能立即收敛。
第三步:不要把“平均值”当作性能结论。
这是作者从GIL实验里得到的最大收益。同一段代码,单线程下p99为24毫秒,8线程争抢时变成983毫秒,而p50仅从13升到76。只看平均值,便会得出"高并发下性能衰减5倍"这种错得离谱的结论。
实际情况应是"多数请求依然正常,但有一部分卡了将近一秒"。这两种描述导向的处理方案截然不同。
第四步:承认每种语言各有自己的职责。
大公司的这条链路上其实不止两种语言。安全关键模块——包括操作系统内核、车辆控制单元、线控底盘安全模块——会采用Ada或SPARK这类为高安全等级设计的语言;后台服务里Go和Java也各占据一块位置。在正确的层级使用正确的工具,属于工程判断,而不是妥协。
回到开篇那个疑问:最终反正要写成C++,为何还要在Python上折腾?把全文的实测数据归纳一下:
其一,Python的慢是分层的。逐行解释是主要损耗,解释一行约16.8纳秒,但它只作用于仍留在Python层的循环;GIL不影响平均性能,却能够令p99扩大40倍;跨语言调用存在6.19倍的固定入场费,可以采用批处理摊平。
其二,训练侧使用Python,买的是迭代速度。生态、动态调试以及海量数据侧工具都集中在这一端,并且转换到C++是自动完成,而非人工重写。
其三,部署侧使用C,买的是确定性。固定节拍、可签名产物、可控内存,再加上Python在部分平台上根本无法使用。
其四,真正昂贵的就是中间那道导出缝。自定义算子、数值漂移、开发机与目标硬件不一致,这三个问题都应被当成独立工程来管理。
所以这套分工不是"先用方便的语言快速搭起来,再用专业语言擦屁股"。它源自两个完全不同的约束条件,并且各自推演出了唯一合理的解。大公司并非没尝试过只保留一种语言,而是在试过之后意识到两端都会吃亏。
Python胜在能快速试错,C的胜势在于按点交付。当一个系统同时需要这两种能力,就会同时需要这两种语言。
那位网友最后补充的一句话让作者颇为认同:"工具不是拿来站队的,是拿来分工的。"
小结:Python训模型、C++做部署并非折中之举,而是迭代速度与确定性两类约束下的必然分工;实测显示p99因GIL放大40倍、跨语言调用有6.19倍固定成本,而真正的工程难点集中在模型导出环节。团队应优先向效率要求的方向选择工具,只在延迟敏感处下沉到热循环,不必盲目整体重构。
