7. CPU/GPU负载优化:减少CPU唤醒次数、使用GPU加速图像处理、降低DSP负载
各位好,我是老张。这一章我们来聊聊CPU/GPU的负载优化。说实话,相机功耗里最让人头疼的就是CPU和GPU的调度问题。你想想看,相机一开,CPU就得不停干活,GPU也得跟着忙活,DSP也不能闲着——这三个家伙要是没协调好,功耗直接起飞。
我个人习惯把这一章分成三个核心方向:减少CPU唤醒次数、用GPU分担CPU压力、降低DSP的无效负载。咱们一个一个来拆解。
核心原则:能少干活就少干活,能并行就别串行,能用专用硬件就别用通用硬件。
7.1 减少CPU唤醒次数——别让CPU老醒
CPU唤醒,说白了就是让CPU从睡眠状态切到工作状态。每次唤醒都要消耗不少能量,就像你半夜被吵醒,再想睡着又得折腾半天。相机场景下,CPU被频繁唤醒的罪魁祸首通常是:帧中断、传感器回调、应用层轮询。
我在项目中遇到过这样一个案例:某款手机预览时CPU占用率高达40%,但实际有效计算量只有10%。剩下的30%全浪费在无意义的唤醒和上下文切换上了。后来怎么解决的?我跟你讲讲。
7.1.1 批量处理中断
相机传感器每帧都会产生中断。如果每帧都唤醒CPU,那30fps就是每秒唤醒30次。其实很多中断处理可以合并——比如把3帧的中断放到一起处理。
// 优化前:每帧都唤醒
void sensor_irq_handler() {
wake_up_cpu();
process_frame();
}
// 优化后:批量处理
void sensor_irq_handler_batched() {
if (batch_count++ < BATCH_THRESHOLD) {
// 只记录,不唤醒
queue_frame();
return;
}
wake_up_cpu();
process_batch();
batch_count = 0;
}
小技巧:BATCH_THRESHOLD一般设2~3比较合适。设太大延迟会变高,用户能感觉到快门延迟。我一般从2开始试,看延迟和功耗的平衡点。
7.1.2 用epoll替代轮询
很多老代码喜欢用while循环轮询某个状态位。这简直是CPU杀手。我曾经在一个第三方算法库中看到这样的代码:
// 错误示范:忙等
while (!is_frame_ready()) {
// 空转,CPU 100%
}
正确的做法是用epoll或者fence机制。让CPU在事件到来前安心睡觉。
// 正确做法:epoll等待
int epfd = epoll_create(1);
struct epoll_event ev;
ev.events = EPOLLIN;
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);
// CPU在这里可以进入深度睡眠
epoll_wait(epfd, &events, 1, -1);
7.1.3 动态帧率调节
不是所有场景都需要30fps。比如你只是打开相机取景,画面静止不动,那降到15fps甚至10fps用户根本感觉不出来。我习惯的做法是:
- 静止场景:降到10~15fps
- 轻微运动:20fps
- 快速运动或扫码:30fps
这个策略在低端机上效果特别明显。我记得有一次在骁龙6系平台上,光这一项就省了80mW的功耗。
7.2 使用GPU加速图像处理
CPU做图像处理,说白了就是拿大刀绣花——不是不能做,但效率太低。GPU天生就是干这个的。尤其是YUV转RGB、色彩校正、滤镜这些操作,GPU比CPU快一个数量级。
7.2.1 RenderScript(老项目首选)
如果你还在维护Android 8~10的项目,RenderScript是个不错的选择。虽然Google已经弃用了,但现有代码还能用。
// RenderScript YUV转RGB示例
#pragma version(1)
#pragma rs java_package_name(com.example.camera)
rs_allocation inputY;
rs_allocation inputUV;
uchar4 __attribute__((kernel)) yuvToRgb(uint32_t x, uint32_t y) {
uchar y_val = rsGetElementAt_uchar(inputY, x, y);
uchar uv_val = rsGetElementAt_uchar(inputUV, x / 2, y / 2);
// 转换逻辑...
return convertToRgb(y_val, uv_val);
}
注意:RenderScript在Android 12以上已经不建议用了。新项目建议直接用Vulkan Compute Shader或者OpenCL。我去年有个项目就因为用了RenderScript,在Android 14上跑出了奇怪的性能问题。
7.2.2 Vulkan Compute Shader(推荐)
Vulkan是现在的主流。它的Compute Shader可以充分利用GPU的并行计算能力。我个人习惯把图像处理拆成多个小kernel,每个kernel处理一个独立的步骤。
// Vulkan Compute Shader 片段
#version 450
layout(local_size_x = 16, local_size_y = 16) in;
layout(binding = 0) uniform sampler2D inputImage;
layout(binding = 1) uniform writeonly image2D outputImage;
void main() {
ivec2 pos = ivec2(gl_GlobalInvocationID.xy);
vec4 color = texelFetch(inputImage, pos, 0);
// 做色彩校正
color.rgb = applyColorMatrix(color.rgb);
imageStore(outputImage, pos, color);
}
这里有个坑:不要频繁创建和销毁Vulkan管线。管线创建开销很大,我见过有人每帧都重新创建,结果GPU负载比CPU还高。正确的做法是在初始化时创建好,运行时只更新uniform buffer。
7.2.3 什么时候该用GPU?
| 操作类型 | CPU效率 | GPU效率 | 建议 |
|---|---|---|---|
| YUV→RGB转换 | 低 | 高 | 必须用GPU |
| 3A统计计算 | 中 | 低 | 用CPU或DSP |
| 滤镜/美颜 | 低 | 高 | GPU首选 |
| 人脸检测 | 中 | 高 | 看算法,NPU更好 |
7.3 降低DSP负载
DSP(数字信号处理器)是相机里专门干图像处理的硬件。它功耗低、效率高,但也不是万能的。DSP最怕的是重复计算和无效配置。
7.3.1 避免重复的3A计算
3A(自动曝光、自动对焦、自动白平衡)是DSP的主要工作之一。但很多场景下,3A参数变化很小,没必要每帧都重新算。我曾经在调试一个项目时发现,DSP每帧都在算AE,但画面亮度根本没变——白白浪费了算力。
解决方案:设置3A计算的触发阈值。只有画面变化超过一定比例时,才触发重新计算。
// 3A计算触发条件
if (abs(current_luminance - last_luminance) > LUMINANCE_THRESHOLD) {
trigger_3a_recalculation();
} else {
// 复用上次的结果
use_last_3a_result();
}
经验值:LUMINANCE_THRESHOLD我一般设5%~10%。太灵敏了省不了电,太迟钝了画面会忽明忽暗。你可以根据具体传感器调一下。
7.3.2 合理配置ISP pipeline
ISP(图像信号处理器)的pipeline里有很多模块:去噪、锐化、色彩校正、Gamma校正等等。不是所有模块在所有场景下都需要打开。比如在暗光下,锐化模块反而会放大噪点,不如关掉。
我习惯的做法是做一个场景-配置映射表:
| 场景 | 去噪 | 锐化 | 色彩增强 | HDR |
|---|---|---|---|---|
| 白天室外 | 低 | 中 | 开 | 关 |
| 暗光 | 高 | 关 | 关 | 开 |
| 扫码 | 关 | 关 | 关 | 关 |
这样DSP只做必要的工作,负载自然就降下来了。
7.3.3 关闭不用的图像处理模块
这个听起来很简单,但很多项目里都有这样的问题:初始化时把所有模块都打开了,后面也没关。比如你明明不需要人脸检测,但DSP还在跑人脸检测的算法。我曾经接手过一个项目,DSP负载一直下不来,最后发现是一个调试用的边缘检测模块忘了关——关了之后直接降了30%的DSP负载。
避坑指南:每次切换场景时,记得检查一下哪些模块可以关掉。不要偷懒只开不关。我曾经因为一个没关的HDR模块,导致预览功耗多了150mW,查了两天才找到原因。
7.4 总结一下
CPU/GPU/DSP的优化,说白了就是让合适的硬件干合适的活。CPU擅长控制逻辑,GPU擅长并行计算,DSP擅长固定算法。别让CPU去干GPU的活,也别让DSP去干CPU的活。
我个人习惯在项目初期就做好负载分配方案,而不是等功耗炸了再回头优化。你想想看,如果一开始就把中断合并、GPU加速、DSP配置这些做好,后面能省多少事?
嗯,这一章就到这里。下一章我们会聊聊更底层的硬件调优,到时候见。
公众号:蓝海资料掘金营,微信deep3321