Оптимизирован ли IL, генерируемый деревьями выражений?

Хорошо, это просто любопытство, не помогает никакой реальной жизни.

Я знаю, что с деревьями выражений вы можете генерировать MSIL на лету, как это делает обычный компилятор C#. Поскольку компилятор может принимать решения об оптимизации, я испытываю желание спросить, как обстоят дела с IL, сгенерированным во время Expression.Compile(), В основном два вопроса:

  1. Поскольку во время компиляции компилятор может создавать разные (может быть незначительные) IL в режиме отладки и в режиме выпуска, есть ли когда-нибудь разница в IL, генерируемом путем компиляции выражения при сборке в режиме отладки и режиме выпуска?

  2. Также JIT, который преобразует IL в собственный код во время выполнения, должен сильно отличаться как в режиме отладки, так и в режиме выпуска. Это также относится и к скомпилированным выражениям? Или IL из деревьев выражений вообще не совпадает?

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

Примечание: я рассматриваю случаи, когда отладчик отключен. Я спрашиваю о настройке конфигурации по умолчанию, поставляемой с "отладкой" и "выпуском" в visual studio.

3 ответа

Решение

Поскольку во время компиляции компилятор может создавать разные (может быть незначительные) IL в режиме отладки и в режиме выпуска, есть ли когда-нибудь разница в IL, генерируемом путем компиляции выражения при сборке в режиме отладки и режиме выпуска?

У этого на самом деле очень простой ответ: нет. Учитывая два идентичных дерева выражений LINQ/DLR, не будет никакой разницы в сгенерированном IL, если одно скомпилировано приложением, работающим в режиме Release, а другое в режиме Debug. Я не уверен, как это будет реализовано в любом случае; Я не знаю ни одного надежного способа для кода в System.Core знать, что в вашем проекте выполняется отладочная сборка или сборка выпуска.

Однако этот ответ может вводить в заблуждение. IL, испускаемый компилятором выражений, может не отличаться между сборками отладки и выпуска, но в случаях, когда компилятором C# испускаются деревья выражений, возможно, что структура самих деревьев выражений может отличаться в режимах отладки и выпуска. Я довольно хорошо знаком с внутренними компонентами LINQ/DLR, но не так сильно с компилятором C#, поэтому могу только сказать, что в этих случаях может быть разница (а может и нет).

Также JIT, который преобразует IL в собственный код во время выполнения, должен сильно отличаться как в режиме отладки, так и в режиме выпуска. Это также относится и к скомпилированным выражениям? Или IL из деревьев выражений вообще не совпадает?

Машинный код, который выдает JIT-компилятор, не обязательно будет сильно отличаться для предварительно оптимизированного IL от неоптимизированного IL. Результаты вполне могут быть идентичными, особенно если единственными отличиями являются несколько дополнительных временных значений. Я подозреваю, что они будут расходиться больше в более крупных и более сложных методах, так как обычно есть верхний предел времени / усилий, которые JIT потратит на оптимизацию данного метода. Но, похоже, вас больше интересует сравнение качества скомпилированных деревьев выражений LINQ/DLR, скажем, с кодом C#, скомпилированным в режиме отладки или выпуска.

Я могу сказать вам, что LINQ/DLR LambdaCompiler выполняет очень мало оптимизаций - наверняка меньше, чем компилятор C# в режиме Release; Режим отладки может быть ближе, но я бы положил деньги на компилятор C#, так как он немного агрессивнее. LambdaCompiler как правило, не пытается сократить использование временных локальных элементов, а такие операции, как условные выражения, сравнения и преобразования типов, обычно используют больше промежуточных локальных элементов, чем вы могли бы ожидать. На самом деле я могу думать только о трех оптимизациях, которые он выполняет:

  1. Вложенные лямбды будут вставлены, когда это возможно (и "когда это возможно", как правило, "большую часть времени"). Это может очень помочь, на самом деле. Обратите внимание, это работает только тогда, когда вы Invoke LambdaExpression; он не применяется, если вы вызываете скомпилированный делегат в своем выражении.

  2. Ненужные / избыточные преобразования типов опускаются, по крайней мере, в некоторых случаях.

  3. Если значение TypeBinaryExpression (То есть, [value] is [Type]) известно во время компиляции, это значение может быть встроено как константа.

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

Как правило, следует предполагать, что IL, полученный из скомпилированного выражения LINQ/DLR, значительно менее оптимизирован, чем IL, созданный компилятором C#. Однако полученный IL-код пригоден для оптимизации JIT, поэтому трудно оценить реальное влияние на производительность, если вы на самом деле не пытаетесь измерить его с помощью эквивалентного кода.

