size_t iterations {0};
const size_t max_iterations {100000
while (abs(z) < 2 && iterations < max_iterations) {
++iterations;
z = pow(z, 2) + c;
}
return iterations;
}
4. Далее перед нами часть функции main
int main()
{
const size_t w {100};
const size_t h {40};
auto scale (scaled_cmplx(
scaler(0, w, -2.0, 1.0),
scaler(0, h, -1.0, 1.0)
));
auto i_to_xy ([=](int x) {
return scale(x % w, x / w);
});
5. В функции to_iteration_count
mandelbrot_iterations(x_to_xy(x))
непосредственно, этот вызов делается асинхронно с помощью std::async
: auto to_iteration_count ([=](int x) {
return async(launch::async,
mandelbrot_iterations, i_to_xy(x));
});
6. Перед внесением последнего изменения функция to_iteration_count
future
, которая станет содержать то же значение позднее, когда оно будет рассчитано асинхронно. Из-за этого требуется вектор для хранения всех значений типа future
, добавим его. Выходной итератор, который мы предоставили функции transform
в качестве третьего аргумента, должен быть начальным итератором для нового вектора выходных данных r
: vector
vector
iota(begin(v), end(v), 0);
transform(begin(v), end(v), begin(r
to_iteration_count);
7. Вызов accumulate
size_t
в качестве второго аргумента, вместо этого он получает значения типа future
. Нужно адаптировать его к данному типу (если бы мы изначально использовали тип auto&
, то этого бы не требовалось), а затем вызвать x.get()
, где мы получили доступ к x
, чтобы дождаться появления значения. auto binfunc ([w, n{0}] (auto output_it, future
mutable {
*++output_it = (x.get()
if (++n % w == 0) { ++output_it = '\n'; }
return output_it;
});
accumulate(begin(r), end(r),
ostream_iterator
}
8. Компиляция и запуск программы дадут результат, который мы видели ранее. Увеличение количества итераций и для оригинальной версии приведет к тому, что распараллеленная версия отработает быстрее. На моем компьютере, имеющем четыре ядра ЦП, поддерживающих гиперпараллельность (что дает восемь виртуальных ядер), я получаю разные результаты для GCC и clang. В лучшем случае программа ускоряется в 5,3 раза, а в худшем — в 3,8. Результаты, конечно, будут различаться для разных машин.
Как это работает
Сначала важно понять всю программу, поскольку далее становится ясно, что вся работа, зависящая от ЦП, происходит в одной строке кода в функции main
transform(begin(v), end(v), begin(r), to_iteration_count);
Вектор v
r
.В оригинальной программе в данной строке тратится все время, требуемое для расчета фрактального изображения. Весь код, находящийся перед ним, выполняет подготовку, а весь код, следующий за ним, нужен лишь для вывода информации на экран. Это значит, что распараллеливание выполнения этой строки критически важно для производительности.
Одним из возможных подходов к распараллеливанию является разбиение всего линейного диапазона от begin(v)
end(v)
на фрагменты одного размера и равномерное их распределение между ядрами. Таким образом, все ядра выполнят одинаковый объем работы. Если бы мы использовали параллельную версию функции std::transform
с параллельной политикой выполнения, то все бы так и произошло. К сожалению, это неверная стратегия для