在Hough Line Transform中,对于每个边缘像素,我们在Hough参数空间中找到相应的Rho和Theta。 Rho和Theta的累加器应该是全局的。如果我们想要并行化算法,那么拆分累加器空间的最佳方法是什么?
答案 0 :(得分:7)
并行化算法的最佳方法可能取决于几个方面。一个重要的方面是您要定位的硬件。正如您使用" openmp"标记了您的问题,我认为,在您的情况下,目标是SMP系统。
要回答你的问题,让我们先来看一下Hough transform的典型,直接的实现(我将使用C,但以下内容也适用于C ++和Fortran):
size_t *hough(bool *pixels, size_t w, size_t h, size_t res, size_t *rlimit)
{
*rlimit = (size_t)(sqrt(w * w + h * h));
double step = M_PI_2 / res;
size_t *accum = calloc(res * *rlimit, sizeof(size_t));
size_t x, y, t;
for (x = 0; x < w; ++x)
for (y = 0; y < h; ++y)
if (pixels[y * w + x])
for (t = 0; t < res; ++t)
{
double theta = t * step;
size_t r = x * cos(theta) + y * sin(theta);
++accum[r * res + t];
}
return accum;
}
给定一组黑白像素(按行存储),Hough空间的角度分量的宽度,高度和目标分辨率,函数hough
返回累加器数组对于霍夫空间(有组织的&#34;角度方式&#34;)并在输出参数rlimit
中存储其距离维度的上限。也就是说,返回的累加器数组中的元素数由res * (*rlimit)
给出。
函数的主体以三个嵌套循环为中心:两个最外层循环遍历输入像素,而有条件执行的最内层循环遍历霍夫空间的角度维度。
为了并行化算法,我们必须以某种方式将其分解为可以并发执行的片段。通常,这种分解是由计算的结构或者由操作的数据的结构引起的。
因为除了迭代之外,函数执行的唯一计算上有趣的任务是最内层循环体中的三角函数,没有明显的机会根据计算结构进行分解。因此,让我们专注于基于数据结构的分解,让我们区分
在我们的例子中,输入数据的结构由像素数组给出,该像素数组作为参数传递给函数hough
,并由函数体中的两个最外层循环迭代。
输出数据的结构由返回的累加器数组的结构给出,并由函数体中最内层的循环迭代。
我们首先看输出数据分解,因为对于Hough变换,它导致最简单的并行算法。
将输出数据分解为可以相对独立生成的单位,实现最内层循环的迭代并行执行。
这样做,必须考虑任何所谓的loop-carried dependencies来循环并行化。在这种情况下,这很简单,因为没有这样的循环携带依赖项:循环的所有迭代都需要对共享数组accum
进行读写访问,但每次迭代都依次运行&#34; private&# 34;数组的一部分(即那些具有i
i % res == t
索引的元素。
使用OpenMP,这为我们提供了以下简单的并行实现:
size_t *hough(bool *pixels, size_t w, size_t h, size_t res, size_t *rlimit)
{
*rlimit = (size_t)(sqrt(w * w + h * h));
double step = M_PI_2 / res;
size_t *accum = calloc(res * *rlimit, sizeof(size_t));
size_t x, y, t;
for (x = 0; x < w; ++x)
for (y = 0; y < h; ++y)
if (pixels[y * w + x])
#pragma omp parallel for
for (t = 0; t < res; ++t)
{
double theta = t * step;
size_t r = x * cos(theta) + y * sin(theta);
++accum[r * res + t];
}
return accum;
}
可以通过并行化最外层循环来获得遵循输入数据结构的数据分解。
然而,该循环确实具有循环携带流依赖性,因为每个循环迭代可能需要对共享累加器阵列的每个单元进行读写访问。因此,为了获得正确的并行实现,我们必须同步这些累加器访问。在这种情况下,可以通过更新累加器原子来轻松完成。循环还带有两个所谓的反依赖性。这些是由内部循环的归纳变量y
和t
引起的,并且通过使它们成为并行外循环的私有变量来轻易处理。
我们最终得到的并行实现如下所示:
size_t *hough(bool *pixels, size_t w, size_t h, size_t res, size_t *rlimit)
{
*rlimit = (size_t)(sqrt(w * w + h * h));
double step = M_PI_2 / res;
size_t *accum = calloc(res * *rlimit, sizeof(size_t));
size_t x, y, t;
#pragma omp parallel for private(y, t)
for (x = 0; x < w; ++x)
for (y = 0; y < h; ++y)
if (pixels[y * w + x])
for (t = 0; t < res; ++t)
{
double theta = t * step;
size_t r = x * cos(theta) + y * sin(theta);
#pragma omp atomic
++accum[r * res + t];
}
return accum;
}
评估两种数据分解策略,我们观察到:
hough
中最内层循环的并行化。由于此循环没有任何循环携带的依赖项,因此我们不会产生任何数据同步开销。但是,由于对每个设置的输入像素执行最里面的循环,由于重复形成一组线程等,我们会产生相当大的开销。通常可以假设OpenMP中的原子操作非常有效,而已知线程开销相当大。因此,人们期望,对于霍夫变换,输入数据分解提供了更有效的并行算法。这通过简单的实验得到证实。对于该实验,我将两个并行实现应用于随机生成的1024×768黑白图像,其目标分辨率为90(即,每弧度1个累加器),并将结果与顺序实现进行比较。此表显示了两个并行实现对不同线程数获得的相对加速比:
# threads | OUTPUT DECOMPOSITION | INPUT DECOMPOSITION
----------+----------------------+--------------------
2 | 1.2 | 1.9
4 | 1.4 | 3.7
8 | 1.5 | 6.8
(该实验是在未加载的双2.2 GHz四核英特尔至强E5520上进行的。所有加速均为五次运行的平均值。顺序实施的平均运行时间为2.66秒。)
请注意,并行实现容易受累加器数组false sharing的影响。对于基于输出数据的分解的实现,通过转置累加器阵列(即,通过组织它&#34;远距离&#34;),可以在很大程度上避免这种错误共享。在我的实验中,这样做并测量了影响,并没有导致任何可观察到的进一步加速。
回到你的问题,&#34;分割累加器空间的最佳方法是什么?&#34;,答案似乎是最好不要分割累加器空间,而是分开输入空间。
如果由于某种原因,你已经决定分割累加器空间,你可以考虑改变算法的结构,以便最外面的循环迭代霍夫空间和内部循环,无论哪个是最小的输入图片的尺寸。这样,您仍然可以派生一个并行实现,该实现仅产生一次线程开销,并且没有数据同步开销。但是,在该方案中,三角函数不再是有条件的,因此,总的来说,每次循环迭代都必须比上面的方案做更多的工作。