При составлении кода с деревьями выражений следует помнить, что, по сути, вы являетесь компилятором 1. Деревья LINQ/DLR предназначены для излучения некоторой другой инфраструктуры компилятора, например, различных реализаций языка DLR. Поэтому вам решать обрабатывать оптимизации на уровне выражений. Если вы неаккуратный компилятор и создаете кучу ненужного или избыточного кода, сгенерированный IL будет больше и с меньшей вероятностью будет агрессивно оптимизирован компилятором JIT. Так что будьте внимательны к выражениям, которые вы строите, но не беспокойтесь слишком сильно. Если вам нужен высокооптимизированный IL, вы, вероятно, должны просто испустить его самостоятельно. Но в большинстве случаев деревья LINQ/DLR работают просто отлично.


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

Квадрат int,

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

// make delegate and find length of IL:
Func<int, int> f = x => x * x;
Console.WriteLine(f.Method.GetMethodBody().GetILAsByteArray().Length);

// make expression tree
Expression<Func<int, int>> e = x => x * x;

// one approach to finding IL length
var methInf = e.Compile().Method;
var owner = (System.Reflection.Emit.DynamicMethod)methInf.GetType().GetField("m_owner", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).GetValue(methInf);
Console.WriteLine(owner.GetILGenerator().ILOffset);

// another approach to finding IL length
var an = new System.Reflection.AssemblyName("myTest");
var assem = AppDomain.CurrentDomain.DefineDynamicAssembly(an, System.Reflection.Emit.AssemblyBuilderAccess.RunAndSave);
var module = assem.DefineDynamicModule("myTest");
var type = module.DefineType("myClass");
var methBuilder = type.DefineMethod("myMeth", System.Reflection.MethodAttributes.Static);
e.CompileToMethod(methBuilder);
Console.WriteLine(methBuilder.GetILGenerator().ILOffset);

Результаты:

В конфигурации отладки длина метода времени компиляции равна 8, а длина испускаемого метода - 4.

В конфигурации Release длина метода времени компиляции равна 4, а длина испускаемого метода также равна 4.

Метод времени компиляции, видимый IL DASM в режиме отладки:

.method private hidebysig static int32  '<Main>b__0'(int32 x) cil managed
{
  .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) 
  // Code size       8 (0x8)
  .maxstack  2
  .locals init ([0] int32 CS$1$0000)
  IL_0000:  ldarg.0
  IL_0001:  ldarg.0
  IL_0002:  mul
  IL_0003:  stloc.0
  IL_0004:  br.s       IL_0006
  IL_0006:  ldloc.0
  IL_0007:  ret
}

и выпуск:

.method private hidebysig static int32  '<Main>b__0'(int32 x) cil managed
{
  .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) 
  // Code size       4 (0x4)
  .maxstack  8
  IL_0000:  ldarg.0
  IL_0001:  ldarg.0
  IL_0002:  mul
  IL_0003:  ret
}

Отказ от ответственности: я не уверен, можно ли что-то сделать (это длинный "комментарий"), но, возможно, Compile() всегда происходит с "оптимизацией"?

Что касается IL

Как указывали другие ответы, обнаружение отладки / выпуска во время выполнения на самом деле не "вещь", потому что это решение во время компиляции, которое контролируется конфигурацией проекта, а не то, что действительно обнаруживается во встроенной сборке. Время выполнения может отражать AssemblyConfiguration атрибут на сборке, проверяя его Configurationсвойство - но это было бы неточным решением для чего-то столь фундаментального для.Net - потому что эта строка может буквально быть чем угодно.

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

Наконец, как уже упоминали другие, DEBUG != UNOPTIMISED - Концепция "отлаживаемой" сборки больше связана с соглашениями, чем с чем-либо еще (что отражено в настройках компиляции по умолчанию для проекта.Net) - соглашениями, которые управляют деталями в PDB (между прочим, не существует таковой), и оптимизирован ли код или нет. Таким образом, возможно иметь оптимизированную сборку отладки, а также неоптимизированную сборку релиза и даже оптимизированную сборку релиза с полной информацией о PDB, которую можно отлаживать так же, как стандартную сборку "отладка".

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

Но как насчет JIT?

Однако, когда дело доходит до JIT, переводящего IL в ассемблер, я думаю, что стоит отметить, что JIT (хотя и не уверен в ядре.Net) ведет себя по-разному, если процесс запускается с подключенным отладчиком по сравнению с запущенным без него. Попробуйте запустить сборку релиза с F5 от VS и сравнить поведение отладки с подключением к нему после того, как он уже запущен.

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

Суть моей мысли здесь в том, что если присутствие отладчика может повлиять на поведение JITing для статически построенного IL, то оно, вероятно, повлияет на его поведение при динамическом построении JIT, таком как связанные делегаты или, в данном случае, деревья выражений. Но насколько я отличаюсь, я не уверен, что мы можем сказать.

Другие вопросы по тегам