Профилирование производительности в .NET.

Я написал класс, который использует методы Stopwatch для профилей и циклы for/foreach. При циклах for и foreach он проверяет стандартный цикл на реализацию Parallel.For или Parallel.ForEach.

Вы должны написать тесты производительности следующим образом:

Метод:

PerformanceResult result = Profiler.Execute(() => { FooBar(); });

Для цикла:

SerialParallelPerformanceResult result = Profiler.For(0, 100, x => { FooBar(x); });

Цикл ForEach:

SerialParallelPerformanceResult result = Profiler.ForEach(list, item => { FooBar(item); });

Всякий раз, когда я запускаю тесты (один из .Execute, .For или .ForEach), я помещаю их в цикл, чтобы я мог видеть, как производительность изменяется с течением времени.

Пример производительности может быть:

Выполнение метода 1 = 200 мс
Выполнение метода 2 = 12 мс
Выполнение метода 3 = 0ms

Для выполнения 1 = 300 мс (последовательный), 100 мс (параллельно)
Для исполнения 2 = 20 мс (серийный), 75 мс (параллельный)
Для выполнения 3 = 2 мс (последовательный), 50 мс (параллельный)

ForEach исполнение 1 = 350 мс (серийный), 300 мс (параллельный)
ForEach исполнение 2 = 24ms (последовательный), 89ms (параллельный)
ForEach исполнение 3 = 1 мс (последовательный), 21 мс (параллельный)

Мои вопросы:

  • Почему производительность меняется со временем, что делает .NET в фоновом режиме, чтобы облегчить это?

  • Как/почему последовательная операция выполняется быстрее параллельной? Я убедился, что я делаю операцию сложной, чтобы увидеть разницу правильно... в большинстве случаев последовательные операции кажутся быстрее!?

ПРИМЕЧАНИЕ.. Для параллельной обработки я тестирую 8-ядерную машину.

Ответ 1

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

(Спасибо Топор и Лорен за ваши комментарии по этому поводу!)

Причины отсутствия секундомера:

  • Измерения рассчитываются через истекшее время в миллисекундах, а не время процессора.
  • На измерения могут влиять фоновые "шумовые" и процессы с интенсивным потоком.
  • Измерения не учитывают компиляцию JIT и накладные расходы.

Говоря об этом, использование секундомера подходит для случайного изучения производительности. Имея это в виду, я немного улучшил свой алгоритм профилирования.

Где перед этим просто выполняется выражение, которое было передано ему, теперь у него есть возможность перебирать выражение несколько раз, создавая среднее время выполнения. Первый запуск может быть опущен, так как в этом случае JIT запускается, и могут возникать некоторые основные накладные расходы. Понятно, что это никогда не будет таким сложным, как использование профессионального инструмента профилирования, такого как Redgate ANTS profiler, но это нормально для более простых задач!

Ответ 2

В соответствии с моим комментарием выше: я сделал несколько простых тестов самостоятельно и не обнаружил разницы с течением времени. Вы можете поделиться своим кодом? Я отведу свой ответ, поскольку он не подходит здесь.

Это мой пример кода. (Я также пытался использовать как статические, так и методы экземпляра без разницы)

class Program
{
    static void Main(string[] args)
    {
        int to = 50000000;
        OtherStuff os = new OtherStuff();

        Console.WriteLine(Profile(() => os.CountTo(to)));
        Console.WriteLine(Profile(() => os.CountTo(to)));
        Console.WriteLine(Profile(() => os.CountTo(to)));
    }

    static long Profile(Action method)
    {
        Stopwatch st = Stopwatch.StartNew();
        method();
        st.Stop();
        return st.ElapsedMilliseconds;
    }
}

class OtherStuff
{
    public void CountTo(int to)
    {
        for (int i = 0; i < to; i++)
        {
            // some work...
            i++;
            i--;
        }
    }
}

Образец вывода будет:

331
331
334

Попробуйте выполнить этот метод:

class OtherStuff
    {
        public string CountTo(Guid id)
        {
            using(SHA256 sha = SHA256.Create())
            {
                int x = default(int);
                for (int index = 0; index < 16; index++)
                {
                    x = id.ToByteArray()[index] >> 32 << 16;
                }
                RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();
                byte[] y = new byte[1024];
                rng.GetBytes(y);
                y = y.Concat(BitConverter.GetBytes(x)).ToArray();
                return BitConverter.ToString(sha.ComputeHash(BitConverter.GetBytes(x).Where(o => o >> 2 < 0).ToArray()));
            }
        }
    }

Пример вывода:

11 
0 
0