Использование свойств и производительности

Я оптимизировал свой код, и я заметил, что использование свойств (даже авто свойств) оказывает глубокое влияние на время выполнения. См. Пример ниже:

[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 

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

Что я здесь делаю неправильно? Я думал, что это будет inlined с помощью JIT optimizer.

Ответы

Ответ 1

Getters/Setters - это синтаксический сахар для методов с несколькими специальными соглашениями (переменная "значение" в сеттере "и список видимых параметров).

В соответствии с этой статьей: "Если любой из формальных аргументов метода является структурой, метод не будет вложен". - ints - это структуры. Поэтому я считаю, что это ограничение применяется.

Я не смотрел на 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
  • Использование boxed getter: 2.585
  • Использование unboxed getter: 2.71

Ответ 2

Следуйте правилу производительности 80/20 вместо микрооптимизации. Напишите код для ремонтопригодности, а не производительности. Возможно, язык ассемблера является самым быстрым, но это не значит, что мы должны использовать язык ассемблера для всех целей.

Вы запускаете цикл 100 миллионов раз, а разница составляет 0,02 миллисекунды или 20 микросекунд. Вызов функции будет иметь некоторые накладные расходы, но в большинстве случаев это не имеет значения. Вы можете доверять компилятору встроенным или выполнять расширенные действия.

Прямой доступ к полю будет проблематичным в 99% случаев, так как вы не будете контролировать, где ссылаются все ваши переменные и фиксируются на слишком многих местах, когда вы обнаружите, что что-то не так.

Ответ 3

Вы говорите, что вы оптимизируете свой код, но мне любопытно, как должна функционировать, и каковы исходные данные, входящие в это, а также размер, поскольку это явно не "настоящий" код, Если вы разбираете большой список данных, рассматривая возможность использования функции 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;
}

Ответ 4

Вы должны проверить, установлен ли флажок "Оптимизировать код".

  1. Если это не проверено, доступ к свойству все еще является вызовом метода
  2. Если этот флажок установлен, свойство встроено, и производительность такая же, как и при прямом доступе к полю, поскольку код JITed будет таким же

В JIT-компиляторе X64 больше ограничений по inlinig. Более подробная информация об оптимизации встраивания JIT64 приведена здесь: Блог Дэвида Бромана по профилированию API CLR: Tail call JIT.

см. пункт #3 The caller or callee return a value type. Если ваше свойство вернет ссылочный тип, получатель свойства будет встроен. Это означает, что свойство int NumRepet { get; set; } int NumRepet { get; set; } int NumRepet { get; set; } не является встроенным, но object NumRepet { get; set; } object NumRepet { get; set; } object NumRepet { get; set; } будет встроен, если вы не нарушите другое ограничение.

Оптимизация X64 JIT оставляет желать лучшего, и поэтому, как упоминает Джон, будет представлена новая

Ответ 5

Вы должны остановить секундомер, когда он завершает цикл, ваш секундомер все еще работает, когда вы пишете на консоль, это может добавить дополнительное время, которое может исказить результаты.

[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);
}