C#将属性值连接到字符串时反映“奇怪”的基准

时间:2016-05-04 08:08:54

标签: c# reflection benchmarking

我试图做一些反射基准测试,但大多数时候我设法让自己感到困惑。有人可以解释为什么两个测试通过?

对于第一个,我期待它失败,但我得到的时间是:

millisecondsReflection - 4970 ms

毫秒 - 6935毫秒

    [Fact]
    public void PropertiesGetterString()
    {
        var bar = new Bar
        {
            Id = 42,
            Number = "42",
        };

        string concat = string.Empty;
        string concatReflection = string.Empty;

        var props = bar.GetType().GetProperties();

        Stopwatch sw = new Stopwatch();
        sw.Start();

        for (int i = 0; i < 100000; i++)
        {
            concatReflection += props[1].GetValue(bar);
        }

        sw.Stop();

        long millisecondsReflection = sw.ElapsedMilliseconds;

        sw.Reset();

        sw.Start();

        for (int i = 0; i < 100000; i++)
        {
            concat += bar.Number;
        }

        sw.Stop();

        long milliseconds = sw.ElapsedMilliseconds;

        millisecondsReflection.Should().BeLessOrEqualTo(milliseconds);
    }

我认为它与字符串连接或类型转换有关,因此我将其更改为列表追加,并得到了预期的结果,即反射速度较慢。

    [Fact]
    public void PropertiesGetterArray()
    {
        var bar = new Bar
        {
            Id = 42,
            Number = "42",
        };

        List<object> concat = new List<object>();
        List<object> concatReflection = new List<object>();

        var props = bar.GetType().GetProperties();

        Stopwatch sw = new Stopwatch();
        sw.Start();

        for (int i = 0; i < 1000000; i++)
        {
            concatReflection.Add(props[1].GetValue(bar));
        }

        sw.Stop();

        long millisecondsReflection = sw.ElapsedMilliseconds;

        sw.Reset();
        sw.Start();

        for (int i = 0; i < 1000000; i++)
        {
            concat.Add(bar.Number);
        }

        sw.Stop();

        long milliseconds = sw.ElapsedMilliseconds;

        millisecondsReflection.Should().BeGreaterOrEqualTo(milliseconds);
    }

结果如下:

毫秒反射 - 184毫秒

毫秒 - 11毫秒

我的问题是我错过了什么?

P.S。在调试模式下花费的时间。如发布模式中的评论所示,时间非常接近

1 个答案:

答案 0 :(得分:2)

摘要

这种差异造成了两件事:字符串连接的开销,它正在淹没反射的开销;以及调试构建处理本地生命周期的方式的不同。

您在发布版本和调试版本之间看到的时间差异是由于调试版本在方法结束之前保持所有本地生存的方式,与发布版本相反。

这导致代码中两个字符串的GC在发布和调试版本之间的行为非常不同。

详细分析

如果您将测试代码更改为仅对字符串属性的长度求和,则会得到预期的结果。

这是我的结果(发布版本):

Trial 1
Length = 20000000
Length = 20000000
Without reflection: 8
With reflection: 1613
Trial 2
Length = 20000000
Length = 20000000
Without reflection: 8
With reflection: 1606
Trial 3
Length = 20000000
Length = 20000000
Without reflection: 8
With reflection: 1598
Trial 4
Length = 20000000
Length = 20000000
Without reflection: 8
With reflection: 1609
Trial 5
Length = 20000000
Length = 20000000
Without reflection: 9
With reflection: 1619

测试代码:

using System;
using System.Diagnostics;

namespace Demo
{
    class Bar
    {
        public int Id { get; set; }
        public string Number { get; set; }
    }

    static class Program
    {
        static void Main()
        {
            for (int trial = 1; trial <= 5; ++trial)
            {
                Console.WriteLine("Trial " + trial);
                PropertiesGetterString();
            }
        }

