【问题标题】:Using properties and performance使用属性和性能
【发布时间】:2014-04-10 09:20:14
【问题描述】:

我正在优化我的代码,我注意到使用属性(甚至是自动属性)对执行时间有深远的影响。请看下面的例子:

[Test]
public void GetterVsField()
{
    PropertyTest propertyTest = new PropertyTest();
    Stopwatch stopwatch = new Stopwatch();
    stopwatch.Start();
    propertyTest.LoopUsingCopy();
    Console.WriteLine("Using copy: " + stopwatch.ElapsedMilliseconds / 1000.0);

    stopwatch.Restart();
    propertyTest.LoopUsingGetter();
    Console.WriteLine("Using getter: " + stopwatch.ElapsedMilliseconds / 1000.0);
    stopwatch.Restart();
    propertyTest.LoopUsingField();
    Console.WriteLine("Using field: " + stopwatch.ElapsedMilliseconds / 1000.0);
}

public class PropertyTest
{
    public PropertyTest()
    {
        NumRepet = 100000000;
        _numRepet = NumRepet;
    }

    int NumRepet { get; set; }
    private int _numRepet;
    public int LoopUsingGetter()
    {
        int dummy = 314;
        for (int i = 0; i < NumRepet; i++)
        {
            dummy++;
        }
        return dummy;
    }

    public int LoopUsingCopy()
    {
        int numRepetCopy = NumRepet;
        int dummy = 314;
        for (int i = 0; i < numRepetCopy; i++)
        {
            dummy++;
        }
        return dummy;
    }

    public int LoopUsingField()
    {
        int dummy = 314;
        for (int i = 0; i < _numRepet; i++)
        {
            dummy++;
        }
        return dummy;
    }
}

在我的机器上的Release 模式下,我得到:

Using copy: 0.029
Using getter: 0.054
Using field: 0.026 

在我的情况下这是一场灾难——如果我想获得最佳性能,最关键的循环就不能使用任何属性。

我在这里做错了什么?我在想这些应该是JIT optimizer 旁边的inlined

【问题讨论】:

  • 当您说“处于发布模式”时,您是指发布 build 配置,还是在没有调试器的情况下运行?如果您在调试器中运行,我完全希望看到显着的影响。另请注意,循环如此紧凑是非常不寻常的......并且对微优化只是证明是瓶颈的应用程序的那些部分是合理的。跨度>
  • 我刚刚自己测试了代码,x86 JIT使得属性访问和字段访问基本一样。 x64 JIT 显示了您在问题中提出的行为。您可能想尝试使用即将推出的新 x64 JIT:blogs.msdn.com/b/dotnet/archive/2014/02/27/…
  • 对 - 我会尝试在独立环境中进行性能测试,尽可能少地干扰结果,就个人而言。 (我喜欢使用简单的控制台应用程序来执行此操作。)但是,正如我所说,看起来 x64 JIT 在内联方面不如 x86 JIT 聪明。
  • 虽然它没有解释差异:我看不出这会产生“深远的影响”还是“灾难”。您正在执行一段代码十亿次,但仍然在最坏的情况下只需要 54 毫秒。如果您想要更多性能,请使用 C 或 ASM 而不是 .Net。
  • @AndreasMüller 不,这不是一场灾难完全 IMO。如果这个小的性能问题一个真正的问题,那么我很抱歉,但是会有 其他类似的性能问题或 更糟 因为 .Net。第一个明显的例子是 垃圾收集器执行,这将是一场真正的灾难,而不仅仅是每次执行几纳秒的损失。

标签: c# .net performance optimization


【解决方案1】:

Getter/Setter 是具有一些特殊约定(setter 中的“值”变量,没有可见的参数列表)的方法的语法糖。

根据article,“如果方法的任何形式参数是结构,则该方法将不会被内联。” -- 整数是结构。因此,我认为这个限制适用。

我没有查看以下代码生成的 IL,但我确实得到了一些有趣的结果,我认为这些结果表明了这种方式......

using System;
using System.Diagnostics;

public static class Program{
public static void Main()
{
    PropertyTest propertyTest = new PropertyTest();
    Stopwatch stopwatch = new Stopwatch();
    stopwatch.Start();
    propertyTest.LoopUsingField();
    Console.WriteLine("Using field: " + stopwatch.ElapsedMilliseconds / 1000.0);


    stopwatch.Restart();
    propertyTest.LoopUsingBoxedGetter();
    Console.WriteLine("Using boxed getter: " + stopwatch.ElapsedMilliseconds / 1000.0);

    stopwatch.Restart();
    propertyTest.LoopUsingUnboxedGetter();
    Console.WriteLine("Using unboxed getter: " + stopwatch.ElapsedMilliseconds / 1000.0);

}

}
public class PropertyTest
{
    public PropertyTest()
    {
        _numRepeat = 1000000000L;
        _field = 1;
        Property = 1;
        IntProperty = 1;
    }

    private long _numRepeat;
    private object _field = null;
    private object Property {get;set;}
    private int IntProperty {get;set;}

    public void LoopUsingBoxedGetter()
    {

        for (long i = 0; i < _numRepeat; i++)
        {
          var f = Property;
        }

    }

    public void LoopUsingUnboxedGetter()
    {
        for (long i = 0; i < _numRepeat; i++)
        {
            var f = IntProperty;
        }
    }

    public void LoopUsingField()
    {
        for (long i = 0; i < _numRepeat; i++)
        {
            var f = _field;
        }
    }
}

