我试图非常有效地将打包的24bpp RGB图像转换为打包的32bpp RGBA。我已尝试使用vImageConvert_RGB888toRGBA8888
中的Accelerate.framework
,但想知道在Metal中使用计算内核是否有更快的方法。我在Metal中尝试了几种不同的方法,但结果总是比使用Accelerate.framework
慢得多,即使对于大于1M像素的大图像也是如此。
这是我的计算内核的样子:
kernel void rgb24_to_rgba32(texture2d<half, access::read> inTexture [[texture(0)]],
texture2d<half, access::write> outTexture [[texture(1)]],
uint2 id [[ thread_position_in_grid ]])
{
uint2 srcAddr1 = uint2(id.x * 3, id.y);
uint2 srcAddr2 = uint2(id.x * 3 + 1, id.y);
uint2 srcAddr3 = uint2(id.x * 3 + 2, id.y);
outTexture.write(half4(inTexture.read(srcAddr1).r, inTexture.read(srcAddr2).r, inTexture.read(srcAddr3).r, 1), id);
return;
}
我将inTexture
定义为r8Unorm
,将outTexture定义为bgra8Unorm
。两个纹理都使用.storageModeShared
加载,因此不应该发生任何内存复制。
代码正常运行并且转换正确执行,但性能不佳。我尝试了不同的threadgroupsPerGrid
和threadsPerThreadgroup
设置,但这些设置都没有达到与Accelerate.framework
相当的效果。
例如,在A7(第1代iPad Air)上,1024x1024图像大约需要32毫秒,而使用Accelerate.framework
则为6毫秒。有趣的是,对于更快的设备,例如基于A9的iPhone 6s(GPU上为1.5 ms,使用Accelerate
为1.1 ms),差异要小得多,但Metal实现总是较慢。
这不是一个对GPU友好的操作(可能是由于无数的未对齐内存访问?)我可能会遗漏一些基本上最大化计算内核性能的东西吗?
更新:使用以下实现,我最终能够获得比上述更好的性能:
此方法使用packed_uint3
进行96位读取,使用packed_uint4
进行128位写入,以显着提高性能。
#define RGB24_TO_RGBA32_PIXEL1(myUint) (myUint | 0xff000000)
#define RGB24_TO_RGBA32_PIXEL2(myUint1, myUint2) (myUint1 >> 24 | \
((myUint2) << 8) | 0xff000000)
#define RGB24_TO_RGBA32_PIXEL3(myUint2, myUint3) (myUint2 >> 16 | \
((myUint3) << 16) | 0xff000000)
#define RGB24_TO_RGBA32_PIXEL4(myUint3) ((myUint3 >> 8) | 0xff000000)
inline packed_uint4 packed_rgb24_to_packed_rgba32(packed_uint3 src) {
return uint4(RGB24_TO_RGBA32_PIXEL1(src[0]),
RGB24_TO_RGBA32_PIXEL2(src[0], src[1]),
RGB24_TO_RGBA32_PIXEL3(src[1], src[2]),
RGB24_TO_RGBA32_PIXEL4(src[2]));
}
kernel void rgb24_to_rgba32_textures(
constant packed_uint3 *src [[ buffer(0) ]],
device packed_uint4 *dest [[ buffer(1) ]],
uint2 id [[ thread_position_in_grid ]])
{
// Process 8 pixels per thread (two packed_uint3s, each containing 4 pixels):
uint index = id.x * 2;
dest[index] = packed_rgb24_to_packed_rgba32(src[index]);
dest[index + 1] = packed_rgb24_to_packed_rgba32(src[index + 1]);
return;
}
通过这种方法,旧设备上的性能差异变得更小(加速比GPU快约2倍),而在更现代(A9)的设备上,金属实际上快了大约40-50%
我尝试过每个线程处理一个,两个或更多packed_uint3
个向量,结论是两个向量是性能的最佳点。
答案 0 :(得分:2)
这里有几种途径可以探索。我不能保证你会让金属在目标设备上超越加速,但也许有机会加速。
考虑使用缓冲区而不是纹理。您的输入缓冲区可以是packed_char3
类型,输出缓冲区可以是packed_char4
类型。然后,您不必每次写入三次纹理读取,而是可以为每个像素索引一次源缓冲区。正如您所看到的,这些读取中的大部分都是未对齐的,但这种方法可能会为您节省一些格式转换和带宽。
考虑为每个内核调用做更多的工作。如果图像尺寸是4或8的倍数(例如),则可以使用循环(应该由编译器展开)来处理内核中的许多像素,从而减少需要调度的线程组的数量。
Accelerate非常适合您的用例,因此您可能希望坚持使用它,除非您在CPU时间紧张或者您可以容忍将工作分派到GPU并等待结果的延迟。
答案 1 :(得分:2)
仅仅为了结束,这是Apple的开发者关系对这个问题的回应。 最重要的是,GPU在这种情况下并没有提供任何真正的优势,因为这种转换不是计算量大的操作。
与工程部门讨论后,评估更多样本 实施,判决结果出在Metal v.s.加速 将打包的24bpp RGB图像转换为打包32bpp的性能 RGBA图像:在较新的设备上,您可以接近相同的图像 使用Metal但Accelerate的性能会更快 操作。 “vImage是一个非常精心调整的实现,从那以后 这个转换操作不是我们能做的最好的计算重量 平价。“
这背后提出的推理是数据局部性和有效性 一次操作多个像素(你提到过的)。 经过测试的最快的Metal实现处理了两个像素 线程仍然落后于
vImageConvert_RGB888toRGBA8888
。使用Metal缓冲区进行了“优化”实现 而不是纹理(你提到的其他东西)和 令人惊讶的是,这种方法的表现稍差。
最后,调整线程组也进行了讨论 通过向内核添加代码来处理调优情况 网格中的线程位置位于目标图像之外。再次, 尽管有这些考虑,加速仍然是最快的 实施
我应该补充说,使用Metal的一个真正优势是CPU使用率,虽然它没有更快,但它确实显着减少了CPU的工作量。对于CPU负载很重的应用程序,Metal方法实际上可能有意义。