在 GPU 上计算积分图像真的比在 CPU 上更快吗?

Is computing integral image on GPU really faster than on CPU?

本文关键字:CPU 图像 GPU 计算 真的      更新时间:2023-10-16

我是GPU计算的新手,所以这可能是一个非常幼稚的问题。
我做了一些查找,似乎在GPU上计算积分图像是一个不错的主意。
然而,当我真正深入研究它时,我想知道它可能不比 CPU 快,尤其是对于大图像。所以我只是想知道你对它的想法,以及GPU是否真的更快一些解释。

因此,假设我们有一个 MxN 图像,积分图像的 CPU 计算大约需要增加 3xMxN,即 O(MxN)。
在 GPU 上,按照"OpenGL 超级圣经"第 6 版提供的代码,它需要一些 KxMxNxlog2(N) + KxMxNxlog2(M) 操作,其中 K 是大量移位、乘法、加法的操作数......
GPU可以并行工作,比如说,一次32个像素,这取决于设备,但它仍然是O(MxNxlog2(M))。
我认为即使在 640x480 的常见分辨率下,CPU 仍然更快。

我错了吗?
[编辑]这是直接来自书中的着色器代码,其思路是使用 2 遍:计算行的积分,然后计算来自传递 1 的结果的列的积分。此着色器代码适用于 1 次传递。

#version 430 core
layout (local_size_x = 1024) in;
shared float shared_data[gl_WorkGroupSize.x * 2];
layout (binding = 0, r32f) readonly uniform image2D input_image;
layout (binding = 1, r32f) writeonly uniform image2D output_image;
void main(void)
{
uint id = gl_LocalInvocationID.x;
uint rd_id;
uint wr_id;
uint mask;
ivec2 P = ivec2(id * 2, gl_WorkGroupID.x);
const uint steps = uint(log2(gl_WorkGroupSize.x)) + 1;
uint step = 0;
shared_data[id * 2] = imageLoad(input_image, P).r;
shared_data[id * 2 + 1] = imageLoad(input_image,
P + ivec2(1, 0)).r;
barrier();
memoryBarrierShared();
for (step = 0; step < steps; step++)
{
mask = (1 << step) - 1;
rd_id = ((id >> step) << (step + 1)) + mask;
wr_id = rd_id + 1 + (id & mask);
shared_data[wr_id] += shared_data[rd_id];
barrier();
memoryBarrierShared();
}
imageStore(output_image, P.yx, vec4(shared_data[id * 2]));
imageStore(output_image, P.yx + ivec2(0, 1),
vec4(shared_data[id * 2 + 1]));
}

你说的integral image是什么意思?

我的假设是将相同分辨率K图像MxN相加。 在这种情况下,它在展位 CPU 和 GPU 上O(K.M.N),但在GPU上恒定时间可以更好,因为 gfx 内存访问比在 CPU端快得多。通常还有比CPU内核更多的GPU内核支持这一点

如果K太大而无法一次U放入GPU纹理单元,则需要使用多个通道,因此O(K.M.N.log(K)/log(U)) K>U......在某些情况下,CPU可能会更快。但正如之前的评论所建议的那样,没有测试,你只能猜测。您还需要考虑到存在无绑定纹理和纹理数组之类的东西,它们允许在单次传递中执行此操作(但我不确定这是否有任何性能成本)。

[编辑1] 清除您真正想要做的事情后

首先假设为简单起见,我们得到了NxN像素的方形输入图像。因此,我们可以将任务分别分为H线和V线(类似于2D FFT)以简化此过程。最重要的是,我们可以将每行细分为M像素组。所以:

N = M.K

其中N是分辨率,M是区域分辨率,K是区域数。

  1. 第一通行证

    每个组的渲染线,所以我们得到了K行大小M。使用片段着色器,仅输出到某些纹理,计算每个区域的积分图像。这T(0.5*K*M^2*N)整个事情可以通过覆盖屏幕的单个 QUAD 以片段渲染的方式完成......

  2. 第二、通行证

    将区域积分转换为完整图像积分。因此,再次渲染K行,并在片段中添加每个前一组的所有最后像素。这T(0.5*K^3*N)整个事情也可以通过覆盖屏幕的单个 QUAD 渲染片段来完成......

  3. 在另一个轴方向上对结果执行 #1,#2

这整个事情转化为

T(2*N*(0.5*K*M^2+0.5*K^3))
T(N*(K*M^2+K^3))
O(N*(K*M^2+K^3))

现在,您可以将M调整为设置的最大性能...如果我将整个事情重写为M,N那么:

T(N*((N/M)*M^2+(N/M)^3))
T(N*(N*M+(N/M)^3))

所以你应该最小化热量,所以我会尝试使用周围的值

N*M = (N/M)^3
N*M = N^3/M^3
M^4 = N^2
M^2 = N
M = sqrt(N) = N^0.5

所以整个事情转换为:

T(N*(N*M+(N/M)^3))
T(N*(N*N^0.5+(N/N^0.5)^3))
T(N^2.5+N^1.5)
O(N^2.5)

哪个比天真的O(N^4)更快 但你是对的,CPU为此要执行的操作较少,O(N^2)并且不需要复制数据或多次传递,因此您应该为您的任务找出特定硬件的阈值分辨率,并根据测量值进行选择。PS 希望我没有在计算中的某个地方犯一个愚蠢的错误。此外,如果您在 CPU 上分别执行 H 和 V 线,则CPU端的复杂性将O(N^3)并且使用这种方法甚至O(N^2.5)不需要每个轴 2 次传递。

看看这个类似的 QA:

  • 如何在GLSL中实现2D光线投射光效果

我认为这是一个很好的起点。