вопрос про кеширование процессором и треды
данные треда с большей вероятностью остаются в кэше проца?
с одной стороны да, но с другой - это же пулл, и тред каждый раз обрабатывает новую порцию данных, которая была только что создана другим тредом и положена в очередь событий.
кроме того, прогон под valgrind --tool=cachegrind выдает странную картинку, что в случае LIFO все чуть-чуть хуже, но один суммарный параметр существенно лучше.
проблема в том, что я плохо знаю cachegrind и не могу правильно интерпретировать результат, фактически я его первый раз в жизни запустил.
кроме того, прогон под valgrind --tool=cachegrind выдает странную картинку, что в случае LIFO все чуть-чуть хуже, но один суммарный параметр существенно лучше.
проблема в том, что я плохо знаю cachegrind и не могу правильно интерпретировать результат, фактически я его первый раз в жизни запустил.
но с другой - это же пулл, и тред каждый раз обрабатывает новую порцию данныхесть же еще данные самого треда.
добавил вывод cachegrind
LL misses: 28,088уменьшилось на порядок
данных довольно мало для телепатии
могу накинуть такой вариант : часто идет поюз последнего потока (если я понял что подразумевается под LIFO) с данными, которые размещаются в той же области области памяти, где и работал этот поток. то есть сказывается локальность рабочих данных для этого потока.
подкину данных для телепатии.
один тред в цикле формирует сообщения с временем создания и посылает в очередь.
другой тред разбирает очередь и отдает эвент в пул первому свободному треду.
обработка заключается в том, что считаем пришедший эвент и время его прихода и приплюсовываем для вычисления среднего времени.
т.е. фактически весь пул простаивает, из него в любой момент работает только один поток.
кроме того еще работает главный поток и поток-пастух пула, который раздает задания.
один тред в цикле формирует сообщения с временем создания и посылает в очередь.
другой тред разбирает очередь и отдает эвент в пул первому свободному треду.
обработка заключается в том, что считаем пришедший эвент и время его прихода и приплюсовываем для вычисления среднего времени.
т.е. фактически весь пул простаивает, из него в любой момент работает только один поток.
кроме того еще работает главный поток и поток-пастух пула, который раздает задания.
часто идет поюз последнего потока (если я понял что подразумевается под LIFO)ага
с данными, которые размещаются в той же области области памяти, где и работал этот поток. то есть сказывается локальность рабочих данных для этого потока
а вот это хз, каждый раз новая порция данных обрабатывается, которая передается через очередь по указателю, который был выделен по new.
разве что служебные данные pthread + какие-то байты от классовой обёртки над pthread - pImpl и еще несколько переменных типа pthread_t и флагов detached и пары других, но с ними вроде как не ведётся активной работы.
подкину данных для телепатии.ивенты генерятся подряд в цикле в одном из потоков? то есть их адреса скорее всего идут последовательно. если у нас используется только один поток и на одном ядре, то обновление соответствующего кеша может происходить реже, чем нежели мы юзали потоки с разных ядер
один тред в цикле формирует сообщения с временем создания и посылает в очередь.
другой тред разбирает очередь и отдает эвент в пул первому свободному треду.
обработка заключается в том, что считаем пришедший эвент и время его прихода и приплюсовываем для вычисления среднего времени.
я бы порекомендовал тупо увеличить размер ивентов, чтобы каждый занимал как минимум одну кеш линию, и прогнать тесты еще разок
т.е. фактически весь пул простаивает, из него в любой момент работает только один поток.где-то идет ожидание того, что ивент обработался и можно работать на след. ивентом? почему ивенты не обрабатываются сразу всеми потоками из пула? после обработки ивента он уничтожается? или остается в памяти до конца прогона? если уничтожается, то его память может использоваться в след. ивентах и может опять сказаться локализация данных
ивенты генерятся подряд, но между генерациями делается пауза, чтобы не забить очередь, иначе будет считаться время ожидания эвента в очереди, а не время между передачей между тредами (тест изначально делался для измерения latency)
в данном тесте ожидания события обработки в явном виде нету, просто между посылками выдерживается достаточно большая пауза которой "хватит всем"
эвенты не параллелятся между тредами просто по постановке задачи - обработчик эвентов привязывается к некоему адресу, все эвенты приходящие на один и тот же адрес должны обрабатываться строго последовательно.
то, что обработчиков может быть много на разных адресах в данном тесте никак не используется.
после обработки эвент уничтожается, и да, много раз видел во время отладки что эвент выделяется чуть ли не по одному и тому же адресу.
другое дело, что эвент - это контейнер с указателями на переменные, которые не сишные переменные а самодельные, с типами и значениями, полиморфные.
в данном тесте ожидания события обработки в явном виде нету, просто между посылками выдерживается достаточно большая пауза которой "хватит всем"
эвенты не параллелятся между тредами просто по постановке задачи - обработчик эвентов привязывается к некоему адресу, все эвенты приходящие на один и тот же адрес должны обрабатываться строго последовательно.
то, что обработчиков может быть много на разных адресах в данном тесте никак не используется.
после обработки эвент уничтожается, и да, много раз видел во время отладки что эвент выделяется чуть ли не по одному и тому же адресу.
другое дело, что эвент - это контейнер с указателями на переменные, которые не сишные переменные а самодельные, с типами и значениями, полиморфные.
фактически, в случае LIFO, работают 3 треда - главный тред, который крутит цикл создания эвентов, диспетчер, который разбирает очереди и раздаёт тредам пула (в данном случае очередь строго одна) и один из тредов пула, который то кладут в LIFO, то забирают оттуда
в случае FIFO роль третьего треда поочередно выполняют все треды пула.
в случае FIFO роль третьего треда поочередно выполняют все треды пула.
ивенты генерятся подряд, но между генерациями делается пауза, чтобы не забить очередь, иначе будет считаться время ожидания эвента в очереди, а не время между передачей между тредами (тест изначально делался для измерения latency)мы решали эту задачу через портирование дисраптора, результами похвастаться не могу, т.к. не знаю их
в данном тесте ожидания события обработки в явном виде нету, просто между посылками выдерживается достаточно большая пауза которой "хватит всем"
если ивент уничтожается, то аллокатор будет реюзать эту память для новых объектов. это сделано для уменьшения фрагментации памяти. пул объектов в любом случае рекомендую попробовать заюзать, удаление объектов - одна из самых тяжелых операций, у нас для этого отдельный консьюмер есть на шине, который тупо деструктор зовет
я пока только читал про дисраптор, разобраться как оно работает толком не смог, но пока мы в него не уперлись, у нас было пожелание 5k eps, пока хватает и основной упор конечно в тормоза интерпретатора, сложную обработку "боевых" эвентов и различные глобальные блокировки из-за некоторых ошибок в архитектуре.
думаю, рано или поздно придется оптимизировать и пул, но до этого еще далеко.
просто мне подумалось что надо в одном конкретном месте в пуле сделать LIFO вместо FIFO, которое там исторически сложилось, но надо обосновать коллеге почему это будет быстрее.
собственно тест показал, что небольшой профит есть, теперь хотелось бы понять почему, а то получается вуду-оптимизация какая-то.
думаю, рано или поздно придется оптимизировать и пул, но до этого еще далеко.
просто мне подумалось что надо в одном конкретном месте в пуле сделать LIFO вместо FIFO, которое там исторически сложилось, но надо обосновать коллеге почему это будет быстрее.
собственно тест показал, что небольшой профит есть, теперь хотелось бы понять почему, а то получается вуду-оптимизация какая-то.
просто мне подумалось что надо в одном конкретном месте в пуле сделать LIFO вместо FIFO, которое там исторически сложилось, но надо обосновать коллеге почему это будет быстрее.для этого надо более реальный тест сделать. текущий - слишком синтетический : странный пул, в которым используется только один поток. ну и std::list мне показался странным. у вас там динамика есть какая-то в плане добавления\удалений потоков или занятые потоки удаляются из этого списка ?
в пуле 64 потока, свободные потоки ждут старта в условной переменной и указатели на них лежат в std::list, занятые потоки после выполнения задания уведомляют "пастуха" потоков и он их кладет обратно в list, оттуда же достает поток, когда находит новый job
у тебя что именно уменьшается, 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 фикс ядро, чтобы все потоки гарантированно работали по очереди
исходя из результатов иомно имхо что-то сказать, если повезет:)
могу предложить попробовать замерить разницу по временам в случаях FIFO и LIFO при следующих правках (по отдельности):
1. увелич существенно время uSleep, чтобы с хорошим запасом рабочий поток успевал обработать евент
2. сделай, чтобы у каджого потока был свой averageTime (где он текущую сумму считает) и count, и чтобы эти 2 переменные занимали всю страницу (не помню, какой там нынче рамер кэшлайна, но 4к хватит с запасом
) - а итоговое среднее пусть считается один раз в конце - чтоб не париться, для каждого потока ебни как-нить так:struct th_counter {
int p1[500];
int sum;
int count;
int p2[500];
}
3. запусти прогу с привязкой на 1 фикс ядро, чтобы все потоки гарантированно работали по очереди
исходя из результатов иомно имхо что-то сказать, если повезет:)
averageTime уменьшается, примерно на 20%, то есть было 60-70мкс, становится 50-55мкс
uSleep уже достаточно велик, проверено, раньше время передачи-обработки было 150-200-250 мкс, с тех пор дооптимизировали сначала до 80, потом до 65.
не то, чтобы этот тест показывал абсолютную производительность, но корреляция со скоростью работы в целом есть.
не то, чтобы этот тест показывал абсолютную производительность, но корреляция со скоростью работы в целом есть.
вообще я тут подумал, наверное, не может 10 микросекунд вылезти из-за единичных cach miss'ов , а вот если как-то так получается , что в FIFO большее число раз потоки на фьютексе засыпают - вполне может
так что я бы с taskset'ом попробовал бы, ну и попытался бы покурить на результаты strace'ов (хотя не факт, что под strace'ом картина не поменяется, но всегда можно пробовать писать в файл и надеяться на чудо
)
upd
------
ну и usleep на секунду я бы тоже попробовал бы, а то там могут оказаться всякие неочевидные завязки на линуксовый квант времни планировщика (который я тоже не помню скока, но секунды точно хватит!
)
так что я бы с taskset'ом попробовал бы, ну и попытался бы покурить на результаты strace'ов (хотя не факт, что под strace'ом картина не поменяется, но всегда можно пробовать писать в файл и надеяться на чудо
)upd
------
ну и usleep на секунду я бы тоже попробовал бы, а то там могут оказаться всякие неочевидные завязки на линуксовый квант времни планировщика (который я тоже не помню скока, но секунды точно хватит!
)а вот это хз, каждый раз новая порция данных обрабатывается, которая передается через очередь по указателю, который был выделен по new.ну вот небось этот new возвращает то же, что в прошлый раз, а результат используется на том же ядре?
Если я меняю FIFO на LIFO, то наблюдаю незначительный прирост производительности, время обработки некоторого набора эвентов уменьшается примерно с 65 микросекунд до 55 микросекунд на эвент.Так а что тебя удивляет? То, что в LIFO меньше cache misses, чем в FIFO, или то, что уменьшение количества cache misses увеличивает производительность?
Чем может объясняться такой эффект?

