vvit19 / Mandelbrot

Mandelbrot set drawing, optimizated with AVX instructions.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Mandelbrot set drawing

Описание проекта

В данном проекте реализована визуализация множества Мандельброта наиболее эффективным способом. Для построения использовалась графическая библиотека SFML, для увеличения производительности использовались AVX инструкции.

Что такое множество Мандельброта?

Точки множества Мандельброта удовлетворяют рекуррентному соотношению: $Z_{n + 1} = Z_n ^ 2 + C_0$, где $Z_0 = 0$, $Z_{i} = X_{i} + i Y_{i}$ ($Z$ - точка на комплексной плоскости), $C_{0}$ - начальная точка (с координатами $x_0$, $y_0$).

Цвет, в который закрашивалась точка, зависел от номера итерации, на котором завершился цикл "подсчета точки". Вычисление номера итерации:

for (int cur_y = 0; cur_y < HEIGHT; ++cur_y)
{
    float c_im = ((float) cur_y - center_y) * scale;

    for (int cur_x = 0; cur_x < WIDTH; ++cur_x)
    {
        float c_real = ((float) cur_x - center_x) * scale;

        int i = 0;
        for (float x = c_real, y = c_im; i < MAX_ITERATIONS; ++i)
        {
            float x_pow = x * x;
            float y_pow = y * y;
            float xy    = x * y;

            float cur_radius = x_pow + y_pow;
            if (cur_radius >= MAX_RADIUS) break;

            x = x_pow - y_pow + c_real;
            y = xy + xy + c_im;             // Z_n = (Z_{n-1})^2 + C_0
        }

        if (i % 2 == 1) pixels_array[pixels_cnt++] = sf::Color::White;
        else pixels_array[pixels_cnt++] = sf::Color::Black;
    }
}

Использование AVX инструкций

AVX инструкции вместо последовательного подсчета каждой точки позволяют обрабатывать несколько точек одновременно. Таким образом, максимальное число итераций цикла уменьшается в 8 раз. Оптимизированный код:

for (__m256 vector_x = c_real_vector, vector_y = c_im_vector; i < MAX_ITERATIONS; ++i)
{
    __m256 x_pow_vector = _mm256_mul_ps (vector_x, vector_x);
    __m256 y_pow_vector = _mm256_mul_ps (vector_y, vector_y);
    __m256 xy_vector    = _mm256_mul_ps (vector_x, vector_y);

    __m256 vector_cur_radius = _mm256_add_ps (x_pow_vector, y_pow_vector);
    if (!CmpVector (vector_cur_radius, max_radius_vector, &iterations_vector)) break;

    vector_x = _mm256_add_ps (x_pow_vector, _mm256_sub_ps (c_real_vector, y_pow_vector));
    vector_y = _mm256_add_ps (c_im_vector,  _mm256_add_ps (xy_vector, xy_vector));
}

Производительность

С помощью sf::Clock и clock.getElapsedTime измерим время работы функций подсчета точек (с AVX/без AVX) в миллисекундах.

No flags -O3
No AVX 106,188 52,132
AVX 32,665 8,032

Примечание: замеры проходят в одинаковых условиях (один режим работы батареи, равное приближение графика).

Однако, во-первых, сразу появляется вопрос, насколько точны средства SFML (в частности, функция clock.getElapsedTime) для вычисления времени.

А во-вторых, в функции подсчета точек присутсвует заполнение массива пикселей:

uint32_t* iterations_array = (uint32_t*) (&iterations_vector);
for (int offset = 0; offset < VECTOR_SIZE; ++offset)
{
    if (iterations_array[offset] % 2 == 1) pixels_array[pixels_cnt++] = sf::Color::White;
    else                                   pixels_array[pixels_cnt++] = sf::Color::Black;
}

В теории, это должно влиять на измеренное время. Рассмотрим эти 2 проблемы детальнее.

Точность SFML в вопросе вычисления времени

Проверять точность функции SFML будем, сравнивая ее с функцией unsigned __int64 __rdtsc() из заголовочного файла intrin.h. Она возвращает метку времени процессора. Метка времени процессора записывает количество циклов с момента последнего сброса. Сравнивать возвращаемые значения rdtsc и clock.getElapsedTime будем при использовании AVX инструкций и флага -O3 в исходной программе. Для достаточного числа данных для сравнения будем изменять число итераций цикла, в котором собственно и считается время. Пример цикла с использованием rdtsc:

for (int i = 0; i < PERFORMANCE_ITERATIONS; i++)
{
    uint64_t tsc_before = __rdtsc ();
    ChooseDrawMandelbrotMode (offset_x, offset_y, scale, pixels_array);
    uint64_t tsc_after  = __rdtsc ();

    total_time += tsc_after - tsc_before;
}

Также для большей точности будем брать среднее значение (для SFML - в секундах, для rdtsc - в тиках процессора) для трех запусков программы для одного значения числа итераций цикла.

PERFORMANCE_ITERATIONS rdtsc clock.getElapsedTime
100 1884733745 0,812
200 3714692996 1,608
300 5562805084 2,413
400 7386785454 3,199
500 9203136702 3,981
600 11045342330 4,785

Теперь построим график зависимости показаний clock.getElapsedTime от показаний rdtsc:

И видно, что точки очень хорошо ложатся на аппроксимирующую прямую, при чем свободный член равен 2 миллисекундам, что является допустимой погрешностью для clock.getElapsedTime. По коэффициенту наклона прямой k можно найти тактовую частоту $&amp;#957$ (вообще говоря, у современных процессоров она переменная, но, по всей видимости, во время исполнения нашей программы ее колебания незначительны):

$&amp;#957 = 1/k = 1/0,4333 = 2,308$ ГГц.

Интересный факт: текущее значение тактовой частоты из настроек Windows также равно 2,3 ГГц:

Подводя итоги имеем, что показатели clock.getElapsedTime линейно зависят от показателей rdtsc и имеют небольшую погрешность, а следовательно, SFML можно применять для расчета времени в данной задаче.

Влияние заполнения массива пикселей на время

Будем рассматривать выполнение программы с AVX инструкциями и оптимизацией -O3. Если при измерении времени просто убрать код заполнения массива пикселей, то оптимизация -O3 уберет весь код функции, время которой нам нужно измерить (потому что в таком случае, результат ее выполнения не будет ни на что влиять). Однако можно присвоить переменной __m256i iterations_vector тип volatile. Это укажет компилятору, что данная переменная может изменяться вне кода нашей программы, что решит нашу проблему.

Пересчитаем время исполнения функции подсчета точки без затрат на заполнение массива пикселей (в скобках указана разница с полученными нами ранее значениями времени):

No flags -O3
No AVX 101,688 (4,42 %) 52,158 (< 0,1 %)
AVX 32,248 (1,29 %) 7,601 (5,67 %)

Видим, что расхождения между полученными значениями незначительны. Следовательно, можно пренебречь влиянием заполнения массива пикселей на время и считать исходные полученные величины верными.

Вывод

Мы доказали, что в данной задаче можно применять средства SFML для подсчета времени исполнения. Также поняли, что даже без флагов оптимизации, использование функции с AVX инструкциями в 1,6 раз быстрее, чем использование обычной функции с -О3. А использование AVX функции с -О3 так вообще обгоняет обычную функцию с -О3 в 6,5 раз.

Очевидно, использовать метод AVX выгоднее с указанием флагов оптимизации (иначе прирост оптимизации небольшой, хоть он и есть).

About

Mandelbrot set drawing, optimizated with AVX instructions.


Languages

Language:C++ 95.3%Language:Makefile 4.7%