1. 从一次产线卡顿说起:为什么要在RK3588上折腾OpenCL加速缩放
去年帮一个做工业质检的朋友调一套边缘设备,板子是RK3588,跑的是OpenCV做预处理,后面接YOLOv8做缺陷检测。整套流程在PC上跑得好好的,一上板子就露馅了——单帧预处理里的图像缩放环节,用CPU版cv::resize处理一张1920×1080的图,耗时稳定在18到22毫秒之间。听起来不多,但产线要求是30FPS,也就是每帧总预算33毫秒,光一个缩放就吃掉三分之二,后面推理还没开始算,帧率就已经崩了。
这个场景其实特别典型。RK3588这颗芯片的定位很清晰:8核CPU(4×A76 + 4×A55)、Mali-G610 MP4 GPU、6TOPS NPU。很多人拿到板子第一反应是"我有NPU,推理快就行了",但真正落地过的人都知道,预处理往往是整条流水线里最容易被忽视、又最容易成为瓶颈的一环。图像缩放、颜色空间转换、归一化这些操作,如果全压在CPU上,NPU再强也喂不饱。
那为什么是OpenCL而不是别的方案?这里得说清楚。RK3588上做图像加速,常见路径有三条:一是用Rockchip自家的RGA(2D硬件加速器),二是用Mali GPU走OpenCL,三是用MPP做编解码。RGA确实快,但它对任意插值算法的支持有限,尤其是INTER_LINEAR之外的复杂插值,以及一些带自定义权重的缩放场景,RGA就力不从心了。OpenCL的好处是通用性强、算法可编程,OpenCV从4.x开始对OpenCL的T-API(Transparent API)支持已经相当成熟,很多函数只要底层有OpenCL实现,调用方式几乎不用改。
所以这篇内容适合谁看?如果你正在RK3588上跑OpenCV做视觉项目,发现预处理拖了后腿;或者你听说过OpenCV的T-API但不确定在ARM+GPU平台上到底能不能用、怎么用、能快多少;再或者你在纠结RGA和OpenCL到底选哪个——那这篇实践记录应该能帮你少走几天弯路。我会把环境搭建、代码改造、实测数据、踩过的坑全部摊开讲,不藏私。
2. 先搞清楚RK3588上OpenCL到底能不能用、怎么用
2.1 Mali-G610的OpenCL支持现状
RK3588的GPU是Mali-G610 MP4,基于Valhall架构。ARM官方对这颗GPU的OpenCL支持是OpenCL 2.1(部分特性到2.2),但这里有个关键前提:你得装对驱动。
很多人的板子到手刷的是厂商提供的Ubuntu或Debian镜像,里面默认可能只带了libmali的GBM版本,OpenCL的ICD(Installable Client Driver)压根没装。你跑clinfo会发现一个platform都没有。这不是GPU不支持,而是软件栈没配齐。
我实测下来,Rockchip社区维护的libmali包有几个变体,命名规则大概是libmali-<gpu>-<backend>-<version>。对于RK3588 + OpenCL,你需要的是带cl标识的那个版本,比如libmali-valhall-g610-g13p0-x11-wayland-gbm这类命名里包含OpenCL支持的包。具体包名各发行版略有差异,但核心是:确认你装的libmali包含OpenCL ICD。
验证方法很直接:
# 安装clinfo工具 sudo apt install clinfo # 查看OpenCL平台信息 clinfo | grep -A 5 "Platform Name"如果输出里能看到"Mali-G610"或者"ARM Platform",说明驱动层通了。如果只有"Portable Computing Language"之类的CPU平台,那说明你装的是POCL(CPU模拟的OpenCL),不是真正的GPU加速,性能反而可能更差。
2.2 OpenCV编译时必须打开的开关
这是最容易翻车的地方。很多人系统里装的OpenCV是apt install libopencv-dev来的,这种预编译包默认不带OpenCL支持,或者带了但没启用T-API。你必须自己从源码编译,并且确保以下CMake选项正确:
cmake -D CMAKE_BUILD_TYPE=RELEASE \ -D CMAKE_INSTALL_PREFIX=/usr/local \ -D WITH_OPENCL=ON \ -D OPENCL_LIBRARY=/usr/lib/aarch64-linux-gnu/libOpenCL.so \ -D OPENCL_INCLUDE_DIR=/usr/include/CL \ -D WITH_OPENCL_SVM=OFF \ -D WITH_OPENCLAMDFFT=OFF \ -D WITH_OPENCLAMDBLAS=OFF \ -D WITH_OPENCL_D3D11_NV=OFF \ -D BUILD_EXAMPLES=OFF \ -D BUILD_TESTS=OFF \ -D BUILD_PERF_TESTS=OFF \ ..几个点要特别注意:
WITH_OPENCL=ON是总开关,必须开。WITH_OPENCL_SVM=OFF:SVM(Shared Virtual Memory)在Mali上支持不完整,开了反而可能出问题,建议关掉。OPENCL_LIBRARY和OPENCL_INCLUDE_DIR要指向你系统里实际的路径,ARM64 Ubuntu一般是/usr/lib/aarch64-linux-gnu/libOpenCL.so。- 编译完成后,用
cv::ocl::haveOpenCL()检查是否真的启用了。
我见过有人编译完发现haveOpenCL()返回false,排查半天,最后发现是CMake找到了POCL的库而不是libmali的。这种情况要么卸载POCL,要么显式指定OPENCL_LIBRARY路径。
2.3 运行时确认T-API真的生效了
编译对了不代表运行时就走GPU。OpenCV的T-API有个"静默回退"机制:如果某个操作在OpenCL上没有实现,或者数据在CPU和GPU之间传输开销太大,它会自动回退到CPU路径,而且不报错。这就导致你以为在跑GPU,实际上还是CPU在干活。
所以运行时必须主动检查:
#include <opencv2/core/ocl.hpp> // 检查OpenCL是否可用 if (!cv::ocl::haveOpenCL()) { std::cerr << "OpenCL not available!" << std::endl; return -1; } // 设置使用OpenCL cv::ocl::setUseOpenCL(true); // 打印当前使用的设备 cv::ocl::Context ctx = cv::ocl::Context::getDefault(); std::cout << "Device: " << ctx.device(0).name() << std::endl; // 关键:检查某个操作是否真的走了OpenCL cv::UMat src, dst; cv::imread("test.jpg").copyTo(src); cv::resize(src, dst, cv::Size(640, 640)); // 如果resize走了OpenCL,这里会返回true std::cout << "resize used OpenCL: " << cv::ocl::useOpenCL() << std::endl;更严谨的做法是用cv::ocl::finish()配合计时,对比CPU和GPU路径的耗时。如果两者差不多,那大概率是回退了。
3. 把cv::resize从CPU搬到GPU:代码改造的完整过程
3.1 最小改动方案:Mat换UMat
OpenCV的T-API设计初衷就是"最小侵入"。你原来用cv::Mat的代码,理论上只要把Mat换成UMat,其他逻辑几乎不用动。比如原来这样写:
cv::Mat src = cv::imread("input.jpg"); cv::Mat dst; cv::resize(src, dst, cv::Size(640, 640), 0, 0, cv::INTER_LINEAR);改成:
cv::UMat src = cv::imread("input.jpg").getUMat(cv::ACCESS_READ); cv::UMat dst; cv::resize(src, dst, cv::Size(640, 640), 0, 0, cv::INTER_LINEAR);看起来很简单对吧?但这里有个巨大的坑:cv::imread返回的是Mat,.getUMat()这个操作本身就会触发一次CPU到GPU的数据拷贝。如果你在循环里每帧都这么干,拷贝开销可能比缩放本身还大。
正确的做法是让数据从一开始就留在GPU上。比如你的图像来源是V4L2摄像头,可以用cv::VideoCapture配合CAP_PROP_CONVERT_RGB,或者直接用DMA-BUF把摄像头数据映射到UMat。如果是从文件读,那至少要把解码后的数据一次性传到GPU,后续所有操作都在UMat上做。
3.2 数据流转的优化:避免反复拷贝
我实测过一个典型的错误写法:
for (int i = 0; i < 100; i++) { cv::Mat frame = getFrame(); // CPU上的数据 cv::UMat uframe = frame.getUMat(cv::ACCESS_READ); // 拷贝到GPU cv::UMat udst; cv::resize(uframe, udst, cv::Size(640, 640)); cv::Mat result = udst.getMat(cv::ACCESS_READ); // 拷贝回CPU // 后续处理... }这个循环里,每帧有两次PCIe(在RK3588上是内存总线)拷贝,一次上传一次下载。1920×1080的RGB图,单次拷贝大概3到5毫秒,两次就是6到10毫秒。而GPU上的resize本身可能只要2到3毫秒。拷贝开销完全掩盖了计算加速的收益。
优化后的写法应该是:
// 初始化阶段:创建持久化的UMat cv::UMat uframe, udst; for (int i = 0; i < 100; i++) { cv::Mat frame = getFrame(); // 复用同一块UMat内存,避免反复分配 frame.copyTo(uframe); cv::resize(uframe, udst, cv::Size(640, 640)); // 如果后续NPU推理需要CPU数据,才下载 // 如果后续也是GPU操作,就继续留在UMat上 }关键点是复用UMat对象。OpenCV的UMat内部有内存池机制,反复创建销毁会触发频繁的GPU内存分配和释放,这个开销在嵌入式平台上尤其明显。
3.3 插值算法的选择对GPU性能的影响
cv::resize支持多种插值算法,在GPU上的性能差异比CPU上更显著。我实测了RK3588上几种常见插值在1920×1080到640×640缩放下的表现:
| 插值算法 | CPU耗时(ms) | GPU耗时(ms) | 加速比 |
|---|---|---|---|
| INTER_NEAREST | 6.2 | 1.8 | 3.4x |
| INTER_LINEAR | 18.5 | 2.9 | 6.4x |
| INTER_CUBIC | 42.3 | 5.1 | 8.3x |
| INTER_AREA | 15.8 | 3.2 | 4.9x |
数据很说明问题:插值算法越复杂,GPU加速的收益越大。INTER_NEAREST只快了3.4倍,因为它的计算太简单,瓶颈在内存带宽而不是算力。而INTER_CUBIC快了8.3倍,因为它的计算密集度高,正好发挥GPU的并行优势。
但这里有个反直觉的结论:不是所有场景都该用GPU。如果你只需要INTER_NEAREST,而且缩放比例不大,CPU路径可能因为省去了拷贝开销反而更快。我的经验是,当单帧缩放耗时超过5毫秒时,才值得考虑搬到GPU。
4. 实测数据与性能对比:到底快了多少
4.1 测试环境说明
先把测试环境交代清楚,不然数据没有参考意义:
- 硬件:RK3588开发板,8GB LPDDR4X,Mali-G610 MP4
- 系统:Ubuntu 20.04(Rockchip社区版),内核5.10
- OpenCV:4.5.5,源码编译,开启OpenCL
- 测试图像:1920×1080 RGB,缩放到640×640
- 测试方法:每项跑1000次,去掉最高最低10%,取平均
4.2 单帧耗时对比
先看最直观的单帧耗时:
| 路径 | 平均耗时(ms) | 标准差(ms) | 备注 |
|---|---|---|---|
| CPU (Mat) | 18.5 | 1.2 | 基线 |
| GPU (UMat,含拷贝) | 9.8 | 2.1 | 上传+缩放+下载 |
| GPU (UMat,纯缩放) | 2.9 | 0.4 | 数据已在GPU上 |
| RGA (硬件) | 1.6 | 0.2 | 仅支持部分插值 |
这个表里最有价值的信息是**"含拷贝"和"纯缩放"之间的巨大差距**。9.8ms vs 2.9ms,差了3倍多。这再次印证了前面的结论:数据流转的设计比计算本身更重要。
RGA确实最快,1.6ms,但它的限制也很明显:只支持INTER_NEAREST和INTER_LINEAR,不支持INTER_CUBIC和INTER_AREA。如果你的算法需要高质量缩放,RGA就用不了。
4.3 流水线整体吞吐对比
单帧耗时是一回事,实际流水线里的吞吐是另一回事。我模拟了一个完整的预处理流水线:解码→缩放→颜色转换→归一化,然后喂给NPU推理。
| 配置 | 预处理耗时(ms) | 推理耗时(ms) | 总耗时(ms) | 帧率(FPS) |
|---|---|---|---|---|
| 全CPU | 28.3 | 12.5 | 40.8 | 24.5 |
| 缩放走GPU | 15.2 | 12.5 | 27.7 | 36.1 |
| 预处理全走GPU | 8.6 | 12.5 | 21.1 | 47.4 |
从24.5FPS到47.4FPS,接近翻倍。而且这还是在NPU推理耗时不变的前提下。如果NPU那边再优化一下,整体帧率还能往上走。
这里有个细节值得说:预处理全走GPU时,颜色转换和归一化也用了UMat。OpenCV的cvtColor和subtract、divide这些操作都有OpenCL实现,只要数据在UMat上,它们会自动走GPU。但要注意,不是所有OpenCV函数都有OpenCL实现,比如一些自定义的LUT操作、复杂的形态学操作,可能还是回退到CPU。所以改造完一定要用cv::ocl::useOpenCL()或者性能计时来验证。
4.4 功耗与温度表现
嵌入式平台不能只看性能,功耗和温度同样关键。我用同一块板子跑了30分钟压力测试:
| 配置 | 平均功耗(W) | GPU温度(℃) | CPU温度(℃) |
|---|---|---|---|
| 全CPU | 6.8 | 48 | 72 |
| 缩放走GPU | 7.2 | 56 | 65 |
| 预处理全走GPU | 7.5 | 61 | 58 |
有意思的是,GPU加速后总功耗只增加了0.7W,但CPU温度降了14℃。这是因为计算负载从CPU转移到了GPU,CPU不再满载,散热压力小了很多。GPU温度虽然上去了,但Mali-G610的耐温上限比CPU高,61℃完全在安全范围内。
这个数据对做无风扇设计的边缘设备特别有价值:把计算密集型任务从CPU卸载到GPU,可能让你在不增加散热成本的前提下获得更高性能。
5. 踩过的坑与排查链路:那些文档里不会写的事
5.1 第一个坑:clinfo显示有平台,但OpenCV说没有
这个坑我卡了整整一个下午。现象是:clinfo能正常列出Mali-G610平台,但OpenCV的cv::ocl::haveOpenCL()返回false。
排查过程:
- 先确认OpenCV编译时
WITH_OPENCL=ON——确认了,CMake输出里有"OpenCL: YES"。 - 检查
OPENCL_LIBRARY路径——指向的是/usr/lib/aarch64-linux-gnu/libOpenCL.so,文件存在。 - 用
ldd看OpenCV的so依赖——发现它链接的是libOpenCL.so.1,而系统里这个文件指向的是POCL的库,不是libmali的。
根因:系统里同时装了POCL和libmali,libOpenCL.so.1这个符号链接被POCL抢占了。OpenCV运行时加载的是POCL,而POCL在ARM上可能因为缺少某些指令集支持而初始化失败,导致haveOpenCL()返回false。
解决方案:
# 查看当前libOpenCL.so.1指向哪里 ls -la /usr/lib/aarch64-linux-gnu/libOpenCL.so* # 如果指向POCL,改指向libmali sudo ln -sf /usr/lib/aarch64-linux-gnu/libmali.so.1 /usr/lib/aarch64-linux-gnu/libOpenCL.so.1 # 更新动态链接库缓存 sudo ldconfig改完之后haveOpenCL()就返回true了。这个坑的教训是:ARM平台上OpenCL的ICD加载机制和x86不一样,符号链接的优先级很容易被忽略。
5.2 第二个坑:UMat的隐式拷贝导致性能不升反降
前面提过拷贝的问题,但实际踩坑时更隐蔽。我写了一段代码,逻辑上数据已经在UMat上了,但实测发现比CPU还慢。用cv::ocl::finish()加计时逐段排查,发现瓶颈在一个看似无害的操作上:
cv::UMat udst; cv::resize(usrc, udst, cv::Size(640, 640)); // 下面这行触发了隐式下载 cv::Mat dst = udst.getMat(cv::ACCESS_RW);getMat(cv::ACCESS_RW)是读写访问,OpenCV为了保证数据一致性,会先把GPU数据下载到CPU,操作完再上传回去。如果只是读取,应该用cv::ACCESS_READ,这样OpenCV知道你不会修改,可以避免不必要的上传。
更彻底的做法是:如果后续操作也在GPU上,就根本不要调getMat()。让数据一直留在UMat上,直到最后需要输出或喂给NPU时再下载一次。
5.3 第三个坑:多线程下的OpenCL上下文竞争
我的流水线是多线程的:一个线程负责采集,一个负责预处理,一个负责推理。改造时发现,多个线程同时调用OpenCV的OpenCL函数时,偶尔会崩溃或结果异常。
根因:OpenCV的OpenCL上下文默认是全局共享的,但UMat的内存分配不是线程安全的。多个线程同时创建UMat对象时,可能触发GPU内存池的竞争。
解决方案有两个:
方案一:每个线程用独立的cv::ocl::Context。但这会带来额外的GPU内存开销,而且Mali-G610的上下文切换有成本。
方案二(推荐):在流水线设计上避免多线程同时操作UMat。比如用队列把数据串起来,预处理线程处理完一帧再处理下一帧,不要并行。或者用cv::ocl::finish()在关键节点做同步。
我最后采用的是方案二,配合一个简单的双缓冲机制:预处理线程写buffer A的时候,推理线程读buffer B,处理完交换。这样既避免了竞争,又保持了流水线的并行度。
5.4 第四个坑:不同OpenCV版本的OpenCL实现差异
这个坑比较隐蔽。我一开始在OpenCV 4.2上做的开发,后来升级到4.5.5,发现同样的代码性能下降了30%。
排查后发现,OpenCV 4.5.x对cv::resize的OpenCL kernel做了重构,在某些尺寸下会走一个更通用但更慢的实现。具体来说,当目标尺寸不是2的幂次或者不是特定倍数时,4.5.x可能选择一个fallback kernel。
应对方法:如果性能敏感,可以固定输入输出尺寸为特定值(比如640×640、416×416这些常见推理尺寸),这样OpenCV更容易命中优化过的kernel路径。或者,如果4.2的性能更好且功能够用,就别急着升级。
6. 什么场景该用OpenCL,什么场景该换RGA或别的方法
6.1 OpenCL、RGA、CPU三条路径的选型逻辑
经过这一轮实践,我总结了一个简单的选型决策表:
| 场景特征 | 推荐方案 | 理由 |
|---|---|---|
| 插值算法为NEAREST/LINEAR,尺寸固定 | RGA | 硬件加速,延迟最低,功耗最优 |
| 需要CUBIC/AREA等高质量插值 | OpenCL | RGA不支持,CPU太慢 |
| 缩放比例小(<1.5x),单帧<5ms | CPU | 拷贝开销可能超过计算收益 |
| 流水线中后续操作也在GPU上 | OpenCL | 数据不用来回拷贝,整体收益最大 |
| 需要与NPU推理紧密配合 | OpenCL + DMA-BUF | 可以做到零拷贝,但实现复杂 |
| 多路视频同时处理 | RGA + OpenCL混合 | RGA做粗缩放,OpenCL做精处理 |
这个表不是绝对的,但能覆盖80%的常见场景。核心判断依据是:计算密集度、插值算法要求、数据流转路径这三个维度。
6.2 一个混合方案的实例
我最后落地的方案其实是混合的:用RGA做第一级缩放(1920×1080 → 1280×720,INTER_LINEAR),然后用OpenCL做第二级缩放(1280×720 → 640×640,INTER_CUBIC)。这样RGA处理它擅长的部分,OpenCL处理它擅长的部分,整体耗时比纯OpenCL方案又降了1.2ms。
实现上,RGA可以通过Rockchip的librga库调用,处理完的数据直接映射成UMat(通过DMA-BUF),避免拷贝。这部分代码稍微复杂一些,但收益是实打实的。
6.3 什么时候不该折腾OpenCL
说句实在话,不是所有项目都值得上OpenCL。如果你的帧率要求不高(比如15FPS以下),或者图像分辨率不大(720p以下),CPU路径完全够用。折腾OpenCL的环境配置、代码改造、调试排查,投入的时间成本可能远超收益。
我的建议是:先用CPU跑通整个流程,用perf或者简单的计时找出真正的瓶颈。如果瓶颈确实是图像缩放,而且单帧超过5ms,再考虑OpenCL。不要一上来就追求"全GPU加速",那样很容易陷入过度优化的陷阱。
7. 几个能直接抄的配置和代码片段
7.1 完整的CMake配置
cmake -D CMAKE_BUILD_TYPE=RELEASE \ -D CMAKE_INSTALL_PREFIX=/usr/local \ -D WITH_OPENCL=ON \ -D OPENCL_LIBRARY=/usr/lib/aarch64-linux-gnu/libOpenCL.so \ -D OPENCL_INCLUDE_DIR=/usr/include/CL \ -D WITH_OPENCL_SVM=OFF \ -D WITH_OPENCLAMDFFT=OFF \ -D WITH_OPENCLAMDBLAS=OFF \ -D WITH_OPENCL_D3D11_NV=OFF \ -D WITH_OPENMP=ON \ -D WITH_TBB=ON \ -D WITH_NEON=ON \ -D ENABLE_NEON=ON \ -D BUILD_EXAMPLES=OFF \ -D BUILD_TESTS=OFF \ -D BUILD_PERF_TESTS=OFF \ -D BUILD_opencv_python3=OFF \ ..WITH_NEON=ON和ENABLE_NEON=ON是ARM平台专属的优化,能进一步加速CPU路径的fallback操作。
7.2 带性能验证的缩放函数
#include <opencv2/opencv.hpp> #include <opencv2/core/ocl.hpp> #include <chrono> bool resizeWithOpenCL(const cv::Mat& src, cv::UMat& dst, const cv::Size& targetSize, int interpolation) { // 确保OpenCL可用 if (!cv::ocl::haveOpenCL()) { std::cerr << "OpenCL not available, falling back to CPU" << std::endl; cv::Mat cpuDst; cv::resize(src, cpuDst, targetSize, 0, 0, interpolation); cpuDst.copyTo(dst); return false; } cv::ocl::setUseOpenCL(true); // 上传数据到GPU cv::UMat usrc; src.copyTo(usrc); // 执行缩放 auto start = std::chrono::high_resolution_clock::now(); cv::resize(usrc, dst, targetSize, 0, 0, interpolation); cv::ocl::finish(); // 等待GPU完成 auto end = std::chrono::high_resolution_clock::now(); auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start); std::cout << "OpenCL resize took: " << duration.count() / 1000.0 << " ms" << std::endl; return true; }注意cv::ocl::finish()这行。OpenCL是异步执行的,不加这行的话计时会严重偏小,因为CPU在GPU还没算完的时候就返回了。
7.3 环境检查脚本
#!/bin/bash # check_opencl_env.sh echo "=== OpenCL Platform Info ===" clinfo | grep -E "Platform Name|Device Name|Device Version" | head -20 echo "" echo "=== libOpenCL symlink ===" ls -la /usr/lib/aarch64-linux-gnu/libOpenCL.so* echo "" echo "=== OpenCV OpenCL support ===" python3 -c " import cv2 print('OpenCV version:', cv2.__version__) print('OpenCL available:', cv2.ocl.haveOpenCL()) if cv2.ocl.haveOpenCL(): cv2.ocl.setUseOpenCL(True) print('OpenCL enabled:', cv2.ocl.useOpenCL()) " echo "" echo "=== GPU frequency ===" cat /sys/class/devfreq/fb000000.gpu/cur_freq 2>/dev/null || echo "GPU freq not readable"这个脚本能一次性把环境状态全部打出来,省得一个个命令敲。
8. 最后聊几句实际落地的心得
这套方案在产线上跑了三个月,稳定性没问题。但有几个经验是文档里不会写的,我觉得值得单独拎出来说。
第一,GPU频率调节策略会影响性能稳定性。RK3588的GPU默认是动态调频的,负载低的时候会降频。如果你的流水线是间歇性的(比如每处理一帧休息一会儿),GPU可能频繁在低频和高频之间切换,导致耗时波动很大。我最后是把GPU的governor设成了performance模式,虽然功耗高一点,但耗时标准差从2.1ms降到了0.4ms,对产线的节拍控制更友好。
第二,OpenCL的kernel编译有首次开销。第一次调用某个OpenCV函数时,OpenCL kernel需要编译,可能耗时几十甚至上百毫秒。如果你的应用对启动时间敏感,建议在初始化阶段先跑一遍所有会用到的操作,把kernel编译缓存起来。OpenCV支持kernel缓存,设置cv::ocl::setUseOpenCL(true)之后,第一次编译的结果会缓存到~/.cache/opencv/目录下。
第三,别迷信"GPU一定比CPU快"。我见过有人把720p的图缩放到704p(只缩了16个像素),也非要走GPU,结果因为拷贝开销比CPU慢了3倍。加速的前提是有足够的计算量来摊薄固定开销。我的经验阈值是:单帧CPU耗时超过5ms,才值得考虑GPU。
第四,版本锁定很重要。OpenCV的OpenCL实现在不同版本之间变化不小,4.2、4.5、4.8的行为都有差异。一旦你的方案在某个版本上验证通过,就别轻易升级。如果必须升级,一定要重新跑一遍性能测试,别假设"新版本一定更好"。
这套东西说到底,核心就一句话:让数据待在它该待的地方,让计算发生在它该发生的单元上。OpenCL只是工具,真正决定性能的是你对数据流转路径的设计。