вопрос про кеширование процессором и треды
данные треда с большей вероятностью остаются в кэше проца?
кроме того, прогон под valgrind --tool=cachegrind выдает странную картинку, что в случае LIFO все чуть-чуть хуже, но один суммарный параметр существенно лучше.
проблема в том, что я плохо знаю cachegrind и не могу правильно интерпретировать результат, фактически я его первый раз в жизни запустил.
но с другой - это же пулл, и тред каждый раз обрабатывает новую порцию данныхесть же еще данные самого треда.
добавил вывод cachegrind
LL misses: 28,088уменьшилось на порядок
данных довольно мало для телепатии
могу накинуть такой вариант : часто идет поюз последнего потока (если я понял что подразумевается под LIFO) с данными, которые размещаются в той же области области памяти, где и работал этот поток. то есть сказывается локальность рабочих данных для этого потока.
один тред в цикле формирует сообщения с временем создания и посылает в очередь.
другой тред разбирает очередь и отдает эвент в пул первому свободному треду.
обработка заключается в том, что считаем пришедший эвент и время его прихода и приплюсовываем для вычисления среднего времени.
т.е. фактически весь пул простаивает, из него в любой момент работает только один поток.
кроме того еще работает главный поток и поток-пастух пула, который раздает задания.
часто идет поюз последнего потока (если я понял что подразумевается под LIFO)ага
с данными, которые размещаются в той же области области памяти, где и работал этот поток. то есть сказывается локальность рабочих данных для этого потока
а вот это хз, каждый раз новая порция данных обрабатывается, которая передается через очередь по указателю, который был выделен по new.
разве что служебные данные pthread + какие-то байты от классовой обёртки над pthread - pImpl и еще несколько переменных типа pthread_t и флагов detached и пары других, но с ними вроде как не ведётся активной работы.
подкину данных для телепатии.ивенты генерятся подряд в цикле в одном из потоков? то есть их адреса скорее всего идут последовательно. если у нас используется только один поток и на одном ядре, то обновление соответствующего кеша может происходить реже, чем нежели мы юзали потоки с разных ядер
один тред в цикле формирует сообщения с временем создания и посылает в очередь.
другой тред разбирает очередь и отдает эвент в пул первому свободному треду.
обработка заключается в том, что считаем пришедший эвент и время его прихода и приплюсовываем для вычисления среднего времени.
я бы порекомендовал тупо увеличить размер ивентов, чтобы каждый занимал как минимум одну кеш линию, и прогнать тесты еще разок
т.е. фактически весь пул простаивает, из него в любой момент работает только один поток.где-то идет ожидание того, что ивент обработался и можно работать на след. ивентом? почему ивенты не обрабатываются сразу всеми потоками из пула? после обработки ивента он уничтожается? или остается в памяти до конца прогона? если уничтожается, то его память может использоваться в след. ивентах и может опять сказаться локализация данных
в данном тесте ожидания события обработки в явном виде нету, просто между посылками выдерживается достаточно большая пауза которой "хватит всем"
эвенты не параллелятся между тредами просто по постановке задачи - обработчик эвентов привязывается к некоему адресу, все эвенты приходящие на один и тот же адрес должны обрабатываться строго последовательно.
то, что обработчиков может быть много на разных адресах в данном тесте никак не используется.
после обработки эвент уничтожается, и да, много раз видел во время отладки что эвент выделяется чуть ли не по одному и тому же адресу.
другое дело, что эвент - это контейнер с указателями на переменные, которые не сишные переменные а самодельные, с типами и значениями, полиморфные.
в случае FIFO роль третьего треда поочередно выполняют все треды пула.
ивенты генерятся подряд, но между генерациями делается пауза, чтобы не забить очередь, иначе будет считаться время ожидания эвента в очереди, а не время между передачей между тредами (тест изначально делался для измерения latency)мы решали эту задачу через портирование дисраптора, результами похвастаться не могу, т.к. не знаю их
в данном тесте ожидания события обработки в явном виде нету, просто между посылками выдерживается достаточно большая пауза которой "хватит всем"
если ивент уничтожается, то аллокатор будет реюзать эту память для новых объектов. это сделано для уменьшения фрагментации памяти. пул объектов в любом случае рекомендую попробовать заюзать, удаление объектов - одна из самых тяжелых операций, у нас для этого отдельный консьюмер есть на шине, который тупо деструктор зовет
думаю, рано или поздно придется оптимизировать и пул, но до этого еще далеко.
просто мне подумалось что надо в одном конкретном месте в пуле сделать LIFO вместо FIFO, которое там исторически сложилось, но надо обосновать коллеге почему это будет быстрее.
собственно тест показал, что небольшой профит есть, теперь хотелось бы понять почему, а то получается вуду-оптимизация какая-то.
просто мне подумалось что надо в одном конкретном месте в пуле сделать LIFO вместо FIFO, которое там исторически сложилось, но надо обосновать коллеге почему это будет быстрее.для этого надо более реальный тест сделать. текущий - слишком синтетический : странный пул, в которым используется только один поток. ну и std::list мне показался странным. у вас там динамика есть какая-то в плане добавления\удалений потоков или занятые потоки удаляются из этого списка ?
в пуле 64 потока, свободные потоки ждут старта в условной переменной и указатели на них лежат в std::list, занятые потоки после выполнения задания уведомляют "пастуха" потоков и он их кладет обратно в list, оттуда же достает поток, когда находит новый job
могу предложить попробовать замерить разницу по временам в случаях 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мкс
не то, чтобы этот тест показывал абсолютную производительность, но корреляция со скоростью работы в целом есть.
так что я бы с 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: