做算法分析这么多年,我一直默认评价一个算法只需要看时间复杂度和空间复杂度就够。直到接手能耗敏感的模块,才意识到自己漏了最重要的一维:能耗模型。简单说,同一个算法在不同数据规模、不同内存布局、不同CPU频率下,每完成单位工作量烧掉的能量,差距可以拉到两到三倍甚至更多。能耗和计算效率从来不是简单的正相关,过度追求任何一边都会翻车。
这篇文章想聊的是,在做算法分析与设计时,怎么把能耗当成一个平等指标去衡量,而不是事后对着电费单做归因。内容适合三类人:正在学算法分析与设计的同学,想给排序、检索、数值计算模块做功耗优化的开发者,以及需要在一批候选算法之间做技术选型的工程师。文章里会给出我实际用过的建模框架、测量命令、实测数据形态,以及几个踩过很多次的坑。
1. 能耗模型与计算效率:为什么二者总像在打架
1.1 先打破一个误区:跑得快的算法不一定省电
很多人第一次听说能耗和效率要平衡时,第一反应是:这不是一回事吗?跑得快,时间短,能耗自然低。这个直觉在“平均功率恒定”的假设下成立,可惜真实处理器上的功率从来不是恒定的。
处理器功耗可以粗略拆成动态功耗和静态功耗。动态功耗正比于负载电容、电压平方和频率,表达式大概写成 P = α·C·V²·f。注意电压是平方项,这是DVFS(动态电压频率调节)能省电的根本原因。静态功耗则来自漏电流,只要芯片通电就存在,和当前执行什么指令关系不大。
于是总能耗可以写成:
E = (P_dynamic + P_static) × t
假设一个算法把CPU利用率和频率都顶满,执行时间很短;另一个算法执行时间稍长,但CPU利用率低、流水线空转多。前者平均功率高,后者平均功率低,最终总能耗谁大谁小,只有实测或建模才知道。这就是“快速算法”不等于“节能算法”的原因。
举一个我真实遇到的例子。有一个字符串匹配需求,理论上KMP比暴力匹配复杂度低很多,但处理短文本时,KMP的前缀表构建和额外分支判断会引入更多指令;数据都在L1缓存里时,暴力匹配反而因分支简单、指令密度低而更省电。后来我在一段长文本场景下才看到KMP的优势。这个经历说明:脱离数据规模和硬件层次谈“哪个算法更省电”,基本等于空谈。
1.2 能耗为什么要建模,而不是只做测量
有读者会问:既然是真是假,跑一遍用功率计测出来不就行了吗?为什么还要建立能耗模型?
测量的确最直接,但有几个限制。第一,测量只能在“有硬件、有代码、有运行环境”之后做,而算法分析往往发生在编码之前,需要先估算几条路径的能耗差别。第二,实测结果只能告诉你“刚才那次运行用了多少能量”,很难告诉你是哪一段指令、哪一类访存造成的。第三,真实业务中的数据规模会变化,数据和缓存的关系也会变化,你不可能把所有情况都测一遍。
能耗模型的价值在于预测和归因。它把能耗拆成若干可解释的项,例如运算指令数量、访存次数、分支错误预测次数、并发线程数等,再对每一项赋予单位成本。这样在编码之前,就可以把候选算法按能耗复杂度排序,也能在实测数据异常时快速定位问题出在访存还是计算。
所以,成熟的能耗优化流程通常是:先建模,再实测,最后用实测数据修正模型参数。不要只做其中一步。
1.3 现实需求:从“跑得快”到“跑得省”
为什么现在越来越强调能耗?不是理论洁癖,而是成本压力。
数据中心里,电力成本可以占到总运营成本的三成上下。服务器采购是一次性的,电费和散热却是持续支出,一个排序任务如果能在同样时延内省下10%能量,规模化后就是可观的数字。移动端和嵌入式场景更直接:同样的功能,算法耗电少一点,续航就长一点,发热就低一点。低功耗设备的散热能力往往比性能更先遇到瓶颈,功耗墙比算力墙更真实。
另外还有一个容易被忽略的场景:按需计费的云函数或无服务器架构。这类业务按执行时长计费,能耗和时间强相关,但单位时间内的功率又会反过来影响调度和配额。算法选择不再单纯看大O复杂度,而是看“在预算时间窗内,哪一种实现的能量成本最低”。这些都是我在实际项目中感受到的变化。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 按“复杂度”给能耗建模:先算清楚每一焦耳花在哪
2.1 三类成本:运算、访存与静态基础功耗
要把能耗当成第三维复杂度,得先清晰定义成本从哪来。
第一类是运算成本。简单说就是CPU执行整数加法、乘法、位运算等指令消耗的能量。不同指令能耗有差异,浮点运算比整数贵,除法比乘法贵,向量指令因为并行度高,单条指令的功耗更高但单位结果能耗往往更低。
第二类是访存成本。现代处理器的算力增长远快于内存带宽增长,于是访存成了主要瓶颈,也是能耗大头。为了讨论方便,下面给一组量级参考值,以一次整数加法为基数1,寄存器访问几乎不增加额外能耗,L1缓存访问大概是几倍,L2缓存访问是十几倍,主存DRAM访问可能是两百到三百倍,磁盘或网络IO则再高几个数量级。
| 访存目标 | 相对整数运算的能耗量级 |
|---|---|
| 寄存器 | 基本可忽略 |
| L1 Cache | 约2—4倍 |
| L2 Cache | 约10倍上下 |
| 主存 DRAM | 约200—300倍 |
| 磁盘/网络IO | 再高若干个数量级 |
这几组数值不需要记准,重要的是理解一个反常识结论:很多时候算法的“计算量”并不是主要能耗来源,数据搬到CPU路上的耗电才是。
第三类是静态基础功耗。芯片只要上电就在耗能,和跑不跑任务无关。算法建模时常把这个当成常数项,但当任务存在大量空闲等待时,静态功耗占比会上升。这也是为什么“让CPU尽早进入睡眠”往往比“让CPU低速运行同一段时间”更省电。
2.2 从 O() 到 E():一个可落地的抽象代价公式
我不建议一上来就建复杂的机器级功耗模型,那样既难校准,又容易被CPU微架构差异坑到。更实用的方式是把时间复杂度的思想平移过来:给核心操作加单位能量成本,再乘以操作次数。
假设一个算法的核心逻辑由三类操作组成:计算指令、缓存命中的访存、缓存未命中的主存访问,那么可以建立如下抽象代价:
E_estimate = N_compute × e_compute + N_cache_hit × e_cache_hit + N_miss × e_miss + E_idle
其中各项代表单位成本,N代表对应操作的估算次数。这个公式看起来像玩具,但实际价值在于用它做横向比较。
举个例子。处理一个长度为n的数组,方案A是“先堆排序再二分查找一次”,方案B是“直接线性扫描一次”。从时间复杂度看,堆排序是O(n log n),二分是O(log n),好像很划算;线性扫描是O(n),似乎很慢。但若数据小到能整体放进L2缓存,方案A构建堆时的乱序写回会造成大量缓存失效,每个主存访问成本是缓存命中的几十倍,方案B的顺序扫描则几乎全部命中缓存。这时公式会给出一个让人惊讶的结论:方案B可能更快也更省电。我在实际小数据集排序需求里验证过这个现象,纯粹的大O比较会给出错误答案。
建模不需要精确到小数。先给各项定单位成本数量级,再估操作次数范围,得出两个方案能耗的“相对量级”,就已经足够筛掉大部分明显不划算的候选算法。
2.3 别忘掉“隐藏复杂度”的能耗翻译
数据结构和系统调用里经常有理论上不影响复杂度的隐藏开销,翻译成能耗后却可能很可观。
- 动态内存分配:malloc/free涉及堆管理、锁和系统调用,单次开销不大,但循环里频繁分配释放,会让本来O(n)的算法变成能耗上的“隐形O(n log n)”。
- 虚函数调用:多态调用是多一次间接跳转,指令层面几乎可忽略,但它破坏分支预测,还会阻止编译器内联,连锁增加指令数。
- 错误处理路径:异常或错误分支很少执行,但为了支持异常机制,常规路径上也会多出一些状态保存指令。
- 容器扩容:例如动态数组在扩张时发生整体拷贝,单次扩容摊还后复杂度仍是O(1),但拷贝时的突发访存会造成功耗尖峰。
在能耗建模阶段,我会把上面每一项都当作“额外的 N_compute 和 N_miss”加到公式里,而不是在代码写成后再优化。这个习惯帮我避开了很多“理论复杂度好看,实际能耗难看”的设计。
3. 真正决定算法能耗的六个维度(不只是指令条数)
3.1 访存密度:算法能耗的最强风向标
访存密度可以理解为单位计算量需要从内存搬运多少数据。一个全是寄存器运算的稠密矩阵分块算法,能耗高但高效;一个频繁随机访问链表节点的算法,能耗高且低效。
我自己判断算法能耗时,第一个指标就是它的访存模式是否规则。顺序访问对缓存和内存预取最友好,跳跃访问最容易产生缓存未命中。所谓“缓存友好的算法”,本质上是在主动提升这个维度上的优势。
以排序为例。快速排序在分割阶段对数组做局部顺序访问,表现不错;但如果实现时对每个子区间使用额外的索引数组并随机跳转,缓存命中率就会明显下滑。堆排序虽然比较次数是O(n log n),但它访问数组的方式是跨层跳跃,每次比较往往要重新加载缓存行,因此堆排序在实测中经常比快速排序耗能高,这不是计算量造成的,而是访存密度太高。
3.2 分支预测失败:看不见的额外“计算”
现代CPU普遍采用深流水线和乱序执行,分支预测错误的代价大约是十几个周期的流水线冲刷。从复杂度分析看,一次错误预测也就是一次比较加跳转,但在能耗模型里,它意味着后续二十条指令全部白算,白算一样耗电。
最能说明问题的例子是二分查找。对一个有序数组做二分查找,每次比较都会产生一个数据相关的分支。如果目标值在数组中随机分布,现代分支预测器几乎无法预测方向,因此每条比较路径都可能引发错误预测。实测下来,这种随机的二分查找不仅慢,单位操作能耗也明显高于顺序扫描。有人因此提出用无分支的算术写法替代条件跳转,虽然指令数量增加,但错误预测减少后整体能耗反而下降。
这说明做能耗分析时,不仅要看算法结构,还要看数据分布对分支预测的“友好程度”。最伤人的不是分支多,而是分支结果不可预测。
3.3 并行扩展的收益递减效应
并行是提高计算效率的常用手段,但对能耗而言,并不是线程越多越省电。并行引入三类额外成本:线程创建和销毁、锁竞争或原子操作、线程间同步带来的等待和唤醒。
计算密集任务的并行在核心数较少时,能耗随吞吐提升而相对划算;核心数继续增加后,性能提升趋缓,但所有核心的静态功耗和总线功耗都在增加,最终可能出现“时延没下降多少,能耗上升一大截”的情况。这种情况在内存密集任务上尤其明显,因为瓶颈是内存带宽,加核只会增加等待,不会提高有效吞吐。
高效的并行能耗分析应该记录两个数字:加速比和“单位工作量能耗”。只要加速比增加的速度低于能耗增加速度,继续扩并行规模对能耗就是不经济的。
3.4 指令集和代码体积的连锁反应
同样逻辑,用复杂指令集或高内聚函数库实现,可能减少指令条数,但会导致代码体积变大。代码体积会影响指令缓存的命中率。一个紧密循环若超过L1指令缓存容量,循环体频繁被淘汰,处理器需要反复去L2甚至主存取指令,这部分能耗很容易被忽略。
我做过一个实验:把同一个小函数分别用-O2和-O3编译。-O3版用了更多循环展开和向量化,单次计算指令更少,但二进制体积变大。当这个函数被放在一个大型业务进程里、周围代码挤占缓存时,-O3版反而比-O2版能耗更高;独立跑benchmark时则相反。这就是“局部最优”和“系统最优”的差异。
3.5 数据规模和缓存层级的关系
算法复杂度只描述规模增长趋势,不描述数据在缓存层级中的位置。能耗模型必须显式考虑数据规模落在哪个层级:L1、L2、L3还是主存。
同一算法在不同数据规模下,能耗趋势可能完全改变。小规模时缓存全命中,算法能耗主要由指令数和分支预测决定;中等规模时L2未命中增多,访存开始主导;大规模时可能连TLB都频繁失效,每次访问页表都成为额外负担。做算法分析时,我习惯把基准数据分别压在L1、L2和主存三个规模档位上测试,而不是只测一个“代表性”规模。
3.6 运行频率与任务到达模式
最后这个维度不在算法本身,而在调度策略。一个CPU密集型任务在低频率下运行更久,静态功耗占比更高;高频率下运行时间短,可以尽早进入低功耗状态。这里存在“竞速到空闲”和“慢速到空闲”的权衡。
对实时任务或延迟敏感任务,通常更适合高频短跑;对后台批处理任务,低频慢跑常能明显降低总能耗。能耗模型如果忽略任务到达间隔和空闲状态,只算任务运行期的能耗,就会高估或低估不同策略的收益。
4. 实操:排序和矩阵乘的能耗实测与读数方法
4.1 测量环境怎么搭:RAPL与perf的最小配置
在Linux下做CPU能耗实测,我首选Intel/AMD的RAPL(Running Average Power Limit)接口。它通过芯片上的能量计数器,以固定间隔累计CPU不同域消耗的能量,读起来相当方便。
最省事的工具是perf,直接执行:
bash复制sudo perf stat -e power/energy-pkg/ ./your_benchmark
energy-pkg表示CPU整个封装的能量,通常还关心energy-cores和energy-dram。测试前最好固定CPU频率,避免睿频带来的噪声。我一般先用cpupower把频率锁定在一个档位:
bash复制sudo cpupower frequency-set -g performance
跑完要恢复可以再切回powersave或schedutil。每次测试前把数据预热一遍,或者干脆把计时起点设在数据初始化完成之后。
关于CPU affinity也要处理一下,避免线程在核间迁移影响缓存:
bash复制taskset -c 0 ./your_benchmark
这样的最小配置已经能拿到比较可信的能耗和耗时数据。注意不同机器、不同温度、不同BIOS设置下数值不可直接对比,只适合同一台机器上的横向比较。
4.2 场景A:三种排序算法在同一输入上的表现
为了展示读数方法,我用一个长度为一千万的随机整数数组,分别跑std::sort、手写递归快速排序(随机pivot)和堆排序。测试机是一台普通x86服务器,频率锁在2.4GHz,重复五轮取中间值。下面这组数字不是跑分竞赛结论,只是为了说明量级关系和读数方式。
| 实现 | 平均耗时(ms) | 平均能耗(J) | EDP(J·s) |
|---|---|---|---|
| std::sort | 812 | 74.7 | 60.7 |
| 手写随机pivot快排 | 1018 | 89.6 | 91.2 |
| 堆排序 | 1650 | 138.6 | 228.7 |
从表格能看出两个信息。第一,手写快排比std::sort慢约25%,能耗高约20%,这不是算法复杂度问题,而是实现质量差异,包括递归深度、局部性、是否内联比较函数等。第二,堆排序无论是时间还是能耗都明显落后,但很多人只从大O复杂度推断,会以为堆排序至少比手写快排更“稳妥”,实测能耗模型马上揭穿了这一点。
我再算一下EDP。EDP是Energy-Delay Product,即能耗乘以耗时,本意是同时惩罚耗时和能耗。以std::sort为基准,手写快排的EDP约为1.5倍,堆排序约为3.8倍。如果业务对“总成本”敏感,那选择顺序就是std::sort、手写快排、堆排序,和只看时间复杂度的直觉基本一致。
4.3 场景B:矩阵乘法的循环顺序与分块大小
矩阵乘法是验证访存影响的经典实验。以1024×1024的双精度矩阵为例,分别跑最朴素的ijk循环、改为ikj循环(内层访问连续内存)、以及加了分块缓存优化的版本。
| 版本 | 平均耗时(ms) | 平均能耗(J) | 说明 |
|---|---|---|---|
| ijk朴素循环 | 9828 | 632 | 内层按列访问,命中率低 |
| ikj循环 | 3410 | 226 | 内层变为行访问 |
| 分块优化(块64) | 1362 | 96 | 更好利用L2缓存 |
虽然时间差异已经很大,能耗差异更惊人:最优版本能耗只有朴素版本的15%左右。原因仍是访存,不是运算次数变化。矩阵乘法计算量完全一样,差别全在于每一轮计算需要从主存搬运多少数据。
我做分块优化时的参数选择逻辑是:先确认目标CPU的L2缓存每核容量,然后让分块矩阵的三块子矩阵总和略小于L2容量的1/2,给数据预取留余量。64×64的双精度矩阵三块总共是96KB,正好适配当时测试机每核256KB的L2缓存。不同机器上的最优分块会不一样,需要重测。
4.4 数据怎么读:只看时间会漏掉多少信息
把上面两组数据放一起,会发现一个规律:耗时的相对差距通常小于能耗的相对差距。也就是说,如果一个算法只是慢20%,它的能耗很可能要高出30%到40%,因为慢往往会伴随更多缓存未命中和更差的访存模式。
我对团队的建议是:在做算法评测时,至少同时记录三个数——耗时、能耗、EDP。如果只能选两个,选耗时和能耗;如果只能选一个,在强调电池续航的场景选能耗,在强调用户体感的场景选耗时。千万别只汇报一个“平均响应时间优化了15%”就收工,那可能掩盖了背后能耗上升15%的事实。
5. 平衡点的找法:三条我在生产环境里验证过的策略
5.1 用EDP这类组合指标做算法选型
单看能耗会偏向选慢但省电的算法,单看时间会偏向选快但费电的算法。EDP把两者相乘,能在两者之间找一个折中点。
实际使用中,EDP的具体系数可以按业务调整。电池供电的移动设备上,我会把能耗权重抬高,例如用E²×D,也就是Energy-Delay Product的变体,让能耗差距在选型决策里被放大;后台离线任务则用E×D²,更强调时延,让慢一点但省电的算法更容易被淘汰。权重不是拍脑袋定的,它应当对应成本结构:功耗损失多少元、时延增加多少元、散热成本多少元,把这些折算成权重。
选型时建议同时列出时间复杂度和“抽象能耗式”。例如:
- A算法:N_compute = n log n,访存顺序型,适合缓存
- B算法:N_compute = n,但访存随机型,cache miss多
在n小于某个阈值时,A可能能耗更低;n变大后B也许反超。这样就能画出一条“交叉规模线”,而不是武断地说哪个算法一定更好。
5.2 编译和运行参数不是“越高越好”
-O3比-O2快,这是很多人的默认认知。但从前面的代码体积实验可知,在真实系统里-O3可能因为指令缓存压力上升而增加能耗。-O3的好处是自动向量化和更多内联,缺点是代码膨胀和编译时间变长。
我给项目的建议是:不要全局开启-O3,而是对热点函数单独标注,或者先分别编译后用perf能耗数据对比。对同样的矩阵乘法代码,我在测试机上分别用-O2和-O3编译,独立运行时-O3能耗低约12%,但把代码放入一个巨大进程再运行时,-O3版能耗反而略高。这个例子说明编译选项的决策必须放在“代码要被放进什么环境里”去考虑,不能看孤立基准。
类似的选择还包括是否开启LTO、是否使用profiling引导优化。PGO在热路径上通常有正面收益,因为它优化的是真实分支走向,能减少错误预测和错误预取,这一点对能耗模型很友好。
5.3 调度与频率:在系统层面找平衡
算法级优化做得再好,如果调度策略不对,能耗一样会浪费在等待和空转上。
一个很常见的浪费是内存密集任务跑在“performance”高频模式。这类任务的主要瓶颈是内存带宽,CPU频率提高并不能显著缩短时间,却会显著提高动态功耗。我实际测过一个数据过滤任务,在最高频率下耗时1.2秒、能耗90J,把频率降到中档后耗时只增加到1.25秒,能耗却降到70J,省了超过20%。这是因为内存等待期间CPU频率带来的收益几乎为零,成本却实实在在地发生了。
另一个浪费是并行度过高。我在一个统计类任务上看到团队开了32个线程处理一个16核机器上的纯内存扫描,耗时反而比16线程多10%,能耗高了40%。线程之间的调度、缓存竞争和同步开销让扩核变成了负收益。判断并行度是否合适的标准很简单:记录“单位数据能耗”,如果继续加线程时这个指标上升,就说明已经过冲了。
5.4 别忘了空闲状态和任务合并
最后一个策略是考虑任务之间的空隙。假设系统中有大量零散小任务,每个任务到达时CPU要从深度睡眠唤醒,唤醒本身要消耗时间和能量。如果把它们合并成更少、更长的批处理任务,可以减少唤醒次数,让CPU在更多时间处于低功耗空闲态。
对算法设计者来说,这意味着数据结构可以适当增加缓冲和批处理接口。比如不是一个元素写一次磁盘,而是一批数据写一次;不是每个请求单独创建一个排序任务,而是攒够若干请求后再统一排序。这些“和算法复杂度无关”的设计,在能耗模型里经常贡献最大收益。
6. 常见问题排查与避坑记录(附速查表)
6.1 为什么两次测量能耗差很多
最常见的原因是CPU频率和温度没有固定。睿频会随负载和温度动态变化,同一段代码在不同轮次的能耗可能差出两位数百分比。解决方法是固定频率、让机器先跑几轮预热、反复测量取中位数而不是平均值。
另一个原因是后台进程干扰。即使占用率不高,后台服务的中断也会唤醒CPU,增加静态功耗占比。测试前关闭不必要的服务,用taskset绑定测试进程到指定核心,并把中断尽量分散到其他核心。
6.2 RAPL读数为0或者权限报错
RAPL需要root权限或修改perf_event_paranoid。如果perf报错,可以先检查proc里的权限设置,或改用直接读取MSR的方式。有些虚拟化环境不开放RAPL计数器,这时只能在物理机上测,或退而使用基于时间的估算模型。
6.3 数据预热对结论的影响
我见过不少人在计时起点前没有做数据初始化,导致把数据填充的能耗计入算法耗时。在缓存友好的算法上,这种错误尤其致命——第一次访问未命中缓存的数据,能耗和耗时可能比正常运行高很多倍。做能耗测试前,先想清楚测量边界:是测“冷缓存”下的首次执行,还是测“热缓存”下的稳态执行。两种都有意义,但要明确说明,不能混在一起比较。
6.4 对比实验里只改一个变量
考察循环顺序对矩阵乘法的影响时,有人会同时把编译选项从-O0改成-O3、把排序列从快速排序换成内省排序,然后得出一个无法归因的能耗差。能耗实验和性能实验一样需要控制变量,一次只改一个因素。尤其是编译器优化选项,改一个级别就可能让访存顺序的影响完全显现不出来。
6.5 忽略了功耗温度对漏电电流的放大效应
芯片温度越高,静态漏电功耗越大。长时间的满负荷测试会让芯片温度持续上升,导致同一个测试在测试序列前段和后段读出的能耗不一致。如果测试时长较长,我建议在两轮之间插入一个短暂的冷却空档,或者把测量轮次顺序随机化,避免“总是先跑A再跑B”造成系统性偏差。
6.6 避坑速查表
| 现象 | 常见原因 | 建议做法 |
|---|---|---|
| 两次能耗差异超过10% | 睿频/温度波动 | 锁定频率,多次测量取中位数 |
| 快算法能耗反而高 | 访存随机或分支难预测 | 用能耗模型查看访存和分支占比 |
| 加线程后能耗大涨 | 内存带宽瓶颈 | 记录单位数据能耗,评估是否过冲 |
| -O3比-O2能耗高 | 指令缓存压力增大 | 放在真实进程环境里对比 |
| RAPL读不到数据 | 权限或虚拟化限制 | 修改perf权限,或改用物理机/MSR |
| 高低频耗时时长相同 | 内存密集任务 | 主动降压降频可明显节能 |
| 冷启动第一次运行偏高 | 缓存和TLB未预热 | 明确测量边界,热/冷分开测 |
我自己的习惯是维护一份“能耗实验记录”,每条记录包含机器型号、固定频率、CPU绑定、室温、数据规模、循环次数和原始计数。这样哪怕几个月后回来复查,也能复原实验条件。没有这种记录,跑出来的能耗数字基本只能用来安慰自己。
最后再分享一个经验:能耗模型和计算效率的平衡,本质上是一个有约束的优化问题,约束来自成本结构、散热能力、用户体感,而不是来自硬件参数本身。我见过太多开发者把CPU的TDP数字背得滚瓜烂熟,却不知道同一段代码换一种访存顺序就能省下接近一半的能量。真正的收益,往往隐藏在算法复杂度公式之外那些不太好量化的维度里:数据局部性、分支行为、并行度、编译策略,以及任务之间如何调度。把这些维度放进你日常的算法分析流程里,能耗就不会再是事后补救的问题,而是和时延、吞吐并列的设计输入。
