вопрос про кеширование процессором и треды

elenangel

Есть рукописный пул тредов, в котором 1 тред раздает задания нескольким другим. Остальные треды хранятся в списке (FIFO, std::list<SomeThread*>).
Если я меняю FIFO на LIFO, то наблюдаю незначительный прирост производительности, время обработки некоторого набора эвентов уменьшается примерно с 65 микросекунд до 55 микросекунд на эвент.
Чем может объясняться такой эффект?
пример результатов запуска cachegrind с FIFO:
 
==14577== 
==14577== I refs: 286,060,949
==14577== I1 misses: 2,512,624
==14577== LLi misses: 7,639
==14577== I1 miss rate: 0.87%
==14577== LLi miss rate: 0.00%
==14577==
==14577== D refs: 134,935,329 (84,278,133 rd + 50,657,196 wr)
==14577== D1 misses: 3,467,114 ( 2,930,082 rd + 537,032 wr)
==14577== LLd misses: 241,198 ( 109,725 rd + 131,473 wr)
==14577== D1 miss rate: 2.5% ( 3.4% + 1.0% )
==14577== LLd miss rate: 0.1% ( 0.1% + 0.2% )
==14577==
==14577== LL refs: 5,979,738 ( 5,442,706 rd + 537,032 wr)
==14577== LL misses: 248,837 ( 117,364 rd + 131,473 wr)
==14577== LL miss rate: 0.0% ( 0.0% + 0.2% )

пример результатов запуска cachegrind с LIFO:
 
==15226== 
==15226== I refs: 285,875,473
==15226== I1 misses: 2,516,969
==15226== LLi misses: 7,445
==15226== I1 miss rate: 0.88%
==15226== LLi miss rate: 0.00%
==15226==
==15226== D refs: 134,898,317 (84,276,542 rd + 50,621,775 wr)
==15226== D1 misses: 3,347,517 ( 2,854,606 rd + 492,911 wr)
==15226== LLd misses: 20,643 ( 10,493 rd + 10,150 wr)
==15226== D1 miss rate: 2.4% ( 3.3% + 0.9% )
==15226== LLd miss rate: 0.0% ( 0.0% + 0.0% )
==15226==
==15226== LL refs: 5,864,486 ( 5,371,575 rd + 492,911 wr)
==15226== LL misses: 28,088 ( 17,938 rd + 10,150 wr)
==15226== LL miss rate: 0.0% ( 0.0% + 0.0% )

Dasar

данные треда с большей вероятностью остаются в кэше проца?

elenangel

с одной стороны да, но с другой - это же пулл, и тред каждый раз обрабатывает новую порцию данных, которая была только что создана другим тредом и положена в очередь событий.
кроме того, прогон под valgrind --tool=cachegrind выдает странную картинку, что в случае LIFO все чуть-чуть хуже, но один суммарный параметр существенно лучше.
проблема в том, что я плохо знаю cachegrind и не могу правильно интерпретировать результат, фактически я его первый раз в жизни запустил.

Dasar

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

elenangel

добавил вывод cachegrind

Maurog

LL misses: 28,088
уменьшилось на порядок
данных довольно мало для телепатии :(
могу накинуть такой вариант : часто идет поюз последнего потока (если я понял что подразумевается под LIFO) с данными, которые размещаются в той же области области памяти, где и работал этот поток. то есть сказывается локальность рабочих данных для этого потока.

elenangel

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

elenangel

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

а вот это хз, каждый раз новая порция данных обрабатывается, которая передается через очередь по указателю, который был выделен по new.
разве что служебные данные pthread + какие-то байты от классовой обёртки над pthread - pImpl и еще несколько переменных типа pthread_t и флагов detached и пары других, но с ними вроде как не ведётся активной работы.

Maurog

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

elenangel

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

elenangel

фактически, в случае LIFO, работают 3 треда - главный тред, который крутит цикл создания эвентов, диспетчер, который разбирает очереди и раздаёт тредам пула (в данном случае очередь строго одна) и один из тредов пула, который то кладут в LIFO, то забирают оттуда
в случае FIFO роль третьего треда поочередно выполняют все треды пула.

Maurog

ивенты генерятся подряд, но между генерациями делается пауза, чтобы не забить очередь, иначе будет считаться время ожидания эвента в очереди, а не время между передачей между тредами (тест изначально делался для измерения latency)
в данном тесте ожидания события обработки в явном виде нету, просто между посылками выдерживается достаточно большая пауза которой "хватит всем"
мы решали эту задачу через портирование дисраптора, результами похвастаться не могу, т.к. не знаю их
если ивент уничтожается, то аллокатор будет реюзать эту память для новых объектов. это сделано для уменьшения фрагментации памяти. пул объектов в любом случае рекомендую попробовать заюзать, удаление объектов - одна из самых тяжелых операций, у нас для этого отдельный консьюмер есть на шине, который тупо деструктор зовет

elenangel

я пока только читал про дисраптор, разобраться как оно работает толком не смог, но пока мы в него не уперлись, у нас было пожелание 5k eps, пока хватает и основной упор конечно в тормоза интерпретатора, сложную обработку "боевых" эвентов и различные глобальные блокировки из-за некоторых ошибок в архитектуре.
думаю, рано или поздно придется оптимизировать и пул, но до этого еще далеко.
просто мне подумалось что надо в одном конкретном месте в пуле сделать LIFO вместо FIFO, которое там исторически сложилось, но надо обосновать коллеге почему это будет быстрее.
собственно тест показал, что небольшой профит есть, теперь хотелось бы понять почему, а то получается вуду-оптимизация какая-то.

Maurog

просто мне подумалось что надо в одном конкретном месте в пуле сделать LIFO вместо FIFO, которое там исторически сложилось, но надо обосновать коллеге почему это будет быстрее.
для этого надо более реальный тест сделать. текущий - слишком синтетический : странный пул, в которым используется только один поток. ну и std::list мне показался странным. у вас там динамика есть какая-то в плане добавления\удалений потоков или занятые потоки удаляются из этого списка ?

elenangel

в пуле 64 потока, свободные потоки ждут старта в условной переменной и указатели на них лежат в std::list, занятые потоки после выполнения задания уведомляют "пастуха" потоков и он их кладет обратно в list, оттуда же достает поток, когда находит новый job

evolet

у тебя что именно уменьшается, averageTime или время исполнения твоей проги?
могу предложить попробовать замерить разницу по временам в случаях FIFO и LIFO при следующих правках (по отдельности):
1. увелич существенно время uSleep, чтобы с хорошим запасом рабочий поток успевал обработать евент
2. сделай, чтобы у каджого потока был свой averageTime (где он текущую сумму считает) и count, и чтобы эти 2 переменные занимали всю страницу (не помню, какой там нынче рамер кэшлайна, но 4к хватит с запасом :) ) - а итоговое среднее пусть считается один раз в конце - чтоб не париться, для каждого потока ебни как-нить так:
struct th_counter {
int p1[500];
int sum;
int count;
int p2[500];
}
3. запусти прогу с привязкой на 1 фикс ядро, чтобы все потоки гарантированно работали по очереди
исходя из результатов иомно имхо что-то сказать, если повезет:)

