В C # копирование переменной члена в локальную переменную стека улучшает производительность? - PullRequest
8 голосов
/ 22 ноября 2011

Я довольно часто пишу код, который копирует переменные-члены в локальную переменную стека, полагая, что это повысит производительность, удалив разыменование указателя, которое должно иметь место при доступе к переменным-членам.

Это допустимо?

Например,

public class Manager {
    private readonly Constraint[] mConstraints;

    public void DoSomethingPossiblyFaster() 
    {
        var constraints = mConstraints;
        for (var i = 0; i < constraints.Length; i++) 
        {
            var constraint = constraints[i];
            // Do something with it
        }
    }

    public void DoSomethingPossiblySlower() 
    {
        for (var i = 0; i < mConstraints.Length; i++) 
        {
            var constraint = mConstraints[i];
            // Do something with it
        }
    }

}

Я думаю, что DoSomethingPossblyFaster на самом деле быстрее, чем DoSomethingPossblySlower.

Я знаю, что это в значительной степени микрооптимизация, но было бы полезно получить окончательный ответ.

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

Ответы [ 4 ]

16 голосов
/ 22 ноября 2011

Что является более читаемым ?Это обычно должно быть вашим основным мотивирующим фактором.Вам даже нужно использовать цикл for вместо foreach?

Поскольку mConstraints равно readonly Я бы потенциально ожидал, что JIT-компилятор сделает это за вас- но на самом деле, что ты делаешь в цикле?Вероятность того, что это будет значительным, довольно мала.Я бы почти всегда выбрал бы второй подход просто для удобства чтения - и я бы предпочел foreach, где это возможно.Оптимизирует ли JIT-компилятор этот случай, будет во многом зависеть от самого JIT - который может варьироваться в зависимости от версии, архитектуры и даже от того, насколько велик метод или другие факторы.Здесь не может быть быть никакого «окончательного» ответа, поскольку всегда возможно, что альтернативный JIT будет оптимизировать по-другому.имеет значение , вы должны сравнить его - тщательно, с максимально реалистичными данными. Только тогда вы должны изменить свой код с наиболее читаемой формы.Если вы «довольно часто» пишете такой код, маловероятно, что вы делаете что-то себе.

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

4 голосов
/ 22 ноября 2011

Если компилятор / JIT еще не делает это или подобную оптимизацию для вас (это большое значение, если), тогда DoSomethingPossiblyFaster должно быть быстрее, чем DoSomethingPossiblySlower.Лучший способ объяснить причину - взглянуть на грубый перевод кода C # на прямой C.

Когда вызывается нестатическая функция-член, в функцию передается скрытый указатель на this.Примерно следующее, игнорируя диспетчеризацию виртуальной функции, поскольку это не имеет отношения к вопросу (или эквивалентно делает Manager запечатанным для простоты):

struct Manager {
    Constraint* mConstraints;
    int mLength;
}

void DoSomethingPossiblyFaster(Manager* this) {
    Constraint* constraints = this->mConstraints;
    int length = this->mLength;


    for (int i = 0; i < length; i++) 
    {
        Constraint constraint = constraints[i];
        // Do something with it
    }
 }

void DoSomethingPossiblySlower() 
{
    for (int i = 0; i < this->mLength; i++) 
    {
        Constraint constraint = (this->mConstraints)[i];
        // Do something with it
    }
}

Разница в том, что в DoSomethingPossiblyFaster, mConstraints живет в стеке, и для доступа требуется только один уровень косвенности указателя, поскольку он находится на фиксированном смещении от указателя стека.В DoSomethingPossiblySlower, если компилятор упускает возможность оптимизации, есть дополнительная косвенная ссылка на указатель.Компилятор должен прочитать фиксированное смещение из указателя стека, чтобы получить доступ к this, а затем прочитать фиксированное смещение из this, чтобы получить mConstraints.

Есть две возможные оптимизации, которые могут свести на нет это попадание:

  1. Компилятор может делать то же, что вы делали вручную, и кэшировать mConstraints в стеке.

  2. Компилятор может хранить this врегистр, так что ему не нужно извлекать его из стека на каждой итерации цикла перед разыменованием.Это означает, что выборка mConstraints из this или из стека является в основном той же самой операцией: одиночная разыменование фиксированного смещения от указателя, который уже находится в регистре.

3 голосов
/ 22 ноября 2011

Вы знаете ответ, который вы получите, верно?«Время это».

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

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


Редактировать: (по направлению к окончательному ответу)

Компиляция обеих функций в режиме деблокирования и проверка IL с IL Dasm показывает, что в обоих местах "PossibleFaster""функция использует локальную переменную, у нее на одну инструкцию меньше
ldloc.0 против
ldarg.0; ldfld class Constraint[] Manager::mConstraints

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

1 голос
/ 23 ноября 2011

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

Самый быстрый режим выпуска X86. Это выполняет одну итерацию моего теста за 7,1 секунды, тогда как эквивалентный код X64 занимает 8,6 секунды. Это было 5 итераций, каждая из которых обрабатывала цикл 19,2 миллиона раз.

Самый быстрый подход для цикла был:

foreach (var constraint in mConstraints)
{
   ... do stuff ...
}

Вторым самым быстрым подходом, который меня очень удивил, было следующее

for (var i = 0; i < mConstraints.Length; i++)
{
    var constraint = mConstraints[i];
    ... do stuff ...
}

Я полагаю, это произошло потому, что mConstraints были сохранены в регистре для цикла.

Это замедлилось, когда я удалил опцию readonly для mConstraints.

Итак, я подытожил, что наличие в этой ситуации для чтения также дает производительность.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...