为什么性能测量会有所不同?

时间:2011-07-23 22:16:42

标签: c# .net performance

我有一个简单的方法可以将数组从一种类型转换为另一种类型。我想找出哪种方法最快。但到目前为止,我得到了不同的结果,我不能断定哪种方法真的更快,哪个边缘。

由于转换仅涉及分配内存,读取数组和转换值,我感到惊讶的是值不是更稳定。我想知道如何进行有意义的精确测量,并且不会从一天变为另一天。 从一天到另一天,差异大约为20%。

.NET 3.5和4.0的JITer,调试和发布模式,在调试器下没有运行可执行文件(在禁用它之前禁用JIT优化)之间当然存在差异,DEBUG和RELEASE之间的C#编译器的代码生成(主要是IL操作和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

从这看起来它确实看起来像哑安全变体比任何不安全的变体更快,虽然边界检查消除不安全的方法应该使它至少同样快,如果不是更快。 只是为了好玩,我还通过LCG(DynamicMethod)编译了相同的IL代码,这似乎比任何这些方法都要慢,尽管委托调用的额外成本似乎并没有在这里发挥如此大的作用。

for循环执行此代码1000万次,这应该会产生稳定的结果。为什么我在这里看到任何差异?使用realtime作为进程优先级也没有帮助(psexec -realtime可执行文件)。我怎样才能得到可靠的数字?

我的测试包括

  • 双四核机器
  • Windows 7 32/64位版本
  • .NET Framework 3.5 / 4.0
  • 可执行文件的32/64位版本。

如果我使用探查器,我不确定他是否会更加扭曲测量结果。由于他不时中断我的应用程序来获取调用堆栈,他肯定会破坏任何可能有助于提高性能的缓存局部性。如果有任何方法具有更好的(数据)缓存局部性,我将无法使用分析器找到它。

EDIT1: 考虑到我没有实时操作系统,我现在进行测量。因为对于一个线程,我有一个15ms的时间窗口被授予Windows调度程序,如果我的测量时间小于15ms,我可以保留调度程序。如果我过早测量,我会得到非常小的滴答计数,这对我来说不会太多。

为了获得稳定的值,我需要足够长的时间跨度让操作系统定期执行任何操作。经验测试表明,30秒以上是一个测量应该采取的良好时间跨度。

然后将该时间跨度划分为远低于15ms的样本时间跨度。然后我将获得每个样本的时间信息。从样本中我可以提取最小/最大和平均值。这样我也可以看到第一次初始化效果。 代码现在看起来像这样

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());

这些测试的值仍然有所不同(<10%),但我认为如果我创建了我的值的直方图,并删除了可能由操作系统,GC,......引起的10%最高值...我可以得到非常稳定的数字,我可以信任。

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: 直方图显示测量值不是随机的。它们看起来像Landau distribution,它应该给我正确的近似算法稳定值。我希望在.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<long,int>[] 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<long,int>[] GenerateHistogram(long [] input)
        {
            Dictionary<long, int> counts = new Dictionary<long, int>();
            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;

        }
    }
}

4 个答案:

答案 0 :(得分:4)

你所说的每个方法调用的平均差异大约是百分之一纳秒。 Windows并不声称是实时操作系统;这些测量结果与您将获得的一样稳定。

顺便说一句,the jitter will eliminate the bounds check inside your CastSafe method。如果你能找到比这更快的东西,我会感到非常惊讶。

(如果瓶颈是CPU,那么您可以使用Parallel.For而不是普通的for循环来提高性能,但要确定您需要针对实际数据进行测试。例如,43个int的数组的缓存行为将大大不同于43,000,000个int的数组。)

答案 1 :(得分:1)

我修改了我的原始问题,发现数字不是随机的,而是遵循分布(看起来像Landau分布),我可以使用拟合算法来获得最可能真实时间的峰值。

答案 2 :(得分:0)

我猜它会在Linux下使用mono运行吗?为避免多任务环境的影响,您可以使用

启动任何程序
time program

并获得一个衡量标准,您的程序使用了多少cpu-time。

您正在测量预热阶段和加载时间,但如果循环中有足够的元素,则应该没有太大问题。也许在Windows平台上有一个等效的程序?

答案 3 :(得分:0)

秒表不准确,请尝试使用HighResClock

http://netcode.ru/dotnet/?lang=&katID=30&skatID=261&artID=7113

不要指望测量精度达到纳秒级,正如其他人写的那样,Win7不是实时操作系统。

此外,在GC.Collect()之后,您可能想要放置GC.WaitForPendingFinalizers();