从 CPU 冒烟到丝滑体验:算法 SRE 性能优化实战全揭秘|得物技术

一、引言
在算法工程中,大家一般关注四大核心维度:稳定、成本、效果、性能。
其中,性能尤为关键——它既能提升系统稳定性,又能降低成本、优化效果。因此,工程团队将微秒级的性能优化作为核心攻坚方向。
本文将结合具体案例,分享算法 SRE 在日常性能优化中的宝贵经验,助力更多同学在实践中优化系统性能、实现业务价值最大化。
二、给浮点转换降温
算法工程的核心是排序,而排序离不开特征。特征大多是浮点数,必然伴随频繁的数值转换。零星转换对 CPU 无足轻重,可一旦规模如洪水倾泻,便会出现 CPU 瞬间飙红、性能断崖式下跌的情况,导致被迫堆硬件,白白抬高成本开销。
例如:《交易商详页相关推荐 - neuron-csprd-r-tr-rel-cvr-v20-s6》 特征处理占用 CPU 算力时间的 61%。其中大量工作都在做 Double 浮点转换,如图所示:

优化前 CPU 时间占比 18%
Double.parseDouble、Double.toString 是 JDK 原生原子 API 了,还能优化?直接给答案:能!
浮点转字符串:Ryu 算法
https://github.com/ulfjack/ryu
Ryu 算法,用“查表+定长整数运算”彻底摒弃“动态多精度运算+内存管理”的重开销,既正确又高效。
算法的完整正确性证明:
https://dl.acm.org/citation.cfm? doid=3296979.3192369
伪代码说明
传统方法 vs. Ryu 算法对比:

字符串转浮点:Fast_Float 算法
https://github.com/wrandelshofer/FastDoubleParser
相比 Java 自带的 Double.parseDouble 使用复杂状态机(如 BigDecimal 或 BigInteger)来处理各种情况,FastDoubleParser 使用以下优化策略。
FastDoubleParser 优化策略
※ 分离阶段
将输入拆分为三个部分:significand、exponent、special cases(如 NaN, Infinity)。
解析时直接处理整数位和小数位的组合。
※ 整型加速 + 倍数转换
在范围允许的情况下使用“64 位整数直接表示”有效位。
再通过预计算的“幂次表(10ⁿ 或 2ⁿ)”进行快速缩放,避免慢速浮点乘法。
※ 避免慢路径
避免使用 BigDecimal 或字符串转高精度,再转回 double 的慢路径。
对于大多数输入,整个解析过程不涉及任何内存分配。
※ SIMD 加速(原版 C++)
在 C++中使用 SIMD 指令批量处理字符,Java 版受限于 JVM,但仍通过循环展开等技术尽量进行优化。
转换思路
压测报告

Double 字符解析相对 JDK 原生 API 4.43 倍 加速
代码优化样例
通过多层判断,尽可能不让 Object o 做 toString()操作。

减少 toString 触发的可能

工具类 替换浮点转换算法

工具类 替换浮点转换算法
性能实测效果
启用 Ryu、Fast_Float 算法替换 JDK 原生浮点转换,效果如下:

优化后 CPU 时间占比 0.19%【性能提升(18-0.19)/18=98%】

CPU 实际获得 50%收益

RT 实际获得 25%左右性能收益
小结
告别原生 JDK 浮点转换的高昂代价,拥抱 Ryu 与 FastDoubleParser,让 CPU 从繁忙到清闲,性能“回血”,节约的成本大家可以吃火锅。
三、拔掉诡异的 GC 毛刺
小堆 GC 问题
特征维度多时内存压力大,GC 问题可以预期。但很多同学可能没有见过,小堆场景,GC 也可能频繁触发,甚至引发异常。
如图所示:18GB 堆 扩容 -> 30GB 堆,均出现 RT99 周期脉冲,致使 5~6%的失败率。

社区瀑布流广告投放-Neuron 精排 因 GC 导致错误
GC 问题分析
首先这是 GC 问题,其次增加了近 1 倍的内存,没有丝毫缓解,判断这应该是个伪 GC 问题。
Neuron 主要功能就是拿着特征转向量做排序。一般特征量都是亿起步,多的达十亿,因此特征缓存必不可少。但是这个场景,仅仅是将 1700 个左右的广告特征信息进行了缓存,为什么对象内存会出现周期性的脉冲?

年轻代+老年代 周期共振脉冲
如图所示,关键的问题在于“共振”。因此要用放大镜看问题,再如图所示:




到这里,其实问题已经很明显了:
C4 作为世界顶级垃圾回收器,GC 的能力不用怀疑,STW(Stop-The-World)的时间理论是亚毫秒级。
如果 GC 能力没问题,算力又充足,那么造成 RT99 翻倍的原因:要么是线程在等数据,要么是线程忙不过来。
Neuron 堆内存大头是缓存,那么老年代回收的数据一定是缓存数据,年轻代一定是在回补缓存缺口。
为什么会有这个逻辑?因为缓存命中率一直是 99.9%【1700 个广告条目】,如图所示:

在极高缓存命中率的场景下,仅清理少量缓存条目,也可能造成“缓存缺口”。缓存缺口本质上也是一次“中断”,线程被迫等待或执行数据回补,导致性能抖动。
为方便理解,类比“缺页中断”(Page Fault):当程序访问未加载的内存页时,操作系统必须中断执行、加载数据,再继续运行。
解决方案
首先是缓存命中率一定是越高越好,99.9%的命中率没毛病。问题出在 1700 条广告缓存条目,究竟为何必须如此频繁地设置过期?【TTL: 60~90s】
原因是:业务期望广告特征,能够尽可能实时更新。


