Почему измерения производительности различаются?

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

Поскольку преобразование касается только выделения памяти, чтения массива и преобразования значений, я удивлен, что значения не являются более стабильными. Я хотел знать, как я могу проводить точные измерения, которые значимы и не меняются от одного дня к другому. Разница составляет около 20% от одного дня к другому.

Конечно, существуют различия между JITer .NET 3.5 и 4.0, режимом отладки и выпуска, не запускающим исполняемый файл под отладчиком (отключает оптимизацию JIT, пока вы не отключите его), генерацией кода компилятора C # между DEBUG и RELEASE (в основном операции nop и больше временных переменных в коде IL).

using System;
using System.Collections.Generic;
using System.Diagnostics;

namespace PerfTest
{
    class Program
    {
        const int RUNS = 10 * 1000 * 1000;


        static void Main(string[] args)
        {
            int[] array = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43 };

            var s2 = Stopwatch.StartNew();
            for (int i = 0; i < RUNS; i++)
            {
                float[] arr = Cast(array);
            }
            s2.Stop();
            GC.Collect();

            var s3 = Stopwatch.StartNew();
            for (int i = 0; i < RUNS; i++)
            {
                float[] arr = Cast2(array);
            }
            s3.Stop();
            GC.Collect();

            var s4 = Stopwatch.StartNew();
            for (int i = 0; i < RUNS; i++)
            {
                var arr = CastSafe(array);
            }
            s4.Stop();


            Console.WriteLine("Times: {0} {1} {2}", s2.ElapsedMilliseconds, s3.ElapsedMilliseconds, s4.ElapsedMilliseconds);
        }

        // Referece cast implementation to check performance
        public static unsafe float[] Cast(int[] input)
        {
            int N = input.Length;
            float[] output = new float[N];

            fixed (int* pIStart = &input[0])
            {
                int* pI = pIStart;
                fixed (float* pOStart = &output[0])
                {
                    float* pO = pOStart;

                    for (int i = 0; i < N; i++)
                    {
                        *pO = (float)*pI;
                        pI++;
                        pO++;
                    }
                }
            }

            return output;
        }

        // Referece cast implementation to check performance
        public static unsafe float[] Cast2(int[] input)
        {
            int N = input.Length;
            float[] output = new float[N];
            fixed (int* pIStart = &input[0])
            {
                int* pI = pIStart;
                fixed (float* pOStart = &output[0])
                {
                    float* pO = pOStart;

                    for (int i = 0; i < N; i++)
                    {
                        pO[i] = (float) pI[i];
                    }
                }
            }

            return output;
        }
        public static float[] CastSafe(int[] input)
        {
            int N = input.Length;
            float[] output = new float[N];

            for (int i = 0; i < input.Length; i++)
            {
                output[i] = (float)input[i];
            }

            return output;
        }
    }
}

Я получаю тогда

  • раз: 1257 1388 1180
  • раз: 1331 1428 1267
  • раз: 1337 1435 1267
  • раз: 1208 1414 1145

Отсюда все выглядит глупо безопасный вариант быстрее, чем любой небезопасный вариант, хотя проверка границ устранение небезопасных методов должно сделать его как минимум таким же быстрым, если не быстрее. Ради интереса я также скомпилировал тот же код IL через LCG (DynamicMethod), который, кажется, даже медленнее, чем любой из этих методов, хотя дополнительные затраты на вызов делегата, похоже, не играют здесь такой большой роли.

Цикл for выполняет этот код 10 миллионов раз, что должно дать стабильные результаты. Почему я вообще вижу здесь различия? Использование реального времени в качестве приоритета процесса также не помогло (исполняемый файл psexec -realtime). Как я могу получить надежные цифры?

Мои тесты включали

  • двухъядерные четырехъядерные машины
  • 32/64-разрядные версии Windows 7
  • .NET Framework 3.5 / 4.0
  • 32/64-разрядные версии исполняемого файла.

Если я используйте профилировщик, не уверен, что он еще больше исказит измерения. Поскольку он время от времени прерывает мое приложение, чтобы получить стеки вызовов, он обязательно уничтожит любую локализацию кеша, которая может повысить производительность. Если есть какой-либо подход с лучшей локализацией кеша (данных), я не смогу найти его с помощью профилировщика.

Edit1: Чтобы принять во внимание, что у меня нет ОС реального времени, я сейчас пробую свои измерения. Поскольку для одного потока у меня есть временное окно в 15 мс, предоставленное планировщику Windows, я могу не использовать планировщик, если я измеряю менее 15 мс. Если я измерю слишком быстро, я получу очень маленькое количество тиков, которое мне мало что скажет.