这会产生..在我的机器上,OS X(最新版本的 Mono),这些结果(以秒为单位):

  • 使用字段:2.606
  • 使用盒装吸气剂:2.585
  • 使用未装箱的吸气剂:2.71

【讨论】:

  • 在 Windows(.net v4.5、x64)上,我没有得到有意义的或可重现的时间差异。由于 JIT 开销(尤其是在仅持续 50 毫秒的“小型”运行时),更改基准测试的顺序也会影响结果。
【解决方案2】:

你说你正在优化你的代码,但我很好奇如何,功能应该是什么,源数据是什么以及它的大小,因为这显然不是“真实”代码.如果您要解析大量数据,请考虑使用 BinarySearch 功能。这比处理大量数据的 .Contains() 函数要快得多。

List<int> myList = GetOrderedList();
if (myList.BinarySearch(someValue) < 0)
// List does not contain data

也许您只是在循环数据。如果您正在循环数据并返回一个值,您可能需要使用 yield 关键字。如果可以的话,另外考虑并行库的潜在用途,或者使用您自己的线程管理。

从发布的来源来看,这似乎不是您想要的,但它非常通用,所以我认为这值得一提。

public IEnumerable<int> LoopUsingGetter()
{
    int dummy = 314;

    for (int i = 0; i < NumRepet; i++)
    {
        dummy++;
        yield return dummy;
    }
}

[ThreadStatic]
private static int dummy = 314;

public static int Dummy
{
    get
    {
        if (dummy != 314) // or whatever your condition
        {
            return dummy;
        }

        Parallel.ForEach (LoopUsingGetter(), (i)
        {
            //DoWork(), not ideal for given example, but due to the generic context this may help
            dummy += i;
        });
    }

    return dummy;
}

【讨论】:

  • 在我们的例子中,并行化是在更高级别上完成的,所以我正在优化的代码应该保持单线程。不过感谢您提出的有效观点!
【解决方案3】:

遵循 80/20 性能规则,而不是微优化。 编写代码是为了可维护性,而不是性能。 也许汇编语言是最快的,但这并不意味着我们应该将汇编语言用于所有目的。

您正在运行循环 1 亿次,差异为 0.02 毫秒或 20 微秒。调用函数会产生一些开销,但在大多数情况下,这并不重要。您可以相信编译器可以内联或执行高级操作。

在 99% 的情况下,直接访问该字段会出现问题,因为您无法控制所有变量的引用位置,并且当您发现有问题时在太多地方进行修复。

【讨论】:

    【解决方案4】:

    您应该在秒表完成循环后停止它,当您向控制台写入数据时,您的秒表仍在运行,这可能会增加额外的时间,从而导致您的结果出现偏差。

    [Test]
    public void GetterVsField()
    {
        PropertyTest propertyTest = new PropertyTest();
        Stopwatch stopwatch = new Stopwatch();
    
        stopwatch.Start();
        propertyTest.LoopUsingCopy();
        stopwatch.Stop();
        Console.WriteLine("Using copy: " + stopwatch.ElapsedMilliseconds / 1000.0);
    
        stopwatch.Reset();
        stopwatch.Start();
        propertyTest.LoopUsingGetter();
        stopwatch.Stop();
        Console.WriteLine("Using getter: " + stopwatch.ElapsedMilliseconds / 1000.0);
    
        stopwatch.Reset();
        stopwatch.Start();
        propertyTest.LoopUsingField();
        stopwatch.Stop();
        Console.WriteLine("Using field: " + stopwatch.ElapsedMilliseconds / 1000.0);
    }
    

    【讨论】:

    • 方法的参数在方法运行之前被评估。评估该属性的值是在该代码行上执行的第一件事。停止秒表甚至可能需要更多时间。最重要的是,这两种解决方案之间的差异实际上将为零,即使它会更慢,它也会始终如一。
    【解决方案5】:

    您必须检查是否选中了优化代码复选框。

    1. 如果不勾选,访问属性还是方法调用
    2. 如果选中,则属性是内联的,并且性能与直接字段访问相同,因为 JIT 代码将是相同的

    X64 JIT 编译器对 inlinig 有更多限制。有关 JIT64 内联优化的更多信息是: David Broman's CLR Profiling API Blog: Tail call JIT conditions.

    请看#3 The caller or callee return a value type点。 如果您的属性将返回引用类型,则属性 getter 将被内联。 这意味着int NumRepet { get; set; } 属性没有内联,但如果您不违反其他限制,object NumRepet { get; set; } 将被内联。

    X64 JIT 的优化很差,这就是 John 提到的引入新 JIT 的原因

    【讨论】:

    • 不正确。发布版本默认选中“优化代码”,即使优化代码也存在性能问题。
    • “如果勾选了属性是内联的并且性能一样”还是不行,可以用当前的x64 JIT编译器测试一下。
    • 这是期望的行为,如果您通过链接博客的条件,它就会这样做。如果将 int 更改为 object,即使在 x64 JIT 上,该属性也会被内联
    • 因为明确提到了发布模式,我不禁假设设置了“优化代码”。如果在调试和发布中存在性能问题,则代码库或数据库中存在明显问题。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2014-09-09
    • 1970-01-01
    • 1970-01-01
    • 2020-04-22
    • 1970-01-01
    • 1970-01-01
    • 2010-12-14
    相关资源
    最近更新 更多