В данном проекте реализована визуализация множества Мандельброта наиболее эффективным способом. Для построения использовалась графическая библиотека SFML, для увеличения производительности использовались AVX инструкции.
Точки множества Мандельброта удовлетворяют рекуррентному соотношению:
Цвет, в который закрашивалась точка, зависел от номера итерации, на котором завершился цикл "подсчета точки". Вычисление номера итерации:
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 инструкции вместо последовательного подсчета каждой точки позволяют обрабатывать несколько точек одновременно. Таким образом, максимальное число итераций цикла уменьшается в 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 будем, сравнивая ее с функцией 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 можно найти тактовую частоту
Интересный факт: текущее значение тактовой частоты из настроек 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 выгоднее с указанием флагов оптимизации (иначе прирост оптимизации небольшой, хоть он и есть).