【问题标题】:Using OpenMP threads and std::(experimental::)simd to compute the Mandelbrot set使用 OpenMP 线程和 std::(experimental::)simd 计算 Mandelbrot 集
【发布时间】: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&lt;T, std::experimental::simd_abi::native&gt;;

fixed_size_simd = std::experimental::simd&lt;T, std::experimental::simd_abi::fixed_size&lt;N&gt;&gt;;

使用native_simd 最有意义,因为我在编译时不知道我的界限。但是我不清楚这些类型代表什么,native_simd&lt;double&gt; 是单个双精度数还是执行向量指令的双精度数集合?那么这个系列有多少双打呢?

如果有人能给我指出使用这些概念的例子,或者给我一些关于如何使用 std::experimental::simd 实现向量指令的指示,我将非常感激。

【问题讨论】:

  • 确实,SIMD Mandelbrot 是一个艰难的权衡,因为每个像素都有一个独立的退出条件。您需要手动矢量化,例如直到向量中的所有像素都“转义”(但仍然记录每个第一次发生的 when,例如,如果要对设置正确)。或者跟踪哪些像素当前在向量中,并在它逃逸或其他东西时替换一个(用混合)。但这会增加大量的簿记和改组开销。
  • TL:DR:Mandelbrot 中的数据并行性很难用 SIMD 来利用(除了天真地总是运行到最大迭代,对于集合外的像素非常慢);您可能需要一个 SIMD API 来公开更多特定于机器的操作。哦,这个 C++ 扩展有 bool any_of(const simd_mask&lt;T, Abi&gt;&amp;) 和类似的函数来测试 vec1 &lt; vec2 simd_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


【解决方案1】:

这是一个非常基本的实现,它有效(据我所知)。测试向量的哪些元素的绝对值大于 2 的方法非常繁琐且效率低下。一定有更好的方法可以做到这一点,但我还没有找到。

我在 AMD Ryzen 5 3600 上获得了大约 72% 的性能提升,并为 g++ 提供了-march=znver2 选项,这低于预期。

template <class T>
void mandelbrot(T xstart, T xend,
                T ystart, T yend)
{
    namespace stdx = std::experimental;

    constexpr auto simdSize = stdx::native_simd<T>().size();
    constexpr unsigned size = 4096;
    constexpr unsigned maxIter = 250;

    assert(size % simdSize == 0);
    unsigned* res = new unsigned[size * size];

    T dx = (xend - xstart) / size;
    T dy = (yend - ystart) / size;

    for (std::size_t row = 0; row != size; ++row)
    {
        T c_imag = ystart + row * dy;
        for (std::size_t col = 0; col != size; col += simdSize)
        {
            stdx::native_simd<T> real{0};
            stdx::native_simd<T> imag{0};
            stdx::native_simd<T> realSquared{0};
            stdx::native_simd<T> imagSquared{0};
            stdx::fixed_size_simd<unsigned, simdSize> iters{0};

            stdx::native_simd<T> c_real;
            for (int idx = 0; idx != simdSize; ++idx)
            {
                c_real[idx] = xstart + (col + idx) * dx;
            }

            for (unsigned iter = 0; iter != maxIter; ++iter)
            {
                realSquared = real * real;
                imagSquared = imag * imag;
                auto isInside = realSquared + imagSquared > stdx::native_simd<T>{4};
                for (int idx = 0; idx != simdSize; ++idx)
                {
                    // if not bigger than 4, increase iters
                    if (!isInside[idx])
                    {
                        iters[idx] += 1;
                    }
                    else
                    {
                        // prevent that they become inf/nan
                        real[idx] = static_cast<T>(4);
                        imag[idx] = static_cast<T>(4);
                    }
                }

                if (stdx::all_of(isInside) )
                {
                    break;
                }        

                imag = static_cast<T>(2.0) * real * imag + c_imag;
                real = realSquared - imagSquared + c_real;
            }
            iters.copy_to(res + row * size + col, stdx::element_aligned);
        }

    }
    delete[] res;
}

整个测试代码(从auto test = (...)开始)编译成

  .L9:
  vmulps ymm1, ymm1, ymm1
  vmulps ymm13, ymm2, ymm2
  xor eax, eax
  vaddps ymm2, ymm13, ymm1
  vcmpltps ymm2, ymm5, ymm2
  vmovaps YMMWORD PTR [rsp+160], ymm2
  jmp .L6
.L3:
  vmovss DWORD PTR [rsp+32+rax], xmm0
  vmovss DWORD PTR [rsp+64+rax], xmm0
  add rax, 4
  cmp rax, 32
  je .L22
.L6:
  vucomiss xmm3, DWORD PTR [rsp+160+rax]
  jp .L3
  jne .L3
  inc DWORD PTR [rsp+96+rax]
  add rax, 4
  cmp rax, 32
  jne .L6

【讨论】:

  • 请注意,比较返回一个全0或全1的向量,即整数0 / -1。 iters -= compare; 递增 0 或 1,即 -(-1)。您希望 asm 为 vcmpltps / vpsubd,使用 FP 比较结果作为 int32_t 或 uint32_t 的向量,因此您可能需要进行一些转换以使编译器对类型感到满意。
  • @PeterCordes 在我的系统上,它是一个全 0 或全 1 的向量(即正数)。所以我可以做iters[idx] += isInside[idx] 但是我仍然有分歧最终导致NaN的问题......我会再玩一点,任何提示都非常感谢!
  • 这不是我得到的。 auto foo (stdx::native_simd&lt;float&gt; v) { return v == v; } 编译为只执行 cmpeqps %xmm0, %xmm0 / ret 的函数。因此它返回一个全为 1 位的向量,它表示一个整数 -1。 (根据英特尔的 asm 手册 felixcloutier.com/x86/cmpps)。您必须执行其他操作才能将其转换为 0x000000000x00000001 而不是 0xffffffff 的向量
  • 如果我按元素打印测试,即std::cout &lt;&lt; test[idx] [0:idx:test.size() - 1],那么我得到 0 或 1。另请参阅此示例:godbolt.org/z/qPxeza 也许我们的意思是别的?
  • 您已经证明 scalar 元素访问比较结果的结果为 bool,但几乎可以肯定有一些方法可以访问比较结果 vector 直接,而不将其布尔化为 0 / 1。就像将其重新解释为 native_simd&lt;unsigned&gt; 向量或其他东西。除非他们削弱它以启用 AVX512 兼容性,否则掩码真的是整数位掩码,而不是矢量元素?嗯,显然 C++ 标准将 simd_mask&lt;T&gt; 指定为具有 bool 元素,甚至它的 copy_to 也好像使用元素访问。 memcpy 有效,但它是一个 hack:godbolt.org/z/xqb5rW
猜你喜欢
  • 2018-06-12
  • 1970-01-01
  • 1970-01-01
  • 2012-02-21
  • 2021-12-17
  • 1970-01-01
  • 2014-10-28
  • 2021-02-28
  • 2013-04-14
相关资源
最近更新 更多