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

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

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

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

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

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

Примечание. Я рассматриваю случаи, когда отладчик отключен. Я спрашиваю о настройке конфигурации по умолчанию, которая поставляется с «отладкой» и «выпуском» в Visual Studio.


person nawfal    schedule 14.10.2013    source источник
comment
Первое, о чем вам нужно подумать, это то, что вы подразумеваете под режимом отладки и режимом выпуска. Существуют различные параметры времени компиляции, на которые влияет конфигурация сборки, но есть также разница между запуском с подключенным отладчиком или без него, что влияет на JIT-оптимизацию (как минимум).   -  person Jon Skeet    schedule 14.10.2013
comment
@JonSkeet Я говорю об отдельных случаях отладчика (которые я отредактирую в ответе), но я не знаю о других настройках времени компиляции. Вы имеете в виду конфигурацию платформы, такую ​​​​как x86, x64 и т. Д.?   -  person nawfal    schedule 14.10.2013
comment
Я имею в виду базовую конфигурацию Debug или Release, которая влияет на такие вещи, как настройки оптимизации времени компиляции и символы препроцессора, такие как DEBUG.   -  person Jon Skeet    schedule 14.10.2013
comment
@JonSkeet действительно именно об этих конфигурациях, о которых я говорю. Отличается ли режим выпуска от конфигурации выпуска?   -  person nawfal    schedule 14.10.2013
comment
Конфигурация в основном объединяет ряд переключателей, включая оптимизацию и символы препроцессора — было бы целесообразно указать, какие именно из них вас интересуют. .)   -  person Jon Skeet    schedule 14.10.2013
comment
@JonSkeet спасибо, я не знал об этом.   -  person nawfal    schedule 14.10.2013


Ответы (3)


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

У этого на самом деле очень простой ответ: нет. При наличии двух идентичных деревьев выражений LINQ/DLR не будет никакой разницы в сгенерированном IL, если одно из них скомпилировано приложением, работающим в режиме выпуска, а другое — в режиме отладки. Я не уверен, как это будет реализовано в любом случае; Я не знаю никакого надежного способа, позволяющего коду в 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 a 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 вручную вы должны выполнять работу, которую компилятор обычно выполняет за кулисами, например автоматически вставлять код для неявных преобразований.

person Mike Strobel    schedule 14.10.2013

Возведение в квадрат 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);

Результаты:

В конфигурации Debug длина метода времени компиляции равна 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() всегда имеет место с «оптимизациями»?

person Jeppe Stig Nielsen    schedule 14.10.2013
comment
Вы не можете сделать каких-либо осмысленных выводов, сравнивая IL, сгенерированный csc.exe, с IL, сгенерированным из дерева выражений LINQ/DLR: работают два совершенно разных компилятора, и нет никакой связи между типами выполняемых ими оптимизаций. Тот факт, что размер метода в режиме C# Release совпадает с размером метода, созданного LambdaCompiler, является случайным. Все это говорит о том, что LambdaCompiler создал более эффективный IL для тривиального метода, чем компилятор C# при работе в режиме отладки. - person Mike Strobel; 16.10.2013
comment
Однако, в зависимости от того, как вы это читаете, ваш вывод более или менее верен: оптимизации, выполняемые LambdaCompiler, фактически всегда включены в том смысле, что они не зависят от конфигурации сборки проекта. Однако оптимизации, выполняемые LambdaCompiler, в любом случае сильно отличаются от оптимизаций, выполняемых csc.exe. - person Mike Strobel; 16.10.2013
comment
@MikeStrobel Хорошая информация. У меня было подозрение, что это было что-то подобное, поэтому я старался быть осторожным в своих выводах. - person Jeppe Stig Nielsen; 16.10.2013
comment
В самом деле, и не помогает то, что компилятор C# по большей части является черным ящиком. Большая часть того, что мы знаем об оптимизации, которую он выполняет, была получена из комментариев инсайдеров Microsoft или путем сравнения выходных данных. Это означает, что я действительно должен был заявить, что два набора оптимизаций независимы друг от друга, а не совершенно различны; в моих комментариях тоже были некоторые предположения. - person Mike Strobel; 16.10.2013

Относительно ИЛ

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

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

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

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

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

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

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

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

person Andras Zoltan    schedule 16.01.2017