        public static void PropertiesGetterString()
        {
            int count = 10000000;

            var bar = new Bar
            {
                Id = 42,
                Number = "42",
            };

            int totalLength = 0;

            var props = bar.GetType().GetProperties();

            Stopwatch sw = new Stopwatch();
            sw.Start();

            for (int i = 0; i < count; i++)
            {
                totalLength += ((string)props[1].GetValue(bar)).Length;
            }

            sw.Stop();
            long millisecondsReflection = sw.ElapsedMilliseconds;
            Console.WriteLine("Length = " + totalLength);

            sw.Reset();
            totalLength = 0;
            sw.Start();

            for (int i = 0; i < count; i++)
            {
                totalLength += bar.Number.Length;
            }

            sw.Stop();
            long milliseconds = sw.ElapsedMilliseconds;
            Console.WriteLine("Length = " + totalLength);

            Console.WriteLine("Without reflection: " + milliseconds);
            Console.WriteLine("With reflection: " + millisecondsReflection);
        }
    }
}

另请注意,我只能使用调试版本而不是版本构建来重现原始结果。

如果我根据您的OP更改我的测试代码以进行字符串连接,我会得到以下结果:

Trial 1
Without reflection: 3686
With reflection: 3661
Trial 2
Without reflection: 3584
With reflection: 3688
Trial 3
Without reflection: 3587
With reflection: 3676
Trial 4
Without reflection: 3550
With reflection: 3700
Trial 5
Without reflection: 3564
With reflection: 3659

最后,为了进一步尝试最小化后台GC对两个循环的影响,我在每次调用sw.Stop()后添加了以下代码:

GC.Collect();
GC.WaitForPendingFinalizers();

将结果更改为:

Trial 1
Without reflection: 3565
With reflection: 3665
Trial 2
Without reflection: 3538
With reflection: 3631
Trial 3
Without reflection: 3535
With reflection: 3597
Trial 4
Without reflection: 3558
With reflection: 3629
Trial 5
Without reflection: 3551
With reflection: 3599

随着这种变化,所有&#34;反思&#34;结果比没有反射&#34;结果,正如您所期望的那样。

最后,让我们研究在调试模式中观察到的差异。

由于循环的顺序,似乎发生了差异。如果在直接循环之前尝试使用反射循环进行一次测试,则会得到不同的结果,反之亦然。

这是我最终测试计划的结果:

Trial 1
PropertiesGetterStringWithoutThenWithReflection()
Without reflection: 3228
With reflection: 5866
PropertiesGetterStringWithThenWithoutReflection()
Without reflection: 5780
With reflection: 3273
Trial 2
PropertiesGetterStringWithoutThenWithReflection()
Without reflection: 3207
With reflection: 5921
PropertiesGetterStringWithThenWithoutReflection()
Without reflection: 5802
With reflection: 3318
Trial 3
PropertiesGetterStringWithoutThenWithReflection()
Without reflection: 3246
With reflection: 5873
PropertiesGetterStringWithThenWithoutReflection()
Without reflection: 5882
With reflection: 3297
Trial 4
PropertiesGetterStringWithoutThenWithReflection()
Without reflection: 3261
With reflection: 5891
PropertiesGetterStringWithThenWithoutReflection()
Without reflection: 5778
With reflection: 3298
Trial 5
PropertiesGetterStringWithoutThenWithReflection()
Without reflection: 3267
With reflection: 5948
PropertiesGetterStringWithThenWithoutReflection()
Without reflection: 5830
With reflection: 3306

请注意,无论是否进行反射,首先运行的循环都是最快的。这意味着差异是在调试版本中完成字符串处理的方式的一些工件。

我怀疑可能发生的事情是调试版本为整个方法保留了连接字符串的活动状态,而发布版本没有,这将影响GC。

以下是上述结果的测试代码:

using System;
using System.Diagnostics;

namespace Demo
{
    class Bar
    {
        public int Id { get; set; }
        public string Number { get; set; }
    }

    static class Program
    {
        static void Main()
        {
            for (int trial = 1; trial <= 5; ++trial)
            {
                Console.WriteLine("Trial " + trial);
                PropertiesGetterStringWithoutThenWithReflection();
                PropertiesGetterStringWithThenWithoutReflection();
            }
        }

