Подтвердить что ты не робот

Каков наилучший способ определить количество потоков для запуска в машине с n ядрами? (С++)

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

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

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

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

Ответ 1

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

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

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

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

Ответ 2

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

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

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

Итак: если вы обновляете данные во время работы над ним, я бы рекомендовал использовать 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 стремится сохранить каждый поток на своем исходном процессоре, так что он не перенесет задачи на другие ядра, которые в противном случае простаивают.

Ответ 3

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

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

Ответ 4

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

Ответ 5

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

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

std::thread::hardware_concurrency()

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

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

Ответ 6

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

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

Поэтому, если ваш код не сильно экономится на обращениях к памяти, я бы предположил, что ответ на ваш вопрос один (1).