Оптимизация компилятором повторяющихся вызовов средств доступа

StackOverflow https://stackoverflow.com/questions/2497113

Вопрос

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

public class nonsensical_calculator
{ 

   ...

    double _rate;
    int _term;
    int _days;

    double monthlyRate { get { return _rate / 12; }}

    public double days { get { return (1 - i); }}
    double ar   { get { return (1+ days) /(monthlyRate  * days)
    double bleh { get { return Math.Pow(ar - days, _term)
    public double raar { get { return bleh * ar/2 * ar / days; }}
    ....
}

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

Предложения по дальнейшему чтению всегда приветствуются

Это было полезно?

Решение

Насколько я знаю, компилятор C# не делает оптимизируйте это, потому что нельзя быть уверенным в побочных эффектах (например,что, если у тебя есть accessCount++ в геттере?) Посмотрите здесь на отличный ответ Эрика Липперта

Из этого ответа:

Компилятор C# никогда не выполняет такого рода оптимизацию;как уже отмечалось, для этого потребуется, чтобы компилятор всмотрелся в вызываемый код и проверил, что вычисленный им результат не изменяется в течение всего времени существования кода вызываемого объекта.Компилятор C# этого не делает.

JIT-компилятор может.Нет причин, почему это не могло быть сделано.Весь код лежит прямо здесь.Встроить метод получения свойств можно совершенно бесплатно, и если джиттер определяет, что встроенный метод получения свойств возвращает значение, которое можно кэшировать в регистре и повторно использовать, то это можно сделать бесплатно.(Если вы не хотите, чтобы это происходило, поскольку значение может быть изменено в другом потоке, то у вас уже есть ошибка состояния гонки;исправьте ошибку, прежде чем беспокоиться о производительности.)

Просто примечание: поскольку Эрик работает в команде компиляторов C#, я доверяю его ответу :)

Другие советы

Несколько случайных мыслей.

Во-первых, как отмечали другие, компилятор C# не выполняет такого рода оптимизацию, хотя джиттер может это делать.

Во-вторых, лучший способ ответить на вопрос о производительности — попробовать и посмотреть.Класс «Секундомер» — ваш друг.Попробуйте миллиард раз оба способа и посмотрите, какой из них быстрее;тогда ты узнаешь.

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

И в-четвертых, еще один ответ предлагал хранить промежуточные результаты в локальной переменной.Обратите внимание, что в некоторых ситуациях это может значительно ускорить процесс, а в других — замедлить. Иногда быстрее пересчитать результат без необходимости, чем сохранять его и искать снова, когда он вам понадобится.

Как это может быть?Архитектуры чипов с небольшим количеством регистров (я смотрю на вас, x86) требуют, чтобы джиттер был очень разумным в отношении того, какие локальные переменные будут находиться в регистрах, а какие будут иметь доступ к стеку.Поощрение джиттера для помещения чего-то, что используется нечасто, в один регистр иногда означает вытеснение чего-то еще из этого регистра, чего-то, что принесет больше пользы от нахождения в регистре, чем ваше нечасто используемое значение.

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

Да, компилятор C# не делает подобных оптимизаций.Но JIT-компилятор, безусловно, делает это.Все опубликованные вами геттеры достаточно малы, чтобы их можно было встроить, что дает прямой доступ к полю.

Пример:

static void Main(string[] args) {
  var calc = new nonsensical_calculator(42);
  double rate = calc.monthlyRate;
  Console.WriteLine(rate);
}

Генерирует:

00000000  push        ebp                          ; setup stack frame
00000001  mov         ebp,esp 
00000003  sub         esp,8 
00000006  mov         ecx,349DFCh                  ; eax = new nonsensical_calculator
0000000b  call        FFC50AD4 
00000010  fld         dword ptr ds:[006E1590h]     ; st0 = 42
00000016  fstp        qword ptr [eax+4]            ; _rate = st0
00000019  fld         qword ptr [eax+4]            ; st0 = _rate
0000001c  fdiv        dword ptr ds:[006E1598h]     ; st0 = st0 / 12
00000022  fstp        qword ptr [ebp-8]            ; rate = st0
      Console.WriteLine(rate);
// etc..

Обратите внимание, что и вызов конструктора, и метод получения свойств исчезли: они встроены в Main().Код напрямую обращается к полю _rate.Даже переменная Calc исчезла, ссылка сохраняется в регистре eax.

Инструкция по адресу 19 показывает, что над оптимизатором можно проделать дополнительную работу.Если позволяет время.

Чтобы представить это немного по-другому, учтите, что свойства на самом деле являются просто обертками вокруг методов после компиляции кода в IL.Итак, если вместо этого:

public class nonsensical_calculator
{
    double bleh
    {
        get { return Math.Pow(ar - days, _term); }
    }
    // etc.
}

У вас было это:

public class nonsensical_calculator
{
    double GetBleh()
    {
        return Math.Pow(ar - days, _term);
    }
}

Ожидаете ли вы, что компилятор оптимизирует вызов метода за вас?

Я не эксперт по джиттеру, но сомневаюсь, что даже джиттер «запомнит» это;ему пришлось бы отслеживать все виды состояний и аннулировать запись при изменении любого из зависимых полей, и каким бы удивительным ни было дрожание .NET, я просто не думаю, что это так уж умно.Он может встроить метод, но обычно это не будет иметь большого значения с точки зрения производительности.

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

Если вам нужна производительность, предварительно вычисляйте эти значения всякий раз, когда изменяются зависимые поля.Или, еще лучше, профиль код, используя такой инструмент, как ЭКАТЕК (бесплатно) или МУРАВЬИ и посмотрите, где на самом деле находятся затраты на производительность.Оптимизация без профилирования — это все равно, что стрелять с завязанными глазами.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top