【发布时间】:2020-12-08 19:44:10
【问题描述】:
我希望使用不同类型的 HPC 范例来实现一个简单的 Mandelbrot 集绘图仪,展示它们的优缺点以及实现的难易程度。想想 GPGPU (CUDA/OpenACC/OpenMP4.5)、线程/OpenMP 和 MPI。并使用这些示例为 HPC 新手提供帮助并了解可能性。代码的清晰性比从硬件中获得绝对顶级性能更重要,这是第二步;)
因为并行化问题很简单,而且现代 CPU 可以使用矢量指令获得大量性能,所以我还想将 OpenMP 和 SIMD 结合起来。不幸的是,简单地添加#pragma omp simd 不会产生令人满意的结果,并且使用内在函数不是非常用户友好或面向未来的。或pretty。
幸运的是,正在对 C++ 标准进行工作,以便更容易通用地实现向量指令,如 TS:"Extensions for parallelism, version 2",特别是第 9 节关于数据并行类型中所述。一个 WIP 实现可以在 here 找到,它基于 VC,可以在 here 找到。
假设我有以下类(已更改以使其更简单)
#include <stddef.h>
using Range = std::pair<double, double>;
using Resolution = std::pair<std::size_t, std::size_t>;
class Mandelbrot
{
double* d_iters;
Range d_xrange;
Range d_yrange;
Resolution d_res;
std::size_t d_maxIter;
public:
Mandelbrot(Range xrange, Range yrange, Resolution res, std::size_t maxIter);
~Mandelbrot();
void writeImage(std::string const& fileName);
void computeMandelbrot();
private:
void calculateColors();
};
以及下面使用 OpenMP 实现computeMandelbrot()
void Mandelbrot::computeMandelbrot()
{
double dx = (d_xrange.second - d_xrange.first) / d_res.first;
double dy = (d_yrange.second - d_yrange.first) / d_res.second;
#pragma omp parallel for schedule(dynamic)
for (std::size_t row = 0; row != d_res.second; ++row)
{
double c_imag = d_yrange.first + row * dy;
for (std::size_t col = 0; col != d_res.first; ++col)
{
double real = 0.0;
double imag = 0.0;
double realSquared = 0.0;
double imagSquared = 0.0;
double c_real = d_xrange.first + col * dx;
std::size_t iter = 0;
while (iter < d_maxIter && realSquared + imagSquared < 4.0)
{
realSquared = real * real;
imagSquared = imag * imag;
imag = 2 * real * imag + c_imag;
real = realSquared - imagSquared + c_real;
++iter;
}
d_iters[row * d_res.first + col] = iter;
}
}
}
我们可以假设 x 和 y 方向的分辨率都是 2/4/8/.. 的倍数,具体取决于我们使用的 SIMD 指令。
不幸的是,std::experimental::simd 上的在线信息非常少。据我所知,也没有任何重要的例子。
在 Vc git 存储库中,有一个 Mandelbrot 集合计算器的实现,但它非常复杂,并且由于缺少 cmets 相当难以理解。
很明显,我应该在函数computeMandelbrot() 中更改双精度的数据类型,但我不确定是什么。 TS 提到了某些类型 T 的两个主要新数据类型,
native_simd = std::experimental::simd<T, std::experimental::simd_abi::native>;
和
fixed_size_simd = std::experimental::simd<T, std::experimental::simd_abi::fixed_size<N>>;
使用native_simd 最有意义,因为我在编译时不知道我的界限。但是我不清楚这些类型代表什么,native_simd<double> 是单个双精度数还是执行向量指令的双精度数集合?那么这个系列有多少双打呢?
如果有人能给我指出使用这些概念的例子,或者给我一些关于如何使用 std::experimental::simd 实现向量指令的指示,我将非常感激。
【问题讨论】:
-
确实,SIMD Mandelbrot 是一个艰难的权衡,因为每个像素都有一个独立的退出条件。您需要手动矢量化,例如直到向量中的所有像素都“转义”(但仍然记录每个第一次发生的 when,例如,如果要对设置正确)。或者跟踪哪些像素当前在向量中,并在它逃逸或其他东西时替换一个(用混合)。但这会增加大量的簿记和改组开销。
-
TL:DR:Mandelbrot 中的数据并行性很难用 SIMD 来利用(除了天真地总是运行到最大迭代,对于集合外的像素非常慢);您可能需要一个 SIMD API 来公开更多特定于机器的操作。哦,这个 C++ 扩展有
bool any_of(const simd_mask<T, Abi>&)和类似的函数来测试vec1 < vec2simd_mask 结果,就像 x86 的movmskpd(_mm_movemask_pd) 让你在所有/任何每个元素的比较结果上进行分支。所以你可以用它来实现 Mandelbrot,但我建议先选择一个对 SIMD 更友好的问题。 -
如果你以前没有做过任何 SIMD 的东西,试试向量点积。或数组的线性搜索。 (对于线程级并行性,这些都与 OpenMP 正交,尽管在大多数编译器中,数组点积可以使用 OMP simd 自动矢量化。)
-
@PeterCordes 感谢您提供的信息!很明显,由于您提到的原因,在 Mandelbrot 集中没有得到完美的缩放。出于我的目的,如果使用
std::simd的幼稚实现是悲观而不是优化,那仍然很好。与使用 OpenACC 一样,通常只添加一个相当于“仅使用 GPU”的 pragma 会导致严重的性能损失,并且通常比 CPU 实现慢得多。因为我想写一些有启发性的例子,展示你不相信编译器会做你认为它会做的事情的情况。 -
@PeterCordes 这也是为什么向量内积对我来说不是一个很好的例子,因为编译器通常会做正确的事情。这会给人一种感觉,矢量化就像添加一个杂注一样简单,或者只是信任 icc 的自动矢量化。这有时是正确的,但通常不是。
标签: c++ g++ openmp simd c++-experimental