Почему одновременная модификация массивов такая медленная?

Я писал программу, чтобы проиллюстрировать эффекты конкуренции за кеш в многопоточных программах. Моей первой попыткой было создать массив длиной и показать, как изменение соседних элементов вызывает разногласия. Вот программа.

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 или других низкоуровневых инструментах отладки. Прошло очень много времени с тех пор, как они мне были нужны. Так что я в тупике.

Моя единственная гипотеза на данный момент состоит в том, что код среды выполнения устанавливает флаг "грязный" при каждой записи. Похоже, что-то подобное потребуется для поддержки создания исключения, если массив изменяется во время его перечисления. Но я с готовностью признаю, что у меня нет прямых доказательств, подтверждающих эту гипотезу.

Кто-нибудь может сказать мне, что вызывает это большое замедление?

31
задан Jim Mischel 29 December 2011 в 19:47
поделиться