LINUX.ORG.RU

Реализация дерева зависимостей по данным для многопоточных приложений на C

 , ,


0

2

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

Например если надо посчитать (sin((a+b+c+d)*(e+f+g)))^((a+b+c+d)+(h+i+j)) то можно построить такое дерево:

              (_^_)
               ^ ^
               | |
          +----+ +---+
          |          |
      sin(_)         |
          ^          |
          |          |
        (_*_)      (_+_)
         ^ ^        ^ ^
         | |        | |
    +----+ +-+------+ |
    |        |        |
 (e+f+g) (a+b+c+d) (h+i+j)

Как я себе это представляю:

Надо строить направленный ациклический граф, в каждой вершине храним указатель на следующие вершины(в примере у (a+b+c+d) две следующих вершины: (_*_) и (_+_)); счетчик числа вершин, указывающих на эту вершину, указатель на функцию, которую надо вызвать; массив указателей на данные, которые в эту функцию надо передавать; указатель на область памяти, куда надо записать результат выполнения функции.

Вначале нужно создать очередь, в которую помещаем указатели на те вершины, на которые никакие другие вершины не указывают. Запускаем некоторое количество тредов. По завершени обработки задачи, каждый тред декрементирует счетчик числа вершин в тех вершинах, на которые указывает та вершина, задача из которой была завершена этим тредом(например по завершении (a+b+c+d) надо декрементировать счетчик у вершин (_*_) и (_+_)). Если оказывается что после декрементирования счетчика в только одной вершине оказался 0, тред немедленно приступает к выполнению задачи из той вершины. Если в двух и более счетчиках в вершинах оказался 0, тред добавляет в очередь указатели на все такие вершины, кроме одной, и выполняет задание из той «одной» вершины, которая не добавлена в очередь. Иначе он берет задачу из очереди. Если в очереди ничего нет, тред выставляет особое значение в некоторой переменной, означающее что этот тред бездействует, после чего этот тред сам себе шлет сигнал SIGSTOP. И надо сделать так, чтобы при добавлении задач в очередь, проверялось наличие бездействующих тредов, и слать им сигнал SIGCONT.

Как подобная модель многопоточности называется? Есть ли уже готовые реализации подобного для C или других языков программирования? Через вызов clone() и всякие там CAS спинлоки такое реализовать нетрудно, как мне кажется. Поверх этого можно еще сделать реактивное программирование.

★★★★★

Последнее исправление: SZT (всего исправлений: 1)

Как-то так работает ForkJoinPool в йаве, можешь глянуть, там подробный джавадок с описанием принципов построения.

Даже не сам пул, а весь фреймворк.

И в OpenMP что-то вроде этого было.

cdshines ★★★★★
()
Последнее исправление: cdshines (всего исправлений: 2)
Ответ на: комментарий от cdshines

Мне хочется реализовать реактивное программирование поверх этого. Если брать что-то из готового, то надо чтобы оно было написано на C с использованием только системных вызовов (вроде линуксового вызова clone()) и cпинлоков, чтобы lock-free. Можно ли это впринципе реализовать без блокировок?

SZT ★★★★★
() автор топика

Почитайте о Intel TBB. Она написана на шаблонах С++, но хоть будет от чего оттолкнуться.

tekilla
()

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

Гораздо прикольней анализировать граф и генерить под него код с оптимальным распараллеливанием.

AIv ★★★★★
()
Ответ на: комментарий от AIv

Конечно же речь идет о значительно более масштабных вычислениях, чем приведенные в примере. Есть еще идея задействовать GPU в этом деле, там ядер очень много.

Вопрос только в том, как там реализовать то, что я описал.

SZT ★★★★★
() автор топика
Последнее исправление: SZT (всего исправлений: 1)
Ответ на: комментарий от SZT

А для решения каких задач Вы планируете это использовать? В какой области?

В GPU ядер много, но там модель вычислений другая.

AIv ★★★★★
()
Ответ на: комментарий от AIv