        public static void PropertiesGetterStringWithoutThenWithReflection()
        {
            Console.WriteLine("PropertiesGetterStringWithoutThenWithReflection()");

            int count = 100000;

            var bar = new Bar
            {
                Id = 42,
                Number = "42",
            };

            var props = bar.GetType().GetProperties();
            string concat1 = "";
            string concat2 = "";

            Stopwatch sw = new Stopwatch();
            sw.Start();

            for (int i = 0; i < count; i++)
            {
                concat2 += bar.Number;
            }

            sw.Stop();
            GC.Collect();
            GC.WaitForPendingFinalizers();
            long milliseconds = sw.ElapsedMilliseconds;
            sw.Restart();

            for (int i = 0; i < count; i++)
            {
                concat1 += (string)props[1].GetValue(bar);
            }

            sw.Stop();
            GC.Collect();
            GC.WaitForPendingFinalizers();
            long millisecondsReflection = sw.ElapsedMilliseconds;

            Console.WriteLine("Without reflection: " + milliseconds);
            Console.WriteLine("With reflection: " + millisecondsReflection);
        }

        public static void PropertiesGetterStringWithThenWithoutReflection()
        {
            Console.WriteLine("PropertiesGetterStringWithThenWithoutReflection()");

            int count = 100000;

            var bar = new Bar
            {
                Id = 42,
                Number = "42",
            };

            var props = bar.GetType().GetProperties();
            string concat1 = "";
            string concat2 = "";

            Stopwatch sw = new Stopwatch();
            sw.Start();

            for (int i = 0; i < count; i++)
            {
                concat1 += (string)props[1].GetValue(bar);
            }

            sw.Stop();
            GC.Collect();
            GC.WaitForPendingFinalizers();
            long millisecondsReflection = sw.ElapsedMilliseconds;
            sw.Restart();

            for (int i = 0; i < count; i++)
            {
                concat2 += bar.Number;
            }

            sw.Stop();
            GC.Collect();
            GC.WaitForPendingFinalizers();
            long milliseconds = sw.ElapsedMilliseconds;

            Console.WriteLine("Without reflection: " + milliseconds);
            Console.WriteLine("With reflection: " + millisecondsReflection);
        }
    }
}

<强>更新

我也在发布版本中重现了差异。我认为这证明了,正如我所怀疑的那样,差异是由于琴弦保持活动的时间长短。

这是测试代码,作为RELEASE版本运行:

public static void PropertiesGetterString()
{
    int count = 100000;

    var bar = new Bar
    {
        Id = 42,
        Number = "42",
    };

    var props = bar.GetType().GetProperties();
    string concat1 = "";
    string concat2 = "";

    Stopwatch sw = new Stopwatch();
    sw.Start();

    for (int i = 0; i < count; i++)
    {
        concat1 += (string)props[1].GetValue(bar);
    }

    sw.Stop();
    GC.Collect();
    GC.WaitForPendingFinalizers();
    long millisecondsReflection = sw.ElapsedMilliseconds;
    sw.Restart();

    for (int i = 0; i < count; i++)
    {
        concat2 += bar.Number;
    }

    sw.Stop();
    GC.Collect();
    GC.WaitForPendingFinalizers();
    long milliseconds = sw.ElapsedMilliseconds;

    Console.WriteLine("Without reflection: " + milliseconds);
    Console.WriteLine("With reflection: " + millisecondsReflection);
    Console.WriteLine(concat1.Length + concat2.Length); // Try with and without this line commented out.
}

如果按原样运行,我会得到以下结果:

Trial 1
Without reflection: 4957
With reflection: 3646
400000
Trial 2
Without reflection: 4941
With reflection: 3626
400000
Trial 3
Without reflection: 4969
With reflection: 3609
400000
Trial 4
Without reflection: 5021
With reflection: 3690
400000
Trial 5
Without reflection: 4769
With reflection: 3637
400000

注意第一个循环(带反射)比第二个循环(没有反射)更快。

现在注释掉方法的最后一行(输出两个字符串长度的行)并再次运行它。这次的结果是:

Trial 1
Without reflection: 3558
With reflection: 3690
Trial 2
Without reflection: 3653
With reflection: 3624
Trial 3
Without reflection: 3606
With reflection: 3663
Trial 4
Without reflection: 3592
With reflection: 3660
Trial 5
Without reflection: 3629
With reflection: 3644

我认为,这证明了调试和发布构建时间之间的差异是由于调试构建使所有本地文件保持活动直到方法结束(这样它们甚至可以在调试器中显示)如果你已经超越了方法中最后一次使用它们的话。)

相比之下,发布版本可以在GC最后一次使用方法后的任何时间进行引用。