Я писал программу, чтобы проиллюстрировать эффекты конкуренции за кеш в многопоточных программах. Моей первой попыткой было создать массив длиной
и показать, как изменение соседних элементов вызывает разногласия. Вот программа.
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).
Эта программа выполняется в моей системе примерно за 1700 мс с одним потоком. С двумя потоками это занимает более 25 секунд. Полагая, что разница заключается в конфликте с кешем, я установил Multipler = 8
и запустил снова. Результат - 12 секунд, так что конкуренция была как минимум частью проблемы.
Увеличение множителя
выше 8 не улучшает производительность.
Для сравнения, аналогичная программа, не использующая массив , занимает всего около 2200 мс с двумя потоками, когда переменные находятся рядом. Когда я разделяю переменные, двухпоточная версия выполняется за то же время, что и однопоточная версия.
Если проблема заключалась в накладных расходах на индексацию массива, можно было бы ожидать, что она появится в однопоточной версии. Мне кажется, что при изменении массива происходит какое-то взаимное исключение, но я не знаю, что это такое.
Глядя на сгенерированный IL, не очень поучительно. Как и просмотр разборки.Дизассемблирование действительно показывает пару вызовов (я думаю) библиотеки времени выполнения, но я не смог войти в них.
В настоящее время я не разбираюсь в windbg или других низкоуровневых инструментах отладки. Прошло очень много времени с тех пор, как они мне были нужны. Так что я в тупике.
Моя единственная гипотеза на данный момент состоит в том, что код среды выполнения устанавливает флаг "грязный" при каждой записи. Похоже, что-то подобное потребуется для поддержки создания исключения, если массив изменяется во время его перечисления. Но я с готовностью признаю, что у меня нет прямых доказательств, подтверждающих эту гипотезу.
Кто-нибудь может сказать мне, что вызывает это большое замедление?