Например для моделирования цифровых логических схем(типа Logisim), запуска Verlilog/VHDL кода, распараллеливания алгоритмов, написанных в функциональном стиле. Вообще это можно использовать в любой области, где задачу в какой-то момент можно разбить на достаточно большие независимые части.

SZT ★★★★★
() автор топика

вангую, что в современных интелах алу и так многоканальное, делает сразу кучу независимых операций за такт.

anonymous
()
Ответ на: комментарий от SZT

Ну тогда еще обмены с памятью учитывайте;-)

AIv ★★★★★
()
Ответ на: комментарий от cdshines

Очень даже причем. Если взять мой пример (sin((a+b+c+d)*(e+f+g)))^((a+b+c+d)+(h+i+j)) и подставить в него числа (sin((1+2+3+4)*(5+6+7)))^((1+2+3+4)+(8+9+10)) выглядеть в псевдокоде это будет так

> {a,b,c,d,e,f,g,h,i,j} = {1,2,3,4,5,6,7,8,9,10}
> res = (sin((a+b+c+d)*(e+f+g)))^((a+b+c+d)+(h+i+j))
> print(res)
< %%результат_вычисления (sin((1+2+3+4)*(5+6+7)))^((1+2+3+4)+(8+9+10))%%
> a = 999
> print(res)
< %%результат_вычисления (sin((999+2+3+4)*(5+6+7)))^((1+2+3+4)+(8+9+10))%%

Чтобы сделать реактивное программирование, надо просто заассоциировать переменную с некоей цепочкой вычислений, хранить промежуточные вычисления для подставленных в формулы (a+b+c+d) (e+f+g) ((a+b+c+d)*(e+f+g)) ... переменных, и когда мы меняем что-то из переменных, которые входят в цепочку вычислений и хотим получить значение, перевычисляетсятся только кусок графа, который зависит от части, в котором была изменена переменная, по зависимостям.

SZT ★★★★★
() автор топика
Последнее исправление: SZT (всего исправлений: 1)

Ты забываешь о том, что в параллельных вычислениях обработка

              (_^_)
               ^ ^
               | |
          +----+ +----------+
          |                 |
      sin(_)                |
          ^                 |
          |                 |
        (_*_)             (_+_)
         ^ ^               ^ ^
         | |               | |
    +----+ +-+        +----+ +--+
    |        |        |         |
 (e+f+g) (a+b+c+d) (a+b+c+d) (h+i+j)
может быть быстрее твоего задания.

AlexVR ★★★★★
()
Ответ на: комментарий от AlexVR

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

             +------ +  
             | TASK 3|
             | (_^_) |
             |  ^ ^  |
             +--| |--+
                | | 
           +----+ +------------+
           |                   |
+--------- | ------ +          |
|      sin(_)       |          |
|          ^        +--------  | -------+
|  TASK 1  |        |  TASK 2  |        |
|        (_*_)      |        (_+_)      |
|         ^ ^       |         ^ ^       |
|         | |       |         | |       |
|    +----+ +-+     |    +----+ +--+    |
|    |        |     |    |         |    |
| (e+f+g) (a+b+c+d) | (a+b+c+d) (h+i+j) |     
|                   |                   |
+-------------------+-------------------+ 

SZT ★★★★★
() автор топика
Ответ на: комментарий от SZT

Если задача memory bound то такая группировка только ухудшит ситуацию, или даже такая группировка может превратить задачу computer bound в memory bound.

AIv ★★★★★
()
Ответ на: комментарий от AIv

Ну это сильно зависит от того, сколько у нас ядер, насколько большие задачи, сколько примерно времени выполняются задачи и насколько это предсказуемо. Можно использовать разные способы разбиения. Скажем, если нам дан большой массив char a[100000] = {2,34, -6, 17 ....} и надо найти сумму всех элементов и у нас всего 4 ядра, то лучше всего разбить массив на 4 равных куска, сделать 4 таска с ними, 4 треда и каждому треду отдать по таску(завершатся они примерно в одно и то же время), чем разбивать например на куски по 50 char-ов и плодить кучу тасков с ними, суммируя промежуточные результаты вычислений.

