Почему Func <> создан из выражения <Func <>> медленнее, чем Func <> объявлен напрямую?
Почему Func<>
, созданный из Expression<Func<>>
через .Compile(), значительно медленнее, чем просто объявленный Func<>
?
Я просто отказался от использования Func<IInterface, object>
, объявленного непосредственно на созданном из Expression<Func<IInterface, object>>
в приложении, над которым я работаю, и заметил, что производительность снижается.
Я только что сделал небольшой тест, а Func<>
, созданный из выражения, "почти" удваивает время Func<>
, объявленного напрямую.
На моей машине Direct Func<>
занимает около 7,5 секунд, а Expression<Func<>>
занимает около 12,6 секунд.
Вот тестовый код, который я использовал (запуская Net 4.0)
// Direct
Func<int, Foo> test1 = x => new Foo(x * 2);
int counter1 = 0;
Stopwatch s1 = new Stopwatch();
s1.Start();
for (int i = 0; i < 300000000; i++)
{
counter1 += test1(i).Value;
}
s1.Stop();
var result1 = s1.Elapsed;
// Expression . Compile()
Expression<Func<int, Foo>> expression = x => new Foo(x * 2);
Func<int, Foo> test2 = expression.Compile();
int counter2 = 0;
Stopwatch s2 = new Stopwatch();
s2.Start();
for (int i = 0; i < 300000000; i++)
{
counter2 += test2(i).Value;
}
s2.Stop();
var result2 = s2.Elapsed;
public class Foo
{
public Foo(int i)
{
Value = i;
}
public int Value { get; set; }
}
Как вернуть производительность?
Есть ли что-нибудь, что я могу сделать, чтобы получить Func<>
, созданный из Expression<Func<>>
, чтобы выполнить, как объявлено напрямую?
Ответы
Ответ 1
Как упоминалось выше, накладные расходы на вызов динамического делегата вызывают замедление. На моем компьютере накладные расходы составляют около 12 нс с моим процессором на частоте 3 ГГц. Способ обойти это - загрузить метод из скомпилированной сборки, например:
var ab = AppDomain.CurrentDomain.DefineDynamicAssembly(
new AssemblyName("assembly"), AssemblyBuilderAccess.Run);
var mod = ab.DefineDynamicModule("module");
var tb = mod.DefineType("type", TypeAttributes.Public);
var mb = tb.DefineMethod(
"test3", MethodAttributes.Public | MethodAttributes.Static);
expression.CompileToMethod(mb);
var t = tb.CreateType();
var test3 = (Func<int, Foo>)Delegate.CreateDelegate(
typeof(Func<int, Foo>), t.GetMethod("test3"));
int counter3 = 0;
Stopwatch s3 = new Stopwatch();
s3.Start();
for (int i = 0; i < 300000000; i++)
{
counter3 += test3(i).Value;
}
s3.Stop();
var result3 = s3.Elapsed;
Когда я добавляю вышеприведенный код, result3
всегда составляет лишь часть секунды выше, чем result1
, для примерно 1 нс служебных данных.
Итак, зачем даже скомпилировать lambda (test2
), когда у вас может быть более быстрый делегат (test3
)? Поскольку создание динамической сборки в общем случае является намного более накладным и сохраняет только 10-20 нс при каждом вызове.
Ответ 2
(Это не правильный ответ, но материал предназначен для того, чтобы помочь найти ответ.)
Статистика, собранная от Mono 2.6.7 - Debian Lenny - Linux 2.6.26 i686 - 2.80GHz одноядерное:
Func: 00:00:23.6062578
Expression: 00:00:23.9766248
Итак, на Mono, по крайней мере, оба механизма, похоже, генерируют эквивалентный IL.
Это IL, сгенерированный Mono gmcs
для анонимного метода:
// method line 6
.method private static hidebysig
default class Foo '<Main>m__0' (int32 x) cil managed
{
.custom instance void class [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::'.ctor'() = (01 00 00 00 ) // ....
// Method begins at RVA 0x2204
// Code size 9 (0x9)
.maxstack 8
IL_0000: ldarg.0
IL_0001: ldc.i4.2
IL_0002: mul
IL_0003: newobj instance void class Foo::'.ctor'(int32)
IL_0008: ret
} // end of method Default::<Main>m__0
Я буду работать над извлечением IL, сгенерированным компилятором выражения.
Ответ 3
В конечном счете, это то, что Expression<T>
не является предварительно скомпилированным делегатом. Это только дерево выражений. Вызов компиляции на LambdaExpression
(что и есть на самом деле Expression<T>
) генерирует код IL во время выполнения и создает для него что-то похожее на DynamicMethod
.
Если вы просто используете код Func<T>
, он предварительно компилирует его так же, как и любую другую ссылку делегата.
Итак, здесь есть 2 источника медлительности:
-
Исходное время компиляции для компиляции Expression<T>
в делегат. Это огромно. Если вы делаете это для каждого вызова - определенно нет (но это не так, поскольку вы используете свой секундомер после вызова компиляции.
-
Это a DynamicMethod
в основном после вызова компиляции. DynamicMethod
(даже сильно типизированные делегаты для них) Фактически медленнее выполнять, чем прямые вызовы. Func<T>
, разрешенные во время компиляции, являются прямыми вызовами. Там были сопоставления производительности между динамически испускаемым IL и временем компиляции IL. Случайный URL: http://www.codeproject.com/KB/cs/dynamicmethoddelegates.aspx?msg=1160046
... Кроме того, в вашем секундомерном тесте для Expression<T>
вы должны запустить свой таймер, когда я = 1, а не 0... Я считаю, что ваша скомпилированная Лямбда не будет JIT, скомпилированной до первого вызова, так что будет хитом производительности для этого первого вызова.
Ответ 4
Это, скорее всего, потому, что первый вызов кода не был перекошен.
Я решил посмотреть на IL, и они практически идентичны.
Func<int, Foo> func = x => new Foo(x * 2);
Expression<Func<int, Foo>> exp = x => new Foo(x * 2);
var func2 = exp.Compile();
Array.ForEach(func.Method.GetMethodBody().GetILAsByteArray(), b => Console.WriteLine(b));
var mtype = func2.Method.GetType();
var fiOwner = mtype.GetField("m_owner", BindingFlags.Instance | BindingFlags.NonPublic);
var dynMethod = fiOwner.GetValue(func2.Method) as DynamicMethod;
var ilgen = dynMethod.GetILGenerator();
byte[] il = ilgen.GetType().GetMethod("BakeByteArray", BindingFlags.NonPublic | BindingFlags.Instance).Invoke(ilgen, null) as byte[];
Console.WriteLine("Expression version");
Array.ForEach(il, b => Console.WriteLine(b));
Этот код получает байт-массивы и печатает их на консоли. Вот вывод на моем компьютере::
2
24
90
115
13
0
0
6
42
Expression version
3
24
90
115
2
0
0
6
42
И вот рефлекторная версия первой функции::
L_0000: ldarg.0
L_0001: ldc.i4.2
L_0002: mul
L_0003: newobj instance void ConsoleApplication7.Foo::.ctor(int32)
L_0008: ret
Во всем методе всего 2 байта!
Это первый код операции, который для первого метода, ldarg0 (загружает первый аргумент), но во втором методе ldarg1 (загружает второй аргумент). Разница здесь в том, что объект сгенерированный выражением фактически имеет цель объекта Closure
. Это также может иметь значение.
Следующий код операции для обоих - ldc.i4.2 (24), что означает загрузку 2 в стек, следующий - код операции для mul
(90), следующий код операции - код операции newobj
(115), Следующие 4 байта - это токен метаданных для объекта .ctor
. Они отличаются друг от друга, поскольку два метода фактически размещаются в разных сборках. Анонимный метод находится в анонимной сборке. К сожалению, я не совсем понял, как решить эти жетоны. Конечный код операции - 42, который равен ret
. Каждая функция CLI должна заканчиваться ret
четными функциями, которые ничего не возвращают.
Есть несколько возможностей, объект закрытия каким-то образом заставляет вещи замедляться, что может быть правдой (но маловероятно), дрожание не способствовало этому методу, и поскольку вы стреляли быстрым чередованием, у него не было до времени до этого пути, ссылаясь на более медленный путь. Компилятор С# в vs также может излучать разные соглашения о вызовах и MethodAttributes
, которые могут выступать в качестве подсказок для джиттера для выполнения различных оптимизаций.
В конечном счете, я бы даже не отдаленно беспокоился об этой разнице. Если вы действительно используете свою функцию 3 миллиарда раз в ходе вашего приложения, а разница в том, что она составляет 5 целых секунд, вы, вероятно, будете в порядке.
Ответ 5
Только для записи: я могу воспроизвести числа с приведенным выше кодом.
Следует отметить, что оба делегата создают новый экземпляр Foo для каждой итерации. Это может быть более важным, чем то, как создаются делегаты. Это не только приводит к множеству распределений кучи, но и GC может также влиять на цифры здесь.
Если я изменил код на
Func<int, int> test1 = x => x * 2;
и
Expression<Func<int, int>> expression = x => x * 2;
Func<int, int> test2 = expression.Compile();
Номера производительности практически идентичны (фактически результат2 немного лучше результата1). Это подтверждает теорию о том, что дорогая часть - это распределение кучи и/или коллекции, а не то, как создается делегат.
UPDATE
Следуя комментарию от Gabe, я попытался изменить Foo
как структуру. К сожалению, это дает более или менее то же количество, что и исходный код, поэтому, возможно, сбор кучи/сбор мусора не является причиной в конце концов.
Однако я также проверил числа для делегатов типа Func<int, int>
, и они очень похожи и намного ниже цифр исходного кода.
Я продолжу копать и с нетерпением жду новых/обновленных ответов.
Ответ 6
Меня интересовал ответ Майкла Б., поэтому я добавил в каждом случае дополнительный вызов до начала секундомера. В режиме отладки метод компиляции (случай 2) выполнялся быстрее почти в два раза (от 6 секунд до 10 секунд), а в режиме выпуска обе версии обе версии были на уровне (разница составляла около ~ 0,2 секунды).
Теперь, что поразительно для меня, что с JIT вышло из уравнения, я получил противоположные результаты, чем Мартин.
Изменить: сначала я пропустил Foo, поэтому результаты выше для Foo с полем, а не с собственностью, с оригинальным Foo сравнение одинаковое, только время больше - 15 секунд для прямого func, 12 секунд для скомпилированных версия. Опять же, в режиме выпуска время похоже, теперь разница составляет около ~ 0,5.
Однако это указывает на то, что если ваше выражение будет более сложным, даже в режиме выпуска будет реальная разница.