Для получения стабильных значений мне нужен достаточно длительный промежуток времени, чтобы позволить ОС делать то, что она делает на регулярной основе. Эмпирические тесты показали, что 30+ секунд - это хороший промежуток времени, на который следует выполнить одно измерение.

Затем этот временной интервал делится на интервалы времени выборки, которые намного меньше 15 мс. Затем я получу информацию о времени для каждого образца. Из образцов я могу извлечь минимум / максимум и среднее значение. Таким образом, я также могу увидеть эффекты начальной инициализации. Теперь код выглядит так

class Program
{
    const int RUNS = 100 * 1000 * 1000; // 100 million runs will take about 30s
    const int RunsPerSample = 100;      // 100 runs for on sample is about 0,01ms << 15ms

    static void Main(string[] args)
    {
        int[] array = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43 };
        long[] sampleTimes = new long [RUNS/RunsPerSample];

        int sample = 0;
        for (int i = 0; i < RUNS; i+=RunsPerSample)
        {
            var sw = Stopwatch.StartNew();
            for (int j = i; j < i+RunsPerSample; j++)
            {
                float[] arr = Cast(array);
            }
            sw.Stop();
            sampleTimes[sample] = sw.ElapsedTicks;
            sample++;
        }
        Console.WriteLine("SampleSize: {0}, Min {1}, Max {2}, Average {3}",
            RunsPerSample, sampleTimes.Min(), sampleTimes.Max(), sampleTimes.Average());

Значения этих тестов все еще различаются (

SampleSize: 100, Min 25, Max 86400, Average 28,614631

  • SampleSize: 100, Min 24, Max 86027, Average 28,762608
  • SampleSize: 100, Min 25, Max 49523, Average 32, 102037
  • SampleSize: 100, Min 24, Max 48687, Average 32,030088

Edit2: Гистограммы показывают, что измеренные значения не случайны. Они выглядят как распределение Ландау , которое должно дать мне стабильные значения с правильными алгоритмами аппроксимации. Я бы хотел, чтобы в .NET существовало что-то вроде ROOT , где я мог бы интерактивно подогнать правильную функцию распределения к своим данным и получить результаты.

Measured Values Histogram

Код для генерации гистограммы с MSChart ] ниже:

using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;
using System.Windows.Forms.DataVisualization.Charting;

namespace ConsoleApplication4
{
    public partial class Histogram : Form
    {
        public Histogram(long [] sampleTimes)
        {
            InitializeComponent();

            Series  histogramSeries = cHistogram.Series.Add("Histogram");

            // Set new series chart type and other attributes
            histogramSeries.ChartType = SeriesChartType.Column;
            histogramSeries.BorderColor = Color.Black;
            histogramSeries.BorderWidth = 1;
            histogramSeries.BorderDashStyle = ChartDashStyle.Solid;

            var filtered = RemoveHighValues(sampleTimes, 40);
            KeyValuePair[] histoData = GenerateHistogram(filtered);

            ChartArea chartArea = cHistogram.ChartAreas[histogramSeries.ChartArea];
            chartArea.AxisY.Title = "Frequency";

            chartArea.AxisX.Minimum = histoData.Min( x=>x.Key );
            chartArea.AxisX.Maximum = histoData.Max( x=>x.Key );

            foreach (var v in histoData)
            {
                histogramSeries.Points.Add(new DataPoint(v.Key, v.Value));
            }

            chartArea.AxisY.Minimum = 0;
            chartArea.AxisY.Maximum = histoData[0].Value + 100;
        }

        // Count the occurence of each value of input and return an array with the value as key and its count as value
        // as ordered list starting with the highest counts.
        KeyValuePair[] GenerateHistogram(long [] input)
        {
            Dictionary counts = new Dictionary();
            foreach (var value in input)
            {
                int old = 0;
                if (!counts.TryGetValue(value, out old))
                {
                    counts[value] = 0;
                }
                counts[value] = ++old;
            }

            var orderedCounts = (from x in counts
                                 orderby x.Value descending
                                 select x).ToArray();

            return orderedCounts;
        }

        long[] RemoveHighValues(long[] input, int maxDifference)
        {
            var min = input.Min();
            var max = input.Max();

            long[] filtered = input;

            while (max - min > maxDifference) // remove all values wich differ by more than maxDifference ticks
            {
                filtered = input.Where(x => x < max).ToArray();
                max = filtered.Max();
            }

            return filtered;

        }
    }
}

6
задан Alois Kraus 24 July 2011 в 15:38
поделиться