缓存失效策略
失效时间 60~90s
关键在于,缓存条目必须及时失效,却又不能因 GC 过度而引发性能问题。从观察结果来看,年轻代的 GC 没有对 RT99 的性能产生明显影响,这说明年轻代 GC 的力度恰到好处,不会造成频繁的“缓存缺口”。既然如此,我们考虑:如果能彻底规避老年代 GC,性能瓶颈的问题是否就能迎刃而解?
因此,我们尝试大幅提高对象晋升到老年代的门槛,直接提升了几个数量级。
在这个场景中,实际有效的对象并不多,最多不过 5GB。 其余大部分都是生命周期不超过 2 分钟的短期广告特征条目(约 1700 条)。这种短生命周期、低占用的场景完全靠年轻代 GC 就能轻松支撑,根本不需要启用分代 GC。
实际测试一天后,完全印证了这一判断:GC 抖动、RT99 抖动以及错误率抖动全都彻底消失,同时内存也没有出现任何泄漏。

GC 毛刺消失

RT99 失败率 毛刺峰值降至 1/10 +
小结
C4 的分代 GC 对大堆确实有奇效,但放在小堆场景里,非要套个复杂架构,就成了典型的“形式主义”
大堆适用,小堆不行。
四、是谁偷走了 RT 时间
业务瓶颈的卡点
最近算法特征多了,推理成本就高了;RT 一长,用户体验就垮了;产品一急,秒开优化就立项了。
全业务链路都已锁定 RT 优化目标,社区个性化精排也在其中,可这一链路优化阻力最大——RT99 长期卡在 120ms 以上,始终难以突破。

活用三昧真火
性能分析必看 CPU 火焰图。一看图就是 GC 问题。
GC 日志分析,年轻代+老年代,堆积起来约 150GB,而堆内存才给 108GB,怎么做到的?->>> 频繁 GC!

GC 算力消耗占比 超 50%


高频 GC
看看哪里分配内存比较疯狂,如图内存分配火焰图所示:

内存分配压力指向两大热点
※ Dump
业务刚需,大量序列化点对象带来的瞬时垃圾情有可原。
※ 特征
真正的“吞金兽”——独占超过 50%的堆。业务方解释:当前 500 万特征才勉强把命中率抬到 80%,想继续往上,只能指数级内存扩容,总特征数 10 亿+。堆已拉到 128GB,找不到更大规格的机器。
也就是说内存主要被特征吞掉了,优化空间基本没有。
如果优化止步于此,显然无法满足业务方的期望,于是我们进一步深入到 Wall 火焰图进行更精细的分析。

Wall 火焰图同时捕获了 CPU 执行与 IO 等待,因此不能简单地以栈顶宽度判断性能瓶颈。否则只会发现线程池空闲的等待任务,看似正常,但真正的性能瓶颈却隐藏在细节中。
因此,我们需要放大视角,聚焦到具体的业务逻辑堆栈位置。在这个案例中,一旦放大便能发现显著问题:特征读取阶段的 IO 等待时间,竟然超过了远程 DML 推理与 Kafka Dump 的总耗时。这直接说明,所谓的 80%特征缓存命中率存在明显的缓存击穿现象,大量请求可能被迫穿透至远端 Redis 或 C 引擎进行加载,其耗时成本远高于本地缓存命中的场景。

逐帧跟踪确认
通过进一步的 Trace 跟踪分析,我们的猜测得到了验证。

通过和 C 引擎团队联合排查发现,现有架构采用了早期的部署模式,其中为索引分片路由而设立的中间 Proxy 层成为性能瓶颈,其 RT999 甚至超过 100ms。这种架构带来的问题在于,上游业务对特征数量需求极大,即使缓存已扩大到 500 万条目,也仅能达到 80%的命中率。算法工程团队通过对特征请求进行多层拆分及异步并发查询优化,但仍有少量长尾特征无法命中缓存,只能依靠 C 引擎响应。一旦任何一批次特征查询触发了 C 引擎的慢查询,这一请求的整体 RT 势必大幅提升,甚至可能超时。
好在 C 引擎同时提供了一种更先进的垂直多副本部署模式,能够去除 Proxy 这一中心化的瓶颈组件。未来的新架构仍会保留索引分片设计,但会利用旁路方式实现完全的去中心化。

小结
通过 Wall 火焰图深入分析 RT 性能瓶颈,并结合 Trace 工具验证猜想,是优化系统性能不可或缺的关键步骤。
五、结语:性能优化无止尽
性能优化没有终点,只有下一个起点。每次性能的提升,不仅是对技术边界的突破,更是为业务创造了更多可能性。本文分享的场景和实操经验,旨在抛砖引玉,帮助各位同学掌握深度性能分析的方法论,避免走弯路,更高效地解决工程难题。希望每位研发和 SRE 同学,都能从微妙的细节中捕捉优化机会,让应用在极致性能的路上稳步前进。
往期回顾
1.得物自研 DScript2.0 脚本能力从 0 到 1 演进
2.社区造数服务接入 MCP|得物技术
3.CSS 闯关指南:从手写地狱到“类”积木之旅|得物技术
4.从零实现模块级代码影响面分析方案|得物技术
5.以细节诠释专业,用成长定义价值——对话 @孟同学 |得物技术
文 / 月醴
关注得物技术,每周更新技术干货
要是觉得文章对你有帮助的话,欢迎评论转发点赞~
未经得物技术许可严禁转载,否则依法追究法律责任。
评论