通过引入轻量级唤醒层决策何时调用 OCR,而非单纯优化算法速度,将功耗降低一个数量级。展示了系统级性能优化的真正瓶颈所在。
不调用 OCR,比让 OCR 跑得更快效果更好——把“什么时候唤醒它”这个决定交给一个微型常驻层:Awake Layer。
我让设备端 OCR 变快了。单次检测从 40ms 降至 20ms,能耗降低 58%。但续航提升依然不够明显。因为单次处理时间减半,并没有改变它的运行次数。
测量后我发现,OCR 的 duty cycle 只有 10%。十分之九的时间里,它都在读取一块什么也没有的屏幕。于是,我不再纠结如何让它更快,而是在前面加了一层,由它决定是否需要调用 OCR——这就是 Awake Layer,一个成本低 70 倍的常驻层。最终,检测相关的功耗预算大约降低了一个数量级。
不过,这并不是一个毫无波折的故事。让 gate 本身节能 4.5 倍后,总功耗却只下降了 22%。真正主导功耗的并不是 gate 自身的成本,而是它发起调用的次数——实际调用次数是理想值的 6.9 倍。
字幕翻译的第一步,是读取屏幕上的文字。只要 OCR 在设备端运行,消耗的就是用户的电量。它不像云服务账单那样清晰可见,而是一张隐形账单。但它一定会到期——以电量消耗的形式出现。
所以,我首先做了最显而易见的事:让 OCR 更快。
这个方向本身没有错。不再读取整个屏幕,只查看最可能出现文字的区域。需要处理的像素少了,速度自然就会提升。
实测单次检测从 40ms 降至 20ms(-51%),能耗从 146mJ 降至 61mJ(-58%)。效果不算差,而且这项优化如今依然保留在产品中。
但续航提升仍然不够明显。原因很简单:如果执行次数不变,那么单次处理时间减半也帮不了太多。无论屏幕上有没有字幕,这项任务都会在每个周期运行,每秒执行五次。我只不过把每次举起的东西减轻了一半,但举起的次数完全没变。
于是,我转而测量调用次数。
十分之九的处理能力,都花在读取一块什么也没有的屏幕上。字幕并不会一直显示:没人说话时,它会消失;即使字幕出现,同一组字符也只会静静停在那里。真正值得读取的时刻其实非常短暂。
这改变了问题本身。问题不再是“怎样让这项工作跑得更快”,而是“这项工作是否有必要执行”。
我给出的答案是 Awake Layer。这个想法简单得令人不好意思:
在昂贵任务之前放置一个成本低几个数量级的常驻层,只让它判断一件事:现在是否需要唤醒昂贵任务?