SZT ★★★★★
() автор топика
Ответ на: комментарий от AIv

Примерно так

char a[100000] = {2,34, -6, 17 ....};
char *p1 = a + 0*(sizeof(a)/4);
char *p2 = a + 1*(sizeof(a)/4);
char *p3 = a + 2*(sizeof(a)/4);
char *p4 = a + 3*(sizeof(a)/4);

SZT ★★★★★
() автор топика
Ответ на: комментарий от SZT

всё уже придумано до нас, чувак

int sum = 0;
int a[100000];

#pragma omp parallel for reduction (+:sum)
for (int i=0; i<sizeof(a)/sizeof(a[0]); ++i) {
  sum += a[i];
}

anonymous
()
Ответ на: комментарий от SZT

Поздравляю, Вы ухудшили итоговую производительность в четыре раза. Читайте про кэширование и структуру памяти.

А вот если в первом треде обрабатывать элементы с номерами 0, 4, 8.. во втором 1, 5, 9... и т.д., то можно даже что то выиграть - хотя суммирование это все равно memory bound, как его ни крути, так что выигрышь будет мизерный (если будет).

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

Just for fun этим конечно можно и нужно заниматься, но я бы посоветовал (простите) что нить почитать для начала про оптимизацию кода и пр. В очень многоих задачах основная проблема это не темп вычислений а медленный доступ к памяти.

AIv ★★★★★
()
Ответ на: комментарий от AIv

Поздравляю, Вы ухудшили итоговую производительность в четыре раза. Читайте про кэширование и структуру памяти.

А я читал :)

Ухудшил я производительность или улучшил, это зависит от того, общий у ядер кэш или нет. Ведь если у каждого ядра свой кеш данных, производительность как раз таки улучшится. https://en.wikipedia.org/wiki/TILE64 например в такой архитектуре у каждого ядра есть свой отдельный кэш, и если раскидать массив на несколько таких ядер как раз по размеру их кэша, это будет очень даже хорошо

SZT ★★★★★
() автор топика
Ответ на: комментарий от yoghurt

В хаскеле это скорее всего реализовано поверх pthread, я же хочу как можно более низкоуровневую реализацию, и чтобы еще на GPU можно было это считать. К тому же в Haskell есть GC, GC - плохо.

SZT ★★★★★
() автор топика
Ответ на: комментарий от SZT

TASK 2

Потенциальный трупут 2.5 такта.

Вырожденное дерево суммы - т.е. реальный трупут твоего таска будет в 5-10раз ниже.

Т.е. утилизация ведра будет 10-20%(реально вообще 2-4%) и выйдешь ты на 100% утилизацию ведра только с 4-х тредов.

Что является абсолютно бесполезной задачей. Я уж не говорю о том, как ты собрался эти таски по 10-20тактов шедулдить - это нереально.

Лучше иди пили шедулер инструкций - он везде говно - спаси мир. Шедулер внутри каменья абсолютно бесполезен. Запили дсл для математики(т.е. без заморочки с потоками выполнения), если лень разбираться с общими случаями в кишках конпелятора.

anonymous
()
Ответ на: комментарий от SZT

Ну Вы для интересу попробуйте неск тестов написать и поглядите что получится;-)

OpenMP Вам кстати выше аноним уже предложил.

Ну 100Кб это нефига не большой массив а суммирование чаров дык вообще извращение... и про AVX не забудьте;-)

AIv ★★★★★
()
Ответ на: комментарий от anonymous

Пфф, это просто ПРИМЕР. Реальные таски будут значительно больше. Совершенно очевидно, что нет смысла так сильно все мельчить

SZT ★★★★★
() автор топика
Ответ на: комментарий от AIv

Ну Вы для интересу попробуйте неск тестов написать и поглядите что получится;-)

