Я писал программу, чтобы проиллюстрировать эффекты конкуренции с кешем в многопоточных программах. Мой первый разрез состоял в том, чтобы создать массив long
и показать, как изменение смежных элементов вызывает конфликт. Здесь программа.
const long maxCount = 500000000;
const int numThreads = 4;
const int Multiplier = 1;
static void DoIt()
{
long[] c = new long[Multiplier * numThreads];
var threads = new Thread[numThreads];
// Create the threads
for (int i = 0; i < numThreads; ++i)
{
threads[i] = new Thread((s) =>
{
int x = (int)s;
while (c[x] > 0)
{
--c[x];
}
});
}
// start threads
var sw = Stopwatch.StartNew();
for (int i = 0; i < numThreads; ++i)
{
int z = Multiplier * i;
c[z] = maxCount;
threads[i].Start(z);
}
// Wait for 500 ms and then access the counters.
// This just proves that the threads are actually updating the counters.
Thread.Sleep(500);
for (int i = 0; i < numThreads; ++i)
{
Console.WriteLine(c[Multiplier * i]);
}
// Wait for threads to stop
for (int i = 0; i < numThreads; ++i)
{
threads[i].Join();
}
sw.Stop();
Console.WriteLine();
Console.WriteLine("Elapsed time = {0:N0} ms", sw.ElapsedMilliseconds);
}
Я запускаю Visual Studio 2010, программу, скомпилированную в режиме выпуска, целевую среду .NET 4.0, "Любой процессор" и выполняется в 64-битной среде исполнения без прикрепленного отладчика (Ctrl + F5).
Эта программа работает около 1,700 мс в моей системе с одним потоком. С двумя потоками он занимает более 25 секунд. Полагая, что разница в конфликте кеша, я установил Multipler = 8
и снова запустил. Результат составляет 12 секунд, поэтому утверждение было по крайней мере частью проблемы.
Увеличение Multiplier
выше 8 не повышает производительность.
Для сравнения, похожая программа, которая не использует массив, занимает около 2,200 мс с двумя потоками, когда переменные смежны. Когда я разделяю переменные, две версии потока выполняются за тот же промежуток времени, что и однопоточная версия.
Если проблема связана с избыточным индексированием массива, вы ожидаете, что она появится в однопоточной версии. Мне кажется, что при изменении массива происходит какое-то взаимное исключение, но я не знаю, что это такое.
Глядя на сгенерированный ИЛ, он не очень просвещает. Также не было разборки. Разборка действительно показывает пару вызовов (я думаю) библиотеки времени выполнения, но я не смог войти в них.
В наши дни я не владею windbg или другими низкоуровневыми инструментами отладки. Это было очень долгое время с тех пор, как я нуждался в них. Поэтому я в тупике.
Моя единственная гипотеза прямо сейчас заключается в том, что код времени выполнения устанавливает "грязный" флаг для каждой записи. Похоже, что что-то подобное потребуется для поддержки исключения исключения, если массив изменен во время его перечисления. Но я с готовностью признаю, что у меня нет прямых доказательств, подтверждающих эту гипотезу.
Может ли кто-нибудь сказать мне, что вызывает это значительное замедление?