воскресенье, 18 августа 2013 г.

Об итерировании статичных массивов в .NET, часть 1

Блог переехал. Актуальная версия поста находится по адресу: http://aakinshin.net/ru/blog/dotnet/static-array-iteration/.


Содержание поста было обновлено: результаты были уточнены, появилась вторая часть с подробным объяснением сложившейся ситуации.

Управляемый подход платформы .NET делает жизнь разработчиков достаточно простой, беря на себя многие рутинные операции. Большую часть времени программист может вообще не вспоминать о технической реализации платформы, сосредоточившись исключительно на логике своего приложения. Но иногда попадаются задачи, критичные по производительности. Существует множество различных подходов к оптимизации кода в таких ситуациях вплоть до переписывания наиболее важных частей кода через неуправляемый код. Однако, зачастую для увеличения скорости приложения достаточно понимать, сколько времени тратится на ту или иную операцию. Знание подобных вещей позволит оптимизировать некоторые методы с помощью достаточно простых модификаций исходного кода.

В этой статье мне хотелось бы поговорить о скорости доступа к массивам, ссылки на которые хранятся в статичных переменных. Дело в том, что в скорость итерирования по ним в зависимости от условий запуска может быть ниже, чем для массива, ссылка на который хранится в обычном поле экземпляра класса или локальной переменной. Рассмотрим пример.

В примере будем решать простую задачу: подсчёт суммы элементов массива. В первом случае мы будем использовать обычное боле класса, а во втором — статическое. Для замеров времени будем использовать BenchmarkDotNet (исходный код примера: ArrayIterationProgram.cs, тестировать следует в Release mode without debugging):

private const int N = 1000, IterationCount = 1000000;

private int[] nonStaticField;
private static int[] staticField;

public void Run()
{
    nonStaticField = staticField = new int[N];

    var competition = new BenchmarkCompetition();
    competition.AddTask("Non-static", () => NonStaticRun());
    competition.AddTask("Static", () => StaticRun());
    competition.Run();
}

private int NonStaticRun()
{
    int sum = 0;
    for (int iteration = 0; iteration < IterationCount; iteration++)
        for (int i = 0; i < N; i++)
            sum += nonStaticField[i];
    return sum;
}

private int StaticRun()
{
    int sum = 0;
    for (int iteration = 0; iteration < IterationCount; iteration++)
        for (int i = 0; i < N; i++)
            sum += staticField[i];
    return sum;
}

На своей машине я получил следующие результаты:

Non-static : 346ms
Static     : 535ms

Если мы взглянем на IL-код целевых методов, то увидим, что они различаются только в одном месте, при обращении к полю:

Non-static:
L_000b: ldarg.0 
L_000c: ldfld int32[] Benchmarks.StaticFieldBenchmark::nonStaticField
Static:
L_000b: ldsfld int32[] Benchmarks.StaticFieldBenchmark::staticField

Заметим, что физически оба поля ссылаются на одну и ту же область памяти. Мы можем ускорить работу со статическим полем, если перед многократным обращением к полю сохраним его в локальную переменную:

private int StaticRun()
{
    var localField = staticField;
    int sum = 0;
    for (int iteration = 0; iteration < IterationCount; iteration++)
        for (int i = 0; i < N; i++)
            sum += localField[i];
    return sum;
}

В итоге StaticRun будет работать столько же, сколько и NonStaticRun.

Объяснение такого поведения можно прочитать во второй части статьи.