Для этого мне процессор подходящий нужен, хе-хе

SZT ★★★★★
() автор топика
Ответ на: комментарий от SZT

В GPU вообще идеология другая, SIMD на SIMD-е стоит и SIMD-ом погоняет.

И Вы попытайтесь себе ответить сначала на вопрос ЗАЧЕМ ЭТО.

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

AIv ★★★★★
()
Ответ на: комментарий от SZT

А Вы на домашнем процессоре - у них знаете ли самое лучшее отношение цена/производительность как правило.

AIv ★★★★★
()
Ответ на: комментарий от AIv

И Вы попытайтесь себе ответить сначала на вопрос ЗАЧЕМ ЭТО.

Чтобы быстро что-то посчитать, нагрузив все ядра

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

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

SZT ★★★★★
() автор топика
Ответ на: комментарий от AIv

элементы с номерами 0, 4, 8.. во втором 1, 5, 9... и т.д.

зачем копировать одно и то же 4 раза? надо выравниваться по размеру строк l1 (для amd) или l2 (для intel) кеша. плюс ассоциативность чтобы не было лишних вытеснений.

anonymous
()
Ответ на: комментарий от SZT

Чтобы быстро что-то посчитать, нагрузив все ядра

Что именно посчитать? Потому что способы распараллеливания зависят очень сильно от специфики задачи, хорошего универсального способа пока еще не придумано. То что есть универсального сливает специализированным решениям на порядки.

Я некоторое время игрался с GCC, изучая его внутреннее представление GIMPLE и это очень даже похоже на символьные вычисления, которые есть в той же Maxima например

Дык AST оно везде AST.

AIv ★★★★★
()
Последнее исправление: AIv (всего исправлений: 1)
Ответ на: комментарий от AIv

Поздравляю, Вы ухудшили итоговую производительность в четыре раза.

Это почему же?

А вот если в первом треде обрабатывать элементы с номерами 0, 4, 8.. во втором 1, 5, 9... и т.д., то можно даже что то выиграть - хотя суммирование это все равно memory bound, как его ни крути,

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

так что выигрышь будет мизерный (если будет).

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

anonymous
()

google://ярусно-параллельная форма алгоритма

good_riddance
()
Ответ на: комментарий от anonymous

Запили дсл для математики

царь решил под умного косить? дсл для математики запилили уже 4000 лет как.

anonymous
()
Ответ на: комментарий от AIv

А вроде компилятор хаскеля ровно то что ТС предлагает и делает? Давно об энтом читал...

Да, так и есть - там раскидывание чистого кода, который, как указал программист, можно считать параллельно, по вычислительным ядрам производится автоматом (как и вычисление их со всеми зависимостями)

yoghurt ★★★★★
()
Ответ на: комментарий от anonymous

Это почему же?

При общем кэше приходиться подкачивать данные из четырех сильно удаленных мест, локализация данных плохая? Ну конечно речь идет о массиве не 100К а зело побольше, и выч. система неск. абстрактная.

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

Не думаю.

По сравнению с чем? Если с один потоком, то в один поток трупут памяти не снять.

Есть еще векторизация.

AIv ★★★★★
()
Ответ на: комментарий от SZT

Реальные таски будут значительно больше.

Значительно - это 3порядка?

Совершенно очевидно, что нет смысла так сильно все мельчить

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

Это уже очень дорого и в считалках нахрен не нужно. Проще распаралелить по данным и всё. Это умеют все и вся уже сотни лет. Разбил на равные части - запустил - отработало всё почти одновременно. Утилизация близка к максимальной.

В чем суть предложения?

anonymous
()
Ответ на: комментарий от AIv

При общем кэше приходиться подкачивать данные из четырех сильно удаленных мест, локализация данных плохая?

Если мы говорим о чтении памяти, то кеширование тут роли не играет. Это просто максимум буфер, а реально лишняя сущность.

