Могу ли я заставить компилятор оптимизировать определенный метод?

Есть ли атрибут, который я могу использовать, чтобы сообщить компилятору, что метод всегда должен быть оптимизирован, даже если глобальный переключатель компилятора /o+ не установлен?

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


РЕДАКТИРОВАТЬ: подробнее о неоптимизациях, которые меня беспокоят...

Рассмотрим следующую реализацию факториальной функции:

static long FactorialRec(int n, long acc)
{
    if (n == 0)
        return acc;
    return FactorialRec(n - 1, acc * n);
}

(Примечание: я знаю, что есть лучшие способы вычисления факториала, это всего лишь пример)

IL, созданный с включенными оптимизациями, довольно прост:

IL_0000:  ldarg.0     
IL_0001:  brtrue.s    IL_0005
IL_0003:  ldarg.1     
IL_0004:  ret         
IL_0005:  ldarg.0     
IL_0006:  ldc.i4.1    
IL_0007:  sub         
IL_0008:  ldarg.1     
IL_0009:  ldarg.0     
IL_000A:  conv.i8     
IL_000B:  mul         
IL_000C:  call        UserQuery.FactorialRec
IL_0011:  ret         

Но неоптимизированная версия совсем другая

IL_0000:  nop         
IL_0001:  ldarg.0     
IL_0002:  ldc.i4.0    
IL_0003:  ceq         
IL_0005:  ldc.i4.0    
IL_0006:  ceq         
IL_0008:  stloc.1     
IL_0009:  ldloc.1     
IL_000A:  brtrue.s    IL_0010
IL_000C:  ldarg.1     
IL_000D:  stloc.0     
IL_000E:  br.s        IL_001F
IL_0010:  ldarg.0     
IL_0011:  ldc.i4.1    
IL_0012:  sub         
IL_0013:  ldarg.1     
IL_0014:  ldarg.0     
IL_0015:  conv.i8     
IL_0016:  mul         
IL_0017:  call        UserQuery.FactorialRec
IL_001C:  stloc.0     
IL_001D:  br.s        IL_001F
IL_001F:  ldloc.0     
IL_0020:  ret         

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

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


person Thomas Levesque    schedule 13.03.2012    source источник
comment
AFAIK, нет - это невозможно   -  person Marc Gravell    schedule 13.03.2012
comment
Компилятор JIT всегда оптимизирует каждый метод.   -  person Steven    schedule 13.03.2012
comment
@ Стивен, нет, если вы не скажете (например, с флагом NoOptimization в MethodImplAttribute). Но в любом случае мой вопрос касается оптимизации компилятора, а не оптимизации JIT, поскольку меня интересует сгенерированный код IL.   -  person Thomas Levesque    schedule 13.03.2012
comment
Я хотел бы увидеть, как вы задокументируете точный тип IL, с которым у вас возникла проблема. Включение оптимизации в компиляторе C# на самом деле малоэффективно, оно просто пропускает коды операций NOP, которые выдает компилятор, чтобы упростить отладку. Единственное практическое решение, которое я могу придумать, — это перенести код в отдельную сборку, которую затем можно просто собрать с включенной оптимизацией.   -  person Hans Passant    schedule 13.03.2012
comment
@HansPassant, это не просто коды операций nop (я мог бы легко их игнорировать), поток кода IL на самом деле другой. Я обновил свой вопрос, чтобы добавить больше деталей.   -  person Thomas Levesque    schedule 13.03.2012
comment
Это звучит так, как будто это может быть грязно - если вы когда-нибудь окажетесь в ситуации, когда вам нужно отладить исходный метод (и поэтому отключите функцию оптимизации всегда, если вы когда-нибудь ее найдете), но там, где это преобразование существующей функциональности метода должно также работать, вы обречены.   -  person Damien_The_Unbeliever    schedule 13.03.2012
comment
Мне кажется, что вы не можете хвостовой вызов оптимизировать код, скомпилированный без /optimize, потому что вызов на самом деле не является последней инструкцией, поэтому ваш дополнительный оптимизатор должен оставить код в покое.   -  person NetMage    schedule 15.03.2012
comment
@NetMage, но что, если код не может выполняться без оптимизации хвостового вызова? т. е. если рекурсия слишком глубокая, она взорвет стек   -  person Thomas Levesque    schedule 15.03.2012


Ответы (3)


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

Что касается исходной проблемы, поскольку MSIL - это язык на основе стека. И спецификации гарантируют состояние стека в операторе ret, вы можете быть на 100% уверены, что можете без проблем добавить хвостовой префикс. Тем не менее, это также маловероятно, что на самом деле это принесет какую-либо пользу, поскольку я действительно не видел, чтобы JIT использовал префикс хвоста для фактической оптимизации окончательно обработанного кода.

person Paul Alexander    schedule 15.03.2012
comment
Связано: stackoverflow.com/questions/491376 - очевидно, .tail оптимизирован для x64, но не для x86. - person BlueRaja - Danny Pflughoeft; 01.05.2013

Можно ли в любом случае динамически генерировать исходный код метода с помощью Microsoft.CSharp.CSharpCodeProvider?

Если вы управляете компиляцией метода, вы можете установить параметры при вызове компилятора с помощью Параметры компилятора.

person NetMage    schedule 14.03.2012
comment
Это не поможет; Я не хочу генерировать код динамически (я имею в виду не исходный код) - person Thomas Levesque; 15.03.2012

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

В частности, даже с call ... ret JITter не гарантирует хвостовой вызов. Таким образом, код IMO С#, основанный на оптимизации хвостового вызова (во избежание переполнения стека), просто не работает. В C# оптимизация хвостового вызова — это чисто оптимизация производительности.

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

person CodesInChaos    schedule 14.03.2012
comment
Я знаю, что C# не подходит для хвостовых вызовов, я просто делал доказательство концепции... не волнуйтесь, я не собираюсь использовать это в рабочем коде;) - person Thomas Levesque; 15.03.2012