图 1:Awake Layer——一个只负责决定是否唤醒的层。上面一行是旧设计(检测与 OCR 在每个周期都以 5Hz 运行,功耗为 204mW,而 duty cycle 只有 10%);下面一行是 Awake Layer 设计(一个耗时 0.5ms 的常驻层,只在字幕发生变化时唤醒检测与 OCR,功耗约为 32mW,下降 84%)。最下面一行比较了单次调用的能耗:昂贵检测为 40.8mJ,常驻层为 0.59mJ,两者相差约 70 倍。注意:这里的“一个数量级”是根据各个已测量组件叠加推算的结果,并非对整台设备进行的一次 ODPM 测量。
这一层只观察两件事:屏幕上现在是否有值得读取的文字(presence),以及它是否发生了变化(change)。它不会读取文字,也完全不知道文字的内容。它只回答一个问题:唤醒它,还是让它继续休眠?
只要把不同方案的成本数量级并排列出来,就很容易理解为什么这样做有效。
大约相差 70 倍。在这样的比例下,让廉价任务持续运行,同时关闭昂贵任务,优势非常明显。如果门卫的薪水只有里面工匠的七十分之一,那么让门卫一天 24 小时站在那里,反而更加便宜。
还有一项设计同样奏效:拆解字幕的变化类型。将字幕变化事件分解后可以发现,45% 是出现,45% 是消失,合计 90% 都是“有或没有”的状态转换,即使是廉价层也能区分。真正困难的是剩下的 10%——字幕始终存在,只是内容被替换。换句话说,其中十分之九的情况,廉价层都能捕捉到。
对于检测任务的持续功耗,结果如下:
每个周期都运行昂贵检测(每秒 5 次):204mW
Awake Layer 常驻运行,仅在字幕变化时执行检测(每秒 0.5 次):约 32mW(-84%)
在此基础上,再叠加前面提到的“单次调用能耗降低 58%”。两项优化叠加后,检测相关的功耗预算从 204mW 降至接近 20mW,也就是大约降低了一个数量级。实时翻译整体功耗约为 2000mW,因此这相当于让续航提升约 9%。
(这里需要坦白说明:这个数量级是根据多个实测组件叠加推算出来的,包括单次检测的实测能耗、常驻层的实测功耗以及实测触发频率;它并不是一次覆盖整台设备的端到端 ODPM 测量。)
此外还有一个额外收益。这一层并不会“读取”文字,它只判断画面中是否存在类似文字的视觉结构。因此,它在日文上学到的能力可以直接迁移到 Latin script 和 Hangul 上(已通过 4 部作品和 4 种书写系统验证)。所有针对单一语言的工程工作,就这样变得没有必要了。删除一项工作,也会同时消除它带来的多语言成本。
到这里为止,故事看起来非常顺利,但也正是在这个地方,我曾经彻底判断错误。
当我把 gatekeeper 的职责换到一个更廉价的模型上后,仅 gate 本身的功耗效率就提升了 4.5 倍。我当时想,总功耗显然也会大幅下降。但事实并非如此:总功耗只下降了 22%。
我把原因拆开分析,看到结果时甚至不得不再确认一遍。
处理 33 条字幕时,它调用昂贵任务大约 228 次,是理想情况的 6.9 倍——理想情况下,每条字幕只需调用一次。
无论你把 gatekeeper 做得多么便宜,如果它一直毫无理由地敲工匠的门,真正消耗电量的仍然是工匠。主导功耗的并不是 gatekeeper 自身的成本,而是 gatekeeper 发起调用的次数。
换句话说,“让它更便宜”还不够;只有真正做到“减少调用次数”,功耗才会发生明显变化。如果我在加入 gate 后就感到满足,可能永远不会注意到这 6.9 倍的过度触发。
既然如此,那就减少调用——于是,我把触发条件收紧为“只在上升沿触发一次”。调用次数从 274 次降至 69 次(-75%),功耗 proxy 下降 48%。结果完全符合预期。
代价是,字幕捕获率从 82% 降至 61%。在横屏画面中,情况更加糟糕——从 71% 降至 32%,三分之二的字幕都被漏掉了。功耗减半了,但作为一款翻译应用,它已经不合格。
我从中学到的是:“减少”并不意味着“粗暴削减”。将调用缩减为一次,有一个前提:这一层必须能准确命中真正值得唤醒的时刻。在具备这种能力之前就减少调用,削掉的每一次调用都会直接变成一次漏检。决策质量与调用次数无法分开考虑。这也是我目前所处的阶段——投入精力把决策这一侧做好。
关注调用次数,而不是单次调用的成本。如果无效调用以 10:1 的比例超过有效调用,那么将单次调用的成本减半,对功耗几乎没有帮助。最先应该测量的不是处理时间,而是 duty cycle——实测结果为 10%。
成本低一个数量级的层,即使持续运行也很划算。但加入 gate 并不是终点。如果 gate 的成本低 70 倍,那么让它一天 24 小时保持唤醒反而更便宜。不过,让 gate 变得更廉价(4.5 倍)和减少无效调用(过度触发 6.9 倍)是两个不同的问题,真正奏效的是后者。
删除一项工作,也会连同它附带的成本一起删除。但获得削减工作的权利,需要先用准确率来交换。针对不同语言的工程、评估和维护,全都随着这项工作的消失而消失;单纯加速永远做不到这一点。另一方面,减少调用的前提,是绝不能错过真正值得唤醒的时刻。如果决策能力仍然薄弱时就进行削减,那么你将用体验损失来换取功耗收益。
这正是这份宣言的第四项原则——Eliminate, Don't Accelerate——最直白的体现。让它跑得更快,换来了 51% 的降幅;决定不再调用它,则换来了一个数量级的改善。
注意:部分功耗 proxy(触发次数 × 成本的相对值)是在测试 harness 内部累积得到的,并非 ODPM 测量结果。上文所说的“一个数量级”,同样是基于多个实测组件叠加推算的结果。
本文最初发表于 LYR Performance Note #028,是“OCR — do less work, on the device”系列的一部分。完整系列可在 lyr.jp/en/research 查看。
如需采取进一步行动,你可以考虑屏蔽此人和/或举报滥用行为。