Локальность - это про доступ данных в границах не выходящих за определённые рамки. Тут доступ сам по себе не локальный - на каждый рид будет кешмисс.

Тут больше про паттерн доступа к памяти. Но он линейный - всё в порядке. Другое дело, что он четырёх поточный, но по логике вещей это не должно быть проблемой.

Не думаю.

Именно так и есть. Вся линейность памяти в юзерспейсе - иллюзия. Это линейность адресного пространства, а не памяти.

Страницы памяти могут быть из какой угодно её части. Найти линейные области памяти длиннее страницы можно только со старта системы, после же - оно полностью раздомизируется.

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

Есть еще векторизация.

Естественно это с векторизацией. Именно с ней и в идеальном случае трупут памяти один ядром никак не снять.

anonymous
()
Ответ на: комментарий от anonymous

Другое дело, что он четырёх поточный, но по логике вещей это не должно быть проблемой.

Ты про неявный автоматический prefetch? Когда много активных ядер то то ли у intel, то ли у amd он отключается. Возможно, у обоих. Ссылку не дам, но стопудофф читал в ихних developers guide.

true_admin ★★★★★
()
Ответ на: комментарий от anonymous

Локальность - это про доступ данных в границах не выходящих за определённые рамки. Тут доступ сам по себе не локальный - на каждый рид будет кешмисс.

При правильной организации процесса - нет. Даже при неправильной орагнизации надо сильно постараться что бы так было.

Вся линейность памяти в юзерспейсе - иллюзия.

Линейность иллюзия, локальность тем не менее нет. Память иерархическая система, и линейность неплохая в общем то модель (на некотром уровне абстракции).

Остальное комментировать не буду.

AIv ★★★★★
()
Ответ на: комментарий от AIv

При правильной организации процесса - нет.

Как? Датасет больше кеша - т.е. он никак не может быть локализован в кеше. Т.е. для того, чтобы прочитать эти данные - надо их прочитать со скоростью памяти, в которой они лежат.

В кеше может быть только их кусок, вот в рамках этого куска и на множественном доступе кеш уже работает. В данном контексте этого нет.

Даже при неправильной орагнизации надо сильно постараться что бы так было.

Наверное вас смутили циферки в профайлере? Они врут - это влияние оптимизаций загрузок.

Линейность иллюзия, локальность тем не менее нет.

С этим никто не спорит. Надо определиться с терминами. Ниже было про иерархическую структуру - в этом суть. Локальность данных, т.е. датасета задачи в рамках определённого места в этой структуре. В данном случае говорилось про кеш.

Датасет этой задачи уже за рамками кеша, поэтому на скорость чтения данных он НИКАК влиять не может. А кусок кода выше какраз суть этой задачи.

Далее уже идёт влияние на множественное чтение. Т.е. локализация паттерна множественного доступа к памяти. Вы говорите про это - я про иное.

линейность неплохая в общем то модель

Возможно.

anonymous
()
Ответ на: комментарий от true_admin

Ты про неявный автоматический prefetch?

Скорее про объединение и упорядочивание.

Когда много активных ядер то то ли у intel, то ли у amd он отключается. Возможно, у обоих. Ссылку не дам, но стопудофф читал в ихних developers guide.

Зачем там привязка к ядрам? Есть обращения к памяти - а кто конкретно запросил - это не имеет значения.

Скорее всегда там было про какие-то эвристики на длину префетча, либо его не деланье, если линия загружена чем-то ещё. Если линия не загружена, то не делать его смысла мало. Т.е. отдача приоритета реально запрошенным данным, а не предполагаемым.

Ну и это не воспроизводится, по крайней мере на интеле.

anonymous
()
Ответ на: комментарий от anonymous

Т.е. ты это просто так написал? Хорошо.

Я сказать хотел то, что сказал. Чётко и ясно - там одна трактовка. Снять трупут памяти один ведром нельзя.

Если не согласен - выкатывай конкретное обоснование, а не хрен пойми что.

anonymous
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.