ообще я тут подумал, наверное, не может 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Считайте.
Почему нет? У топикстартера разница в 10 раз у last level cache misses.само по себе это не говорит, что ухудщения из-за миссов, если 0.0001% увеличить хоть в 100 раз - это ни на что не повлияет
в случае FIFO роль третьего треда поочередно выполняют все треды пула.Когда fifo - кэшлайн\ы куда идет запись гоняются по интерконнекту туда-сюда.
В дизрапторе одна из вещей, позволяющая добиться той производительности, это то, что в определенный расшаренный участок памяти пишет только один тред, что минимизирует нагрузку на интерконнект.
Там есть возможность мониторить загрузку интерконнекта в рантайме, так что можно проверить не утыкается ли все в него.
Тред не осилил, но сделал вывод, что у прогеров этого форума суровые аватары.
и основной упор конечно в тормоза интерпретатораПерехуячъ его чтобы компилировал при помощи LLVM. Даже лучше перепиши на Питоне, с использованием llvmpy, в процессе интерпретатор упростится в миллион раз и у тебя появится возможность нормально добавлять разные интересные оптимизации и фичи.
Trigger Warning: я на самом деле не юзал llvmpy, только читал про неё.
Оставить комментарий
elenangel
Есть рукописный пул тредов, в котором 1 тред раздает задания нескольким другим. Остальные треды хранятся в списке (FIFO, std::list<SomeThread*>).Если я меняю FIFO на LIFO, то наблюдаю незначительный прирост производительности, время обработки некоторого набора эвентов уменьшается примерно с 65 микросекунд до 55 микросекунд на эвент.
Чем может объясняться такой эффект?
пример результатов запуска cachegrind с FIFO:
пример результатов запуска cachegrind с LIFO: