Конференция "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]
    Спасибо, попробую.
  • Awak00m (07.07.18 21:47) [14]
    Вот. Попробовал. Вставил отладочную печать в два места: где каждый поток ждет завершения очередного шага расчета двух своих "соседей" (сверху и снизу) и собственно в сам расчет шага текущего потока. Вставил так, чтобы печать была только, если время выполнения > 100 мс.
    И вот, получается такой лог:
    https://www.dropbox.com/s/xj7ibdk05nq2210/debug.LOG?dl=1

    А сама процедура потока здесь:
    https://www.dropbox.com/s/mw44rx4aesswc9y/laythread.txt?dl=1

    Т.е. в логе нет ни одного места, где сам "полезный" расчет занимал бы > 100 мс, но зато куча ожиданий своих "соседей". Как такое может быть? И ожидание и выдача сигналов "соседям" - все тут в одной этой процедуре. Вроде бы, никуда больше смотреть не нужно. Т.е. такое впечатление, что система занята еще чем-то, что тормозит вообще всю задачу, но тогда непонятно, а почему не тормозится и процесс расчета шага. Пробовал менять приоритет потока, поставил tpHighest. Не влияет. Не представляю, куда еще копать.

    Спасибо всем откликнувшимся.
  • Eraser © (08.07.18 00:52) [15]

    > Awak00m   (07.07.18 21:47) [14]

    в тонкости твоего кода не вдавался, но есть подозрение, что куча потоков ждут/проверяют в бесконечном цикле одну и ту же крит. секцию. это легко проверить, добавь внутрь цикла, но вне крит. секции sleep(0) или sleep(1).
  • Awak00m (08.07.18 15:56) [16]

    > Eraser ©   (08.07.18 00:52) [15]

    Извините, я не понял. И что должно случиться? Какой от этого должен быть эффект? Я вставил и никакой разницы не усмотрел. Вообще, про такую ошибку я понимаю (что такого не должно быть в коде) иначе теряется весь смысл многопоточности.
    Но у меня вроде бы единственное место, где есть вход в критическую секцию, это конструкция вида
         col.EnterCS;
         try
           FIsReady := true;
         finally
           col.LeaveCS;
         end;

    Т.е. FIsReady - это ThreadSafe vaiable. Никаких задержек здесь быть не должно ведь?
  • Дмитрий Белькевич © (10.07.18 19:34) [17]

    > это ThreadSafe vaiable


    зачем синхронизировать доступ к ThreadSafe variable? она уже и так ThreadSafe :)

    У тебя в какой-то части кода идёт синхронизация потоков, которая, вероятно, сильно тормозит общую производительность. Чем менее синхронизированы потоки - тем лучше. То есть, в идеале, каждый поток должен выполняться полностью независимо, тогда будет наилучшая производительность. Синхронизация не улучшает, а ухудшает общую производительность. Однако, она необходима если используются общие данные. Если же данные в каждом потоке свои, то синхронизацию можно вообще не делать. Однако в жизни - так почти никогда не получается. Cинхронизация - это как неизбежное зло, которую стоит делать как можно на меньшее время и на меньшем участке кода потока.
  • Awak00m (24.01.19 17:34) [18]
    Вот, решил закрыть тему.
    Проблема решилась совершенно неожиданным (для меня) образом :)
    Система нитей вообще ни при чем, и с ней все хорошо. Кстати, косвенным подтверждением этого являлось то, что на 2-хядерном все было ОК.
    Проблема в вычислениях. При минимизации методом Ньютона использовался модуль, выкачанный из Инета, с процедурой инверсии матрицы NxN. И там использовались динамические массивы. Т.е. типа такого
    type
     TMatrixData = array of array of Double;

    При замене на статический тип
    type
     TMatrixRow = array[0..RMtx - 1] of Double;
     TWorkMatrixRow = array[0..2 * RMtx - 1] of Double;
     TMatrixData = array[0..RMtx - 1] of TMatrixRow;
     TWorkMatrixData = array[0..RMtx - 1] of TWorkMatrixRow;

    Все начало летать.
    А что, правда SetLength() лочит нити? Вот никогда бы не подумал. И, опять же, как же оно на двух-то ядрах работает и не лочит ничего?

    Все равно, спасибо всем, кто откликнулся.
  • Pavia © (25.01.19 23:03) [19]
    SetLength() при увеличении длины массива вызывает reAllocate - перераспределение памяти, getMem() move freeMem(). Для защиты от многопоточности getMem и freemem обёрнуты мьютексом(спин-локом).

    А то что у вас глючит, то я писал у вас защиты от локов никакой, нет.
  • Awak00m (26.01.19 17:10) [20]

    > Для защиты от многопоточности getMem и freemem обёрнуты
    > мьютексом(спин-локом).

    Понятно. Я по наивности думал, что это решено на уровне ядра оси ))

    > А то что у вас глючит, то я писал у вас защиты от локов
    > никакой, нет.

    Так а что Вы конкретно имеете в виду? Что значит "защита от локов"? Сами нити ведь работают норм, и друг другу не мешают. Друг друга они не лочат. Можете пример привести?
  • Awak00m (27.01.19 11:22) [21]
    > Для защиты от многопоточности getMem и freemem обёрнуты
    > мьютексом(спин-локом).
    А, кажется, я понял. При очень частом обращении к GetMem() / FreeMem() львиная доля времени тратится на них, а не на полезные вычисления. Поэтому и получается простой в нитях. А там, видимо, это относительно часто происходит.
    Но, все равно, непонятно, почему на двух ядрах загрузка 100%, а на восьми - только 3%. Что, при почти одной тактовой частоте i7 2,3 ГГц настолько быстрее "считает", чем Core-II Duo 2,16 ГГц? Или на i7 GetMem() настолько медленнее работает? Казалось бы, а проц-то здесь при чем? ОС везде одна и та же.
 
Конференция "WinAPI" » Многопоточная программа и распределение ядер CPU [D7, WinXP]
Есть новые Нет новых   [118454   +49][b:0][p:0.001]