elenangel

averageTime уменьшается, примерно на 20%, то есть было 60-70мкс, становится 50-55мкс

elenangel

uSleep уже достаточно велик, проверено, раньше время передачи-обработки было 150-200-250 мкс, с тех пор дооптимизировали сначала до 80, потом до 65.
не то, чтобы этот тест показывал абсолютную производительность, но корреляция со скоростью работы в целом есть.

evolet

вообще я тут подумал, наверное, не может 10 микросекунд вылезти из-за единичных cach miss'ов , а вот если как-то так получается , что в FIFO большее число раз потоки на фьютексе засыпают - вполне может
так что я бы с taskset'ом попробовал бы, ну и попытался бы покурить на результаты strace'ов (хотя не факт, что под strace'ом картина не поменяется, но всегда можно пробовать писать в файл и надеяться на чудо :))
upd
------
ну и usleep на секунду я бы тоже попробовал бы, а то там могут оказаться всякие неочевидные завязки на линуксовый квант времни планировщика (который я тоже не помню скока, но секунды точно хватит! :))

Marinavo_0507

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

Anturag

Если я меняю FIFO на LIFO, то наблюдаю незначительный прирост производительности, время обработки некоторого набора эвентов уменьшается примерно с 65 микросекунд до 55 микросекунд на эвент.
Чем может объясняться такой эффект?
Так а что тебя удивляет? То, что в LIFO меньше cache misses, чем в FIFO, или то, что уменьшение количества cache misses увеличивает производительность? :confused:

Anturag

 
ообще я тут подумал, наверное, не может 10 микросекунд вылезти из-за единичных cach miss'ов
Почему нет? У топикстартера разница в 10 раз у last level cache misses.
On a modern machine, an L1 miss will typically cost around 10 cycles, an LL miss can cost as much as 200 cycles
Считайте.

evolet

Почему нет? У топикстартера разница в 10 раз у last level cache misses.
само по себе это не говорит, что ухудщения из-за миссов, если 0.0001% увеличить хоть в 100 раз - это ни на что не повлияет

katrin2201

в случае FIFO роль третьего треда поочередно выполняют все треды пула.
Когда fifo - кэшлайн\ы куда идет запись гоняются по интерконнекту туда-сюда.
В дизрапторе одна из вещей, позволяющая добиться той производительности, это то, что в определенный расшаренный участок памяти пишет только один тред, что минимизирует нагрузку на интерконнект.
Там есть возможность мониторить загрузку интерконнекта в рантайме, так что можно проверить не утыкается ли все в него.

Evgeny_T

Тред не осилил, но сделал вывод, что у прогеров этого форума суровые аватары.

bleyman

и основной упор конечно в тормоза интерпретатора
Перехуячъ его чтобы компилировал при помощи LLVM. Даже лучше перепиши на Питоне, с использованием llvmpy, в процессе интерпретатор упростится в миллион раз и у тебя появится возможность нормально добавлять разные интересные оптимизации и фичи.
Trigger Warning: я на самом деле не юзал llvmpy, только читал про неё.
Оставить комментарий
Имя или ник:
Комментарий: