Понимание результатов параллельного профилирования VS2010 C#
У меня есть программа со многими независимыми вычислениями, поэтому я решил распараллелить ее.
Я использую Parallel.For/Each.
Для двухъядерной машины результаты были хорошими - загрузка ЦП составляла примерно 80-90% большую часть времени. Тем не менее, с двумя машинами Xeon (то есть с 8 ядрами) я получаю лишь около 30%-40% загрузки ЦП, хотя программа тратит довольно много времени (иногда более 10 секунд) на параллельные секции, и я вижу, что она использует на 20-30 больше тем в этих разделах по сравнению с серийными разделами. Каждый поток занимает больше 1 секунды, поэтому я не вижу причин для того, чтобы они не работали параллельно - если только не возникла проблема с синхронизацией.
Я использовал встроенный профилировщик VS2010, и результаты странные. Несмотря на то, что я использую блокировки только в одном месте, профилировщик сообщает, что около 85% времени программы тратится на синхронизацию (также 5-7% сна, 5-7% выполнения при менее 1% ввода-вывода).
Заблокированный код - это всего лишь кеш (словарь) get/add:
bool esn_found;
lock (lock_load_esn)
esn_found = cache.TryGetValue(st, out esn);
if(!esn_found)
{
esn = pData.esa_inv_idx.esa[term_idx];
esn.populate(pData.esa_inv_idx.datafile);
lock (lock_load_esn)
{
if (!cache.ContainsKey(st))
cache.Add(st, esn);
}
}
lock_load_esn
является статическим членом класса типа Object. esn.populate
читает из файла, используя отдельный StreamReader для каждого потока.
Однако, когда я нажимаю кнопку "Синхронизация", чтобы увидеть, что вызывает наибольшую задержку, я вижу, что профилировщик сообщает о строках, которые являются строками входа функции, и не сообщает о самих заблокированных секциях.
Он даже не сообщает о функции, которая содержит вышеуказанный код (напоминание - единственная блокировка в программе) как часть профиля блокировки с уровнем шума 2%. С уровнем шума в 0% он сообщает обо всех функциях программы, которые я не понимаю, почему они считаются блокирующими синхронизацию.
Итак, мой вопрос - что здесь происходит?
Как может быть так, что 85% времени уходит на синхронизацию?
Как мне узнать, в чем на самом деле проблема с параллельными разделами моей программы?
Благодарю.
Обновление: после углубления в потоки (с использованием чрезвычайно полезного визуализатора) я обнаружил, что большая часть времени синхронизации была потрачена на ожидание, пока поток GC завершит выделение памяти, и что требовались частые выделения из-за операций изменения размера общих структур данных,
Мне нужно будет посмотреть, как инициализировать мои структуры данных, чтобы они выделяли достаточно памяти при инициализации, возможно, избегая этой гонки за потоком GC.
Я сообщу о результатах позже сегодня.
Обновление: кажется, что выделение памяти действительно было причиной проблемы. Когда я использовал начальные возможности для всех словарей и списков в параллельно выполняемом классе, проблема синхронизации была меньше. Теперь у меня было только около 80% времени синхронизации с пиками 70% загрузки ЦП (предыдущие пики были только около 40%).
Я углубился в каждый поток и обнаружил, что теперь много вызовов GC allocate были сделаны для выделения небольших объектов, которые не были частью больших словарей.
Я решил эту проблему, предоставив каждому потоку пул предварительно выделенных таких объектов, который я использую вместо вызова "новой" функции.
Поэтому я, по сути, реализовал отдельный пул памяти для каждого потока, но очень грубо, что отнимает много времени и на самом деле не очень хорошо - мне все еще приходится использовать много нового для инициализации этих объектов, только теперь я сделайте это один раз глобально, и в потоке GC будет меньше конфликтов, даже если потребуется увеличить размер пула.
Но это определенно не то решение, которое мне нравится, так как оно не обобщается легко, и я не хотел бы писать свой собственный менеджер памяти.
Есть ли способ сказать.NET выделить заранее определенный объем памяти для каждого потока, а затем взять все выделения памяти из локального пула?
1 ответ
Вы можете выделить меньше?
У меня было несколько подобных опытов, когда я смотрел на плохую работу и обнаружил, что суть проблемы - это GC. Тем не менее, в каждом случае я обнаруживал, что я случайно терял память в каком-то внутреннем цикле, без необходимости выделяя тонны временных объектов. Я бы внимательно посмотрел код и посмотрел, есть ли выделения, которые можно удалить. Я думаю, что программы редко "нуждаются" в значительном распределении во внутренних циклах.