Конференция "WinAPI" » Многопоточная программа и распределение ядер CPU [D7, WinXP]
 
  • Awak00m (26.05.18 16:52) [0]
    Здравствуйте всем,
    Проблема следующая: сделал программу, в которой много (порядка 300) потоков. Не все они работают одновременно, но одновременно работает все же очень много (> 100). На разных CPU  загрузка ядер принципиально отличается: на стареньком Core 2 Duo все работает, как часы: оба ядра загружены на 100%, как и ожидалось. А вот на I5 и I7 загрузка CPU где-то около 0, и реально видно, что программа работает медленнее в разы! Попробовал в MainForm.OnCreate() вставить
    SetProcessAffinityMask(GetCurrentProcess(), 20);


    Т.е. как бы сэмулировать Core 2 Duo на I7. Заработало так же, как на Core2, причем интересно, что результат сильно различается в зависимости от маски, т.е. какие именно 2 ядра включать. Если включить 3 ядра, то эффект ускорения пропадает. Аналогично на I5, только там нет виртуальных ядер и поэтому маска другая.

    Схема потоков процесса:
    https://www.dropbox.com/s/h67ouxxukywm18r/threads.jpg?dl=1
    Пунктирной линией обозначено состояние ожидания. Е.е. постоянно работают только TShLayerThread-ы, а остальные только запускают их и ждут их завершения. Всего таких штук (как на картинке) 3. Число TShLayerThread-ов в каждом блоке около 90.

    Между собой TShLayerThread-ы взаимодействуют так:
    https://www.dropbox.com/s/74k345lcshawb1y/layers.jpg?dl=1
    Т.е. они не просто работают все параллельно, а ждут друг друга на каждом шаге расчета. Реально одновременно должно работать около половины. Шагов в каждом цикле порядка 200, хотя вряд ли это важно.

    Непонятно, где именно проблема. Есть нормально работающая программа, где используется только один блок потоков и нет вообще никаких дополнительных (ждущих окончания), т.е. в точности как на второй картинке, и эта программа работает нормально. По крайней мере, на I7 загружена половина ядер, что можно объяснить тем, что реальных ядер у I7 всего 4. И реально там все шустро работает, и видно, к примеру, что на более медленном CPU работает медленнее.

    В какую сторону рыть?

    Программа x64 сделана в XE7, а система Win7x64

    -Спасибо
  • Eraser © (27.05.18 17:35) [1]

    > Awak00m   (26.05.18 16:52) 

    увы, без конкретики тут вряд ли что посоветуешь.
  • dmk © (28.05.18 20:52) [2]
    >эффект ускорения пропадает
    По своему опыт могу сказать, что может быть достигнут предел пропускной способности памяти. В принципе, 2-3 ядра могут загрузить шину намертво. В данном случае добавление ядер не дает никакого прироста. Проц считает намного быстрее, чем пропускает память.
    Помогает или «равномерное размазывание» обмена с памятью (запись в переменные или массивы) или выполнение алгоритма полностью в регистрах.
  • Awak00m (28.05.18 22:05) [3]
    Не понял вот этого: "В принципе, 2-3 ядра могут загрузить шину намертво".
    Т.е. я бы понял Ваше объяснение, если бы, начиная со скольки-то ядер производительность дальше бы не росла. Но я вижу, что при маске на 2 ядра эти ядра загружаются каждое процентов на 50, а если больше ядер задействовать, то просто 0% на всех ядрах и считает медленнее в разы (раза в 3 или 4). Т.е. производительность не достигает предела, а катастрофически падает.
    Это все равно, как если двое работяг работают быстро, а как к ним третьего добавляешь, так они начинают на троих соображать :)) Натурально именно такая ассоциация приходит в голову)

    Да, и я не упомянул еще, что даже при маске на 2 ядра эффект "ускорения" наблюдается не всегда. Как правило, но не всегда. Иногда одно ядро загружается на 100% и тогда вышеупомянутые блоки TShLayerThread-ов выполняются чисто последовательно. Т.е. сначала один прогресс-индикатор проезжает до 100%, потом другой, потом третий. Порядок произвольный, но одновременно работает только один блок. А если "нормально" все пошло на 2-х ядрах, то все три прогресса шустро бегут параллельно. От чего зависит - непонятно. Один раз запустишь - так пошло, другой раз - иначе. Но в процентах 90 случаев два ядра работают параллельно.
    Вот картинки:
    - "нормальная" работа 2-х ядер
    https://www.dropbox.com/s/ct25h2vdf868q1e/2cpu-1.jpg?dl=1
    - когда работает только 1 ядро из двух, но на 100%:
    https://www.dropbox.com/s/666stfenrrzgwat/2cpu-2.jpg?dl=1

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

    Про реализацию на регистрах - это круто, конечно, но метод Ньютона... на регистрах... Нереально.

    Про «равномерное размазывание» обмена с памятью я вообще не понял, что имеется в виду.
  • dmk © (28.05.18 23:07) [4]
    Еще раз: Проц считает намного быстрее, чем пропускает память.
    Процессор при загрузке всего в 40-50% также может забить все пропускную полосу (шину/память) до отказа. Зависит от алгоритма и кол-ву обращений к памяти.
  • Pavia © (28.05.18 23:40) [5]

    > Похоже, что это где-то внутри оси какая-то задница с оптимизацией
    > нитей. Не похоже, что это физический лимит проца. Хотя я,
    >  конечно, не специалист по интелу. Я подумал, что, может,
    >  кто с подобным сталкивался.

    Классика гонка процессов либо дедлок.
  • ку ку (29.05.18 22:45) [6]
    а как к ним третьего добавляешь, так они начинают

    один человек вкручивает лампочку за 30 секунд.
    трое человек не успеют вкрутить эту же лампочку за те же 30 секунд.

    ваш кэп.

    человеки - ядра и их много
    лампочка - шина и она одна.

    а вкручивать хотят все трое одновременно. одну и ту же лампочку. им так велели.
  • han_malign © (01.06.18 15:42) [7]

    > Число TShLayerThread-ов в каждом блоке около 90.

    - создание и переключение потока занимает порядка 1000 тактов.
    Соответственно если, например - суммировать элементы простого целочисленного массива, используя 2 потока на 2 ядрах - получишь ~x2 ускорение.
    А если использовать 100 потоков - для идеального планировщика потоков, если каждый поток справляется со своей задачей за один квант времени - это будут  лишние 100000 тактов, то есть для 100000 элементов - получим ~x2 замедление.
    А это случай чистой параллельности, при правильной реализации - без межпоточной синхронизации, без разделения линий кэша и т.д. ...

    Помимо этого - чем больше обработчиков - тем больше сбоев кэша, что чаще всего -
    > 'может забить все пропускную полосу (шину/память) до отказа'


    Для таких задач используют пул потоков, с рекомендованным лимитом не превышающим количества ядер x 2. Причем x2 - имеет смысл только если в обработчиках есть циклы ожидания(IO например) посередине процесса...

    Главное правило параллельных вычислений - если один вспомогательный поток ждёт второй вспомогательный поток - значит один из них лишний.
  • Дмитрий Белькевич © (05.06.18 23:06) [8]
    загрузка сильно зависит от многих факторов. далеко не всегда упирается в шину. я на многих операциях вижу у себя на i7 существенное распараллеливание многих процессов, до 6-7 раз (4 ядра x hyper-threading = 8 виртуальных).
    если на двух процессорах считает нормально, а даже на трёх - плохо, то скорее всего проблема в каком-то дедлоке. либо потоки начинают драться за какой-то ресурс, либо что-то подобное.
    если бы упиралось в шину то, скорее всего, загрузка была бы равномерно размазана по трем и больше процессорам но был бы их недогруз - то есть равномерная прогрузка на 20-40%, но не ноль.
  • Eraser © (06.06.18 02:26) [9]

    > Дмитрий Белькевич ©   (05.06.18 23:06) [8]

    +1

    если пригрузить шину на 100% это будет сильно заметно по общей деградации производительности системы, читай других приложений.
  • Dimka Maslov © (12.06.18 14:57) [10]
    Лично я обычно делаю так
    1. Большой объём данных разбивается на участки, количество которых строго равно количеству процессоров (в т.ч. виртуальных).
    2. Для каждого блока запускается свой поток.
    3. При разработке алгоритма работы потока основное внимание уделяется минимизации операций синхронизации, ибо в них происходит основное снижение производительности.
  • Awak00m (18.06.18 18:04) [11]
    В общем, спасибо всем откликнувшимся.
    Пока нашел только подтверждение моим догадкам:
    > Дмитрий Белькевич ©   (05.06.18 23:06) [8]

    А с этим:
    > Dimka Maslov ©   (12.06.18 14:57) [10]
    трудно не согласиться, но смущает то, что "основной" алгоритм связи нитей в другой программе работает на ура. А в этой просто сделано некое объединение нескольких таких алгоритмов, которые тоже между собой синхронизируются. Но ведь само ожидание типа WaitForMultipleObjects() с таймаутом в 100 мс ну никак не должно снижать производительность. Или может?

    А кто знает способ понять, где именно стоит многопоточная программа львиную долю времени? И вообще, как-нибудь можно побороть дикие лаги в отладчике х64? При большом количестве потоков это просто смерть как медленно. В смысле сам процесс создания/завершения потоков (видимо) проходя через отладчик адски тормозит. И даже, если просто нажать Program Reset, то дикий лаг на пару минут. Тупо сидишь и втыкаешь. Пробовал чего-то гуглить, но не особо чего нарыл, а то, что нарыл, практически не помогает.
    На х32 такого нет. Но делать х32 приложение ооочень не хочется.
  • Eraser © (19.06.18 22:46) [12]

    > Awak00m   (18.06.18 18:04) [11]


    > А кто знает способ понять, где именно стоит многопоточная
    > программа львиную долю времени?

    есть специальные профилировщики, погугли delphi profiler. но для уверенной работы с ними нужен определенный опыт и сноровка.
    я бы порекомендовал вручную замерять время через тот же GetTickCount, также логировать точки выполнения кода через outputdebugstring, но смотреть вывод не под отладчиком (т.к. высоконагруженный проект просто не будет нормально работать с ним), а через DebugView  https://docs.microsoft.com/en-us/sysinternals/downloads/debugview
    обратить внимание на точки, где простаивают потоки. даю 99% гарантии, что просто логическая ошибка в механизме синхронизации/ожидания.
  • Awak00m (20.06.18 12:12) [13]
    > Eraser ©   (19.06.18 22:46) [12]
    Спасибо, попробую.
 
Конференция "WinAPI" » Многопоточная программа и распределение ядер CPU [D7, WinXP]
Есть новые Нет новых   [92084   +37][b:0.001][p:0.002]