HTML Diff
0 added 0 removed
Original 2026-01-01
Modified 2026-03-10
1 <p>Теги: c++, многопоточность, разработчик c++, std::random_shuffle, std::rand, std::shuffle, threads_count</p>
1 <p>Теги: c++, многопоточность, разработчик c++, std::random_shuffle, std::rand, std::shuffle, threads_count</p>
2 <p>Пример из практики: изучали мы, значит, многопоточность. Для лучшего понимания и погружения нужно было выполнить простую задачу - написать функцию, которая максимально задействует CPU. Что угодно лишь бы без ввода-вывода. Для простоты можно было добиться того, чтобы на машине для запуска время работы такой функции было около двух секунд. Добившись этого, необходимо было запустить столько функций, сколько ядер.</p>
2 <p>Пример из практики: изучали мы, значит, многопоточность. Для лучшего понимания и погружения нужно было выполнить простую задачу - написать функцию, которая максимально задействует CPU. Что угодно лишь бы без ввода-вывода. Для простоты можно было добиться того, чтобы на машине для запуска время работы такой функции было около двух секунд. Добившись этого, необходимо было запустить столько функций, сколько ядер.</p>
3 <p>Общее время работы измениться не должно было. То есть должно остаться в пределах тех же двух секунд, можно мерить на глаз, никакого подвоха тут нет. Времени на переключение мало, активные фоновые процессы притушим. Суть примера в том, чтобы увидеть, как распределяется вычислительный объём на ядрах.</p>
3 <p>Общее время работы измениться не должно было. То есть должно остаться в пределах тех же двух секунд, можно мерить на глаз, никакого подвоха тут нет. Времени на переключение мало, активные фоновые процессы притушим. Суть примера в том, чтобы увидеть, как распределяется вычислительный объём на ядрах.</p>
4 <p>Итак, запускаем пять таких функций. Напомню, наша функция работает около двух секунд. Это значит, общее время должно получиться около десяти секунд.</p>
4 <p>Итак, запускаем пять таких функций. Напомню, наша функция работает около двух секунд. Это значит, общее время должно получиться около десяти секунд.</p>
5 <h2>Как это проверить?</h2>
5 <h2>Как это проверить?</h2>
6 <p>Запускаем все пять подряд. Время суммируется. Запускаем все пять одновременно, но с разрешением работать только на одном ядре, например командой:</p>
6 <p>Запускаем все пять подряд. Время суммируется. Запускаем все пять одновременно, но с разрешением работать только на одном ядре, например командой:</p>
7 <p>Можем даже воспользоваться высокоточным профайлером (шутка конечно же, но его точности нам хватит):</p>
7 <p>Можем даже воспользоваться высокоточным профайлером (шутка конечно же, но его точности нам хватит):</p>
8 time taskset -c 0 ./a.out<p>Что у нас должно получиться при пяти потоках и разном количестве доступных ядер:</p>
8 time taskset -c 0 ./a.out<p>Что у нас должно получиться при пяти потоках и разном количестве доступных ядер:</p>
9 ядер|секунд -|- 1|10 2| 5 3| 3.3 5| 2<p>Дальнейшее увеличение доступных ядер ни к чему не приведёт.</p>
9 ядер|секунд -|- 1|10 2| 5 3| 3.3 5| 2<p>Дальнейшее увеличение доступных ядер ни к чему не приведёт.</p>
10 <h2>Градусник не врёт, поменяйте тело</h2>
10 <h2>Градусник не врёт, поменяйте тело</h2>
11 <p>Приходит вопрос от слушателя "Что за ерунда?". Не получаются такие замеры, хоть ты тресни. Причём не просто не похожи, а отличаются разительно. Получаю код, начинаем разбираться:</p>
11 <p>Приходит вопрос от слушателя "Что за ерунда?". Не получаются такие замеры, хоть ты тресни. Причём не просто не похожи, а отличаются разительно. Получаю код, начинаем разбираться:</p>
12 int vector_size = 1'000; int repeat = 100'000; int main(int argc, char *argv[]) { int threads_count = start_parsing(argc,argv); std::vector&lt;std::thread&gt; tv; tv.reserve(threads_count); for (int i = 0; i &lt; threads_count; i++) { tv.push_back(std::thread([](){ std::vector&lt;int&gt; v; v.resize(vector_size); std::iota(v.begin(),v.end(), 0); for (int i = 0; i &lt; repeat; i++) { std::random_shuffle(v.begin(),v.end()); } })); } std::cout &lt;&lt; tv.size() &lt;&lt; '\n'; for (auto&amp; t : tv) { t.join(); } }<p>На первый взгляд кажется, всё верно. Запускаем threads_count потоков с функциями, которые оголтело перемешивают элементы вектора. Замеряем без ограничений по ядрам, четыре ядра точно есть.</p>
12 int vector_size = 1'000; int repeat = 100'000; int main(int argc, char *argv[]) { int threads_count = start_parsing(argc,argv); std::vector&lt;std::thread&gt; tv; tv.reserve(threads_count); for (int i = 0; i &lt; threads_count; i++) { tv.push_back(std::thread([](){ std::vector&lt;int&gt; v; v.resize(vector_size); std::iota(v.begin(),v.end(), 0); for (int i = 0; i &lt; repeat; i++) { std::random_shuffle(v.begin(),v.end()); } })); } std::cout &lt;&lt; tv.size() &lt;&lt; '\n'; for (auto&amp; t : tv) { t.join(); } }<p>На первый взгляд кажется, всё верно. Запускаем threads_count потоков с функциями, которые оголтело перемешивают элементы вектора. Замеряем без ограничений по ядрам, четыре ядра точно есть.</p>
13 <p>Одна функция в своём потоке отрабатывает за пять секунд. Запущенные две функции в двух потоках вместо ожидаемых тех же пяти секунд отрабатывают аж за тридцать три. А в четыре потока на четырёх ядрах аж за пятьдесят три.</p>
13 <p>Одна функция в своём потоке отрабатывает за пять секунд. Запущенные две функции в двух потоках вместо ожидаемых тех же пяти секунд отрабатывают аж за тридцать три. А в четыре потока на четырёх ядрах аж за пятьдесят три.</p>
14 <p>Аномалия скрывалась в алгоритме std::random_shuffle, который в используемой библиотеке использовал std::rand в качестве генератора случайных чисел. Замена его на std::shuffle проблему полностью решила и теперь в независимости от количества потоков и наличии свободных ядер время держится в районе пяти секунд.</p>
14 <p>Аномалия скрывалась в алгоритме std::random_shuffle, который в используемой библиотеке использовал std::rand в качестве генератора случайных чисел. Замена его на std::shuffle проблему полностью решила и теперь в независимости от количества потоков и наличии свободных ядер время держится в районе пяти секунд.</p>
15 <h2>Мораль</h2>
15 <h2>Мораль</h2>
16 <p>При написании многопоточного кода пристальное внимание приходится уделять поведению сторонних библиотек. Но это вовсе не означает, что стандартная библиотека должна этой участи избегать, она тоже может подбрасывать сюрпризы.</p>
16 <p>При написании многопоточного кода пристальное внимание приходится уделять поведению сторонних библиотек. Но это вовсе не означает, что стандартная библиотека должна этой участи избегать, она тоже может подбрасывать сюрпризы.</p>
17 <p><em>Есть вопрос? Напишите в комментариях!</em></p>
17 <p><em>Есть вопрос? Напишите в комментариях!</em></p>
18  
18