Каков наилучший способ определить количество потоков, запускаемых на машине с n ядрами? (C ++) - PullRequest
20 голосов
/ 17 января 2012

У меня есть vector<int> с 10 000 000 (10 миллионов) элементов, и что моя рабочая станция имеет четыре ядра. Есть функция с именем ThrFunc, которая работает с целым числом. Предположим, что время выполнения для ThrFunc для каждого целого числа в vector<int> примерно одинаково.

Как определить оптимальное количество потоков для запуска? Является ли ответ столь же простым, как количество элементов, деленное на количество ядер? Или есть более тонкие вычисления?

Редактирование для предоставления дополнительной информации

  • Нет необходимости в блокировке; каждый вызов функции требует только чтения доступ

Ответы [ 6 ]

23 голосов
/ 17 января 2012

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

В более абстрактных терминах вы хотите максимально возможную пропускную способность. Получение максимальной пропускной способности требует наименьшего количества конфликтных точек между потоками (поскольку исходная проблема тривиально распараллеливается). Число точек разногласий, вероятно, будет числом потоков, совместно использующих ядро, или вдвое больше, поскольку ядро ​​может выполнять один или два логических потока (два с гиперпоточностью).

Если ваша рабочая нагрузка использует ресурс, для которого у вас имеется менее четырех доступных (ALU на Bulldozer? Доступ к жесткому диску?), То количество потоков, которые вы должны создать, будет ограничено этим.

Лучший способ найти правильный ответ - проверить все вопросы по аппаратному обеспечению.

11 голосов
/ 17 января 2012

Ответ Бореалида включает в себя тест и выяснение , которое невозможно превзойти по совету.

Но, возможно, есть еще кое-что для тестирования, чем вы думаете: вы хотите, чтобы ваши потоки по возможности избегали конфликта данных. Если данные полностью доступны только для чтения, вы можете увидеть лучшую производительность, если ваши потоки обращаются к «похожим» данным - обязательно просматривая данные небольшими блоками за раз, поэтому каждый поток получает доступ к данным из одни и те же страницы снова и снова . Если данные полностью только для чтения, то нет проблем, если каждое ядро ​​получит свою собственную копию строк кэша. (Хотя это может не максимально использовать кеш каждого ядра.)

Если данные каким-либо образом изменены, то вы увидите значительное улучшение производительности, если будете на большом расстоянии от отделять друг от друга. Большинство кэшей хранят данные вдоль строк кэша , и вы отчаянно хотите, чтобы каждая строка кэша не отскакивала между процессорами для обеспечения хорошей производительности. В этом случае вы можете захотеть, чтобы разные потоки работали с данными, которые на самом деле далеко друг от друга, чтобы никогда не сталкиваться друг с другом.

Итак: если вы обновляете данные во время работы с ними, я бы рекомендовал иметь N или 2 * N потоков выполнения (для N ядер), начиная их с SIZE / N * M в качестве отправной точки, для потоки от 0 до M. (0, 1000, 2000, 3000, для четырех потоков и 4000 объектов данных.) Это даст вам наилучшую возможность подачи различных строк кэша к каждому ядру и позволит обновлениям проходить без отскока строк кэша:

+--------------+---------------+--------------+---------------+--- ...
| first thread | second thread | third thread | fourth thread | first ...
+--------------+---------------+--------------+---------------+--- ...

Если вы не обновляете данные во время работы с ними, вы можете запустить N или 2 * N потоков выполнения (для N ядер), начиная с 0, 1, 2, 3 и т. Д. И перемещая каждый элемент вперед на N или 2 * N элементов с каждой итерацией. Это позволит кеш-системе извлекать каждую страницу из памяти по одному разу, заполнять кеш ЦП практически идентичными данными и, как мы надеемся, заполнять каждое ядро ​​свежими данными.

+-----------------------------------------------------+
| 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 ... |
+-----------------------------------------------------+

Я также рекомендую использовать sched_setaffinity(2) непосредственно в вашем коде для принудительного различных потоков к их собственным процессорам. По моему опыту, Linux стремится сохранить каждый поток на своем исходном процессоре настолько, чтобы не переносить задачи на другие ядра, которые в противном случае простаивают.

4 голосов
/ 17 января 2012

Если предположить, что ThrFunc привязан к процессору, то вам нужно, вероятно, один поток на ядро ​​и разделить элементы между ними.

Если в функции есть элемент ввода-вывода, то ответ более сложный, потому что вы можете иметь один или несколько потоков на ядро, ожидающих ввода-вывода, пока выполняется другой. Пройдите несколько тестов и посмотрите, что получится.

2 голосов
/ 17 января 2012

Оптимальное количество потоков должно равняться количеству ядер, и в этом случае вычислительная мощность каждого ядра будет полностью использована, если вычисления для каждого элемента будут независимыми.

1 голос
/ 20 января 2012

Я согласен с предыдущими комментариями.Вы должны запустить тесты, чтобы определить, какое число дает лучшую производительность.Однако это даст только лучшую производительность для конкретной системы, для которой вы оптимизируете.В большинстве сценариев ваша программа будет работать на машинах других людей, в архитектуре которых вы не должны делать слишком много предположений.

Хороший способ для численного определения количества потоков для запуска будет использовать

std::thread::hardware_concurrency()

Это часть C ++ 11 и должна давать количество логических ядер в текущей системе.Под логическими ядрами подразумевается либо физическое количество ядер (в случае, если процессор не поддерживает аппаратные потоки (например, HyperThreading)), либо количество аппаратных потоков.

Существует также функция Boost, которая делает то же самое, см. Программно найти количество ядер на машине .

0 голосов
/ 13 марта 2012

Оптимальное количество ядер (потоков), вероятно, будет определяться тем, когда вы достигнете насыщения системы памяти (кешей и оперативной памяти).Другим фактором, который может вступить в игру, является межъядерная блокировка (блокировка области памяти, к которой другие ядра могут получить доступ, обновление и последующая разблокировка) и насколько она эффективна (как долго блокировка установлена ​​и как частоон заблокирован / разблокирован).

Одно ядро, на котором работает универсальное программное обеспечение, код и данные которого не оптимизированы для многоядерных процессоров, само по себе будет близко к насыщению памяти.Добавление большего количества ядер в таком случае приведет к замедлению работы приложения.

Так что, если ваш код не сильно экономит доступ к памяти, я думаю, ответ на ваш вопрос - один (1).

...