Usando subprocesos OpenMP y std: :( experimental: :) simd para calcular el conjunto de Mandelbrot
Estoy buscando implementar un trazador de conjuntos de Mandelbrot simple usando diferentes tipos de paradigmas de HPC, mostrando sus fortalezas y debilidades y cuán fáciles o difíciles son sus implementaciones. Piense en GPGPU (CUDA / OpenACC / OpenMP4.5), subprocesos / OpenMP y MPI. Y utilice estos ejemplos para que los programadores nuevos en HPC se apoyen y vean cuáles son las posibilidades. La claridad del código es más importante que obtener el máximo rendimiento absoluto del hardware, ese es el segundo paso;)
Debido a que el problema es trivial de paralelizar y las CPU modernas pueden obtener una gran cantidad de rendimiento usando instrucciones vectoriales, también quiero combinar OpenMP y SIMD. Desafortunadamente, simplemente agregar un #pragma omp simd
no produce resultados satisfactorios y el uso de elementos intrínsecos no es muy fácil de usar ni está preparado para el futuro. O bonita .
Afortunadamente, se está trabajando en el estándar C ++ de modo que debería ser más fácil implementar instrucciones vectoriales genéricamente, como se menciona en el TS: "Extensiones para paralelismo, versión 2" , específicamente la sección 9 sobre tipos de datos paralelos. Se puede encontrar una implementación de WIP aquí , que se basa en VC, que se puede encontrar aquí .
Supongamos que tengo la siguiente clase (que se ha cambiado para que sea un poco más simple)
#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();
};
Y la siguiente implementación del computeMandelbrot()
uso de OpenMP
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;
}
}
}
Podemos suponer que las resoluciones en las direcciones xey son múltiplos de 2/4/8 / .., dependiendo de las instrucciones SIMD que usemos.
Desafortunadamente, hay muy poca información disponible en línea sobre std::experimental::simd
. Ni ejemplos no triviales por lo que pude encontrar.
En el repositorio de Vc git, hay una implementación de la calculadora de conjuntos de Mandelbrot, pero es bastante complicada y debido a la falta de comentarios es bastante difícil de seguir.
Está claro que debería cambiar los tipos de datos de los dobles en la función computeMandelbrot()
, pero no estoy seguro de qué. El TS menciona dos nuevos tipos de datos principales para algunos tipos T,
native_simd = std::experimental::simd<T, std::experimental::simd_abi::native>;
y
fixed_size_simd = std::experimental::simd<T, std::experimental::simd_abi::fixed_size<N>>;
Usar native_simd
tiene más sentido, ya que no conozco mis límites en tiempo de compilación. Pero entonces no me queda claro qué representan estos tipos, ¿es un native_simd<double>
doble simple o es una colección de dobles en los que se ejecuta una instrucción vectorial? ¿Y entonces cuántos dobles hay en esta colección?
Si alguien pudiera señalarme ejemplos en los que se utilizan estos conceptos, o darme algunos consejos sobre cómo implementar instrucciones vectoriales usando std :: experimental :: simd, estaría muy agradecido.
Respuestas
Aquí hay una implementación muy básica, que funciona (por lo que puedo decir). La comprobación de qué elementos del vector tienen un valor absoluto superior a 2 se realiza de una forma muy engorrosa e ineficaz. Debe haber una mejor manera de hacer esto, pero aún no la he encontrado.
Obtengo un aumento de rendimiento del 72% en un AMD Ryzen 5 3600 y le doy la opción a g ++ -march=znver2
, que es menos de lo esperado.
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;
}
Todo el código de prueba (a partir de auto test = (...)
) se compila hasta
.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