Производительность выходного параметра С#

Влияют ли параметры out в C# на производительность, о которых мне следует знать? (как исключения)

Я имею в виду, это хорошая идея иметь метод с параметром out в цикле, который будет запускаться пару миллионов раз в секунду?

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


person Tamas Czinege    schedule 09.02.2009    source источник


Ответы (6)


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

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

Я не согласен с утверждением Конрада о том, что «возвращаемые значения для всех типов> 32 бит в любом случае обрабатываются аналогично или идентичным аргументам out на машинном уровне». Вот небольшое тестовое приложение:

using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;

struct BigStruct
{
    public Guid guid1, guid2, guid3, guid4;
    public decimal dec1, dec2, dec3, dec4;
}

class Test
{
    const int Iterations = 100000000;

    static void Main()
    {
        decimal total = 0m;
        // JIT first
        ReturnValue();
        BigStruct tmp;
        OutParameter(out tmp);

        Stopwatch sw = Stopwatch.StartNew();
        for (int i=0; i < Iterations; i++)
        {
            BigStruct bs = ReturnValue();
            total += bs.dec1;
        }
        sw.Stop();
        Console.WriteLine("Using return value: {0}",
                          sw.ElapsedMilliseconds);

        sw = Stopwatch.StartNew();
        for (int i=0; i < Iterations; i++)
        {
            BigStruct bs;
            OutParameter(out bs);
            total += bs.dec1;
        }
        Console.WriteLine("Using out parameter: {0}",
                          sw.ElapsedMilliseconds);
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    public static BigStruct ReturnValue()
    {
        return new BigStruct();
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    public static void OutParameter(out BigStruct x)
    {
        x = new BigStruct();
    }
}

Результаты:

Using return value: 11316
Using out parameter: 7461

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

Не стесняйтесь критиковать тестовое приложение — возможно, я что-то упустил!

person Jon Skeet    schedule 09.02.2009
comment
Интересный результат - в моем случае параметр out - это просто перечисление int, но ничего особенного. - person Tamas Czinege; 09.02.2009
comment
Джон: пожалуйста, обдумайте мой ответ на ваше замечание. Ваш код на самом деле довольно хорош, потому что он показывает жалкое отсутствие оптимизации в возвращаемых значениях, но это не то, что я имел в виду в своем заявлении. - person Konrad Rudolph; 09.02.2009
comment
@Konrad: Тогда я предлагаю вам пересмотреть свое заявление. Выходной параметр C# явно не обрабатывается так же, как возвращаемое значение C# на машинном уровне в .NET. - person Jon Skeet; 09.02.2009
comment
@Jon: да, я добавил уточнение. Заметьте, что ваш эталон — именно та причина, по которой я изначально написал «похожие или идентичные»: я не был уверен, выполнил ли JIT необходимую оптимизацию, чтобы исключить бесполезную копию объекта. - person Konrad Rudolph; 09.02.2009
comment
+1 - Очень хорошая работа. Этот, должно быть, обратился к вашему любопытству. Я бы догадался, как и Конрад, что выходные параметры и возвращаемые значения будут обрабатываться в стеке. Однако, подумав об этом и увидев ваш пример, безусловно, действительно имеет смысл записывать результаты непосредственно в кучу. - person Mark Brittingham; 09.02.2009
comment
@Mark: я сомневаюсь, что здесь задействована куча (поскольку это все локальные переменные). out вместо этого будет передавать указатель на место в стеке. - person Konrad Rudolph; 09.02.2009
comment
@Konrad: Спасибо за разъяснения, это очень помогает. - person Jon Skeet; 09.02.2009
comment
@Jon: я думаю, что в одном из циклов может быть скрытая инициализация переменной. Попробуйте использовать BigStruct bs = new BigStruct(); - меняет ли это результаты (я бы попробовал, но мой ноутбук сломался :() - person configurator; 09.02.2009
comment
@configurator: Да, это делает исходную версию немного медленнее (но все же быстрее, чем возврат) - но зачем мне вообще это делать? Значение не используется. - person Jon Skeet; 09.02.2009
comment
Я думаю, что опция NoInlining здесь неуместна - JIT должен встроить вызов; это сделало бы избыточную копию гораздо более очевидной для оптимизатора. Однако удаление атрибута и повторный запуск теста приводят к той же тенденции — версия с возвращаемым значением (намного) медленнее. Использование AggressiveInlining вряд ли поможет; но это влияет на производительность в слегка измененном бенчмарке, поэтому похоже, что оно работает. IOW: даже при встроенном компиляторе JIT не может очень хорошо обнаруживать избыточные копии. - person Eamon Nerbonne; 04.02.2013
comment
@JonSkeet: одна любопытная особенность C# и vb.net заключается в том, что такой оператор, как someVariable = new structType(params);, часто реализуется путем передачи someVariable в качестве параметра out в конструктор; это хорошо для производительности, но иногда может вызвать интересные семантические причуды. Поскольку параметры out представляют собой концепцию C#, а не .net, виртуальный метод с параметром out, который переопределяется в языке, отличном от C#, не должен ничего делать с этим параметром. Если конструктор структуры передает this такому методу, C# считает, что он определенно назначен, но он может не быть записан. - person supercat; 12.02.2013
comment
Привет. Я из будущего. А в наше время (2018 год, .NET 4.7.2) я запускал этот бенчмарк. Я получил следующее: Out -> 982, Return -> 985 (скомпилировано для x64, x86 больше похоже на возврат ~2700 против ~1550 на выходе). Просто поместите это здесь для тех, кто, возможно, изучает это. - person Mike; 21.08.2018

Не проблема производительности, а то, что возникло ранее - вы не можете использовать их с вариантностью в C# 4.0.

Лично я предпочитаю использовать out параметры в достаточном количестве в своем частном коде (т.е. внутри класса, имея метод, возвращающий несколько значений без использования отдельного типа), но стараюсь избегать их в общедоступный API, за исключением шаблона bool Try{Something}(out result).

person Marc Gravell    schedule 09.02.2009

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

На самом деле возвращаемые значения для всех типов > 32 бит в любом случае обрабатываются так же, как out аргументы на машинном уровне.

Обратите внимание, что последний оператор не предполагает, что возвращаемое значение параметра == out в .NET. Бенчмарк Джона показывает, что это явно (и, к сожалению) не так. На самом деле, чтобы сделать его идентичным, оптимизация именованного возвращаемого значения используется в компиляторах C++. Что-то подобное потенциально может быть сделано в будущих версиях JIT для повышения производительности возврата больших структур (однако, поскольку большие структуры довольно редко встречаются в .NET, эта оптимизация может оказаться ненужной).

Однако (и с моими очень ограниченными знаниями сборки x86) возврат объектов из вызовов функций обычно влечет за собой выделение достаточного пространства в месте вызова, помещение адреса в стек и заполнение его путем копирования возвращаемого значения внутрь. Это в основном то же самое, что и out, только опускает ненужную временную копию значения, поскольку к целевой ячейке памяти можно получить прямой доступ.

person Konrad Rudolph    schedule 09.02.2009
comment
Я думаю, что я категорически не согласен с последним утверждением... хотите подтвердить это? Пишем небольшое тестовое приложение... - person Jon Skeet; 09.02.2009
comment
Джон: Я не уверен, что ты имеешь в виду. На уровне машинного кода (в сборке X86) вы возвращаете значения, помещая их в стек, как и параметры. Это все, что я имел в виду. Я не имел в виду эквивалентность более высокого уровня в CIL. - person Konrad Rudolph; 09.02.2009
comment
Конрад: IIRC, на уровне машинного кода x86 вы возвращаете значения с помощью eax - person Tamas Czinege; 09.02.2009
comment
Джон: также взгляните на это: blogs.msdn.com/slippman /archive/2004/02/03/66739.aspx — я знаю, что RVO — это не концепция .NET, но я думаю, что упомянутое там преобразование — от возвращаемого значения к выходному параметру — происходит практически везде. Также сравните использование COM возвращаемого значения! - person Konrad Rudolph; 09.02.2009
comment
@DrJokepu: точно моя точка зрения; eax содержит только 32 бита. Вы можете поместить туда указатель или число, но редко полный объект. - person Konrad Rudolph; 09.02.2009
comment
@Konrad: Посмотрите на мой тест. Возврат значения напрямую означает, что оно находится в исходном стеке, а затем копируется в вызывающую программу. Использование параметра out позволяет избежать этого - вы пишете непосредственно в память, которая этого хочет, за счет небольшого перенаправления. - person Jon Skeet; 09.02.2009
comment
@Jon - значит ли это, что для больших структур параметры будут более эффективными? - person configurator; 09.02.2009
comment
Последнее предложение разъяснения является важным отличием ИМО :) - person Jon Skeet; 09.02.2009
comment
Я понимаю, что вы имеете в виду, но не убьет ли раскручивание стека ссылочный адрес, который возвращается на %eax в качестве оптимизации? Вероятно, это все равно не по теме в этом вопросе. - person Edwin Jarvis; 09.02.2009
comment
@Augusto: чтобы предотвратить это, память для возвращаемого значения выделяется на сайте вызывающего, а не на сайте вызываемого. - person Konrad Rudolph; 09.02.2009
comment
Взял по вашей ссылке, очень интересно. - person Edwin Jarvis; 09.02.2009

Основной причиной отказа от параметров out является читаемость кода, а не производительность.

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

В девяти случаях из десяти вам лучше создать свой собственный тупой класс записи, а не использовать параметр out — его проще читать и понимать, когда вы вернетесь к коду позже.

person Keith    schedule 09.02.2009
comment
Это может быть полезно в функциях Try.., таких как Dictionary‹›.TryGetValue и int.TryParse... - person thinkbeforecoding; 09.02.2009
comment
Да, мне не очень нравится шаблон out по тем же причинам, но в этом случае он на самом деле более читаем, чем использование какого-либо класса-контейнера. - person Tamas Czinege; 09.02.2009
comment
По этой причине я не слишком люблю шаблон TryParse, но всегда есть некоторые исключения. В вашем собственном коде обычно проще их не использовать, и нет никакой разницы в производительности. - person Keith; 09.02.2009
comment
Для типов значений вы избегаете второй копии, так как метод записывает прямо в пространство, на которое указывает вызывающий. Для больших структур это может быть экономией. - person Marc Gravell; 09.02.2009
comment
Спасибо @Marc и @Jon - так что параметры на самом деле быстрее (+1 к обоим вашим ответам)! Здесь вы постоянно изучаете что-то новое ;-) Я по-прежнему считаю, что в 90% случаев вам лучше использовать класс записи для удобства чтения. - person Keith; 10.02.2009

Out параметры передаются по ссылке. Таким образом, в стеке передается только указатель.

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

person thinkbeforecoding    schedule 09.02.2009
comment
Я думаю, вы думаете о другом языке, чем С# :) - person Giovanni Galbo; 09.02.2009
comment
@ Джованни - хочешь пройти квалификацию? Для меня это звучит нормально - единственный незначительный момент заключается в том, что компилятор выполняет разыменование, а не вы... - person Marc Gravell; 09.02.2009
comment
Нет, значение не копируется в стек, поэтому, когда значение необходимо для чтения (после инициализации... хорошо, изменяемые переменные не рекомендуются), вы должны получить значение из ссылки. - person thinkbeforecoding; 09.02.2009

Использование параметра out не влияет на производительность. Параметр Out в основном является ссылочным параметром, поэтому и вызывающий, и вызываемый объект указывают на один и тот же участок памяти.

person Giovanni Galbo    schedule 09.02.2009