使用GPU进行像素格式转换?

时间:2016-09-29 23:55:01

标签: ios performance gpu gpgpu metal

我试图非常有效地将打包的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加载,因此不应该发生任何内存复制。

代码正常运行并且转换正确执行,但性能不佳。我尝试了不同的threadgroupsPerGridthreadsPerThreadgroup设置,但这些设置都没有达到与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个向量,结论是两个向量是性能的最佳点。

2 个答案:

答案 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方法实际上可能有意义。