У меня есть программа со многими независимыми вычислениями, поэтому я решил распараллелить ее.
Я использую Parallel.For/Every.
Результаты были хороши для использования двухъядерной машины - загрузка процессора примерно в 80% -90% в большинстве случаев. Однако с двойной машиной Xeon (т.е. с 8 ядрами) я получаю только около 30% -40% загрузки процессора, хотя программа тратит довольно много времени (иногда более 10 секунд) на параллельные разделы, и я вижу, что она использует около 20-30 больше потоков в этих разделах по сравнению с последовательными разделами. Каждый поток занимает более 1 секунды для завершения, поэтому я не вижу причин, чтобы они не работали параллельно - если нет проблемы с синхронизацией.
Я использовал встроенный профайлер VS2010, и результаты странные. Несмотря на то, что я использую блокировки только в одном месте, профилировщик сообщает, что около 85% времени программы тратится на синхронизацию (также 5-7% спящего режима, 5-7% выполнения, менее 1% IO).
Заблокированный код - это только кеш (словарь) 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 были выделены для выделения небольших объектов, которые не были частью больших словарей.
Я решил эту проблему, предоставив каждому потоку пул preallocated таких объектов, который я использую вместо вызова "новой" функции.
Итак, я по существу реализовал отдельный пул памяти для каждого потока, но очень грубым способом, который очень трудоемкий и на самом деле не очень хороший - мне все еще нужно использовать много нового для инициализации этих объектов, только теперь я делаю это один раз в глобальном масштабе, и в GC поток меньше, даже когда приходится увеличивать размер пула.
Но это определенно не решение, которое мне нравится, поскольку оно не обобщается легко, и я бы не хотел писать свой собственный менеджер памяти.
Есть ли способ сказать .NET распределить предопределенный объем памяти для каждого потока, а затем взять все распределения памяти из локального пула?