В настоящее время я занимаюсь некоторыми оптимизациями последней меры, в основном для развлечения и обучения, и обнаружил кое-что, что оставило у меня пару вопросов.
Во-первых, вопросы:
Expression<...>
версия моего простого метода сложения (Int32+Int32 => Int32) работает быстрее, чем минимальная версия DynamicMethod?Вот короткая и полная программа, которая демонстрирует это. На моей системе вывод такой:
DynamicMethod: 887 ms
Lambda: 1878 ms
Method: 1969 ms
Expression: 681 ms
Я ожидал, что лямбда и вызовы методов будут иметь более высокие значения, но версия DynamicMethod постоянно примерно на 30-50% медленнее (вариации, вероятно, из-за Windows и других программ). Кто-нибудь знает причину?
Вот программа:
using System;
using System.Linq.Expressions;
using System.Reflection.Emit;
using System.Diagnostics;
namespace Sandbox
{
public class Program
{
public static void Main(String[] args)
{
DynamicMethod method = new DynamicMethod("TestMethod",
typeof(Int32), new Type[] { typeof(Int32), typeof(Int32) });
var il = method.GetILGenerator();
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Add);
il.Emit(OpCodes.Ret);
Func<Int32, Int32, Int32> f1 =
(Func<Int32, Int32, Int32>)method.CreateDelegate(
typeof(Func<Int32, Int32, Int32>));
Func<Int32, Int32, Int32> f2 = (Int32 a, Int32 b) => a + b;
Func<Int32, Int32, Int32> f3 = Sum;
Expression<Func<Int32, Int32, Int32>> f4x = (a, b) => a + b;
Func<Int32, Int32, Int32> f4 = f4x.Compile();
for (Int32 pass = 1; pass <= 2; pass++)
{
// Pass 1 just runs all the code without writing out anything
// to avoid JIT overhead influencing the results
Time(f1, "DynamicMethod", pass);
Time(f2, "Lambda", pass);
Time(f3, "Method", pass);
Time(f4, "Expression", pass);
}
}
private static void Time(Func<Int32, Int32, Int32> fn,
String name, Int32 pass)
{
Stopwatch sw = new Stopwatch();
sw.Start();
for (Int32 index = 0; index <= 100000000; index++)
{
Int32 result = fn(index, 1);
}
sw.Stop();
if (pass == 2)
Debug.WriteLine(name + ": " + sw.ElapsedMilliseconds + " ms");
}
private static Int32 Sum(Int32 a, Int32 b)
{
return a + b;
}
}
}
Метод, созданный через DynamicMethod
, проходит через два тэнка, в то время как метод, созданный через Expression<>
, не проходит ни через один.
Вот как это работает. Вот последовательность вызовов для вызова fn(0, 1)
в методе Time
(я жестко закодировал аргументы в 0 и 1 для удобства отладки):
00cc032c 6a01 push 1 // 1 argument
00cc032e 8bcf mov ecx,edi
00cc0330 33d2 xor edx,edx // 0 argument
00cc0332 8b410c mov eax,dword ptr [ecx+0Ch]
00cc0335 8b4904 mov ecx,dword ptr [ecx+4]
00cc0338 ffd0 call eax // 1 arg on stack, two in edx, ecx
Для первого вызова, который я исследовал, DynamicMethod
, строка call eax
выглядит следующим образом:
00cc0338 ffd0 call eax {003c2084}
0:000> !u 003c2084
Unmanaged code
003c2084 51 push ecx
003c2085 8bca mov ecx,edx
003c2087 8b542408 mov edx,dword ptr [esp+8]
003c208b 8b442404 mov eax,dword ptr [esp+4]
003c208f 89442408 mov dword ptr [esp+8],eax
003c2093 58 pop eax
003c2094 83c404 add esp,4
003c2097 83c010 add eax,10h
003c209a ff20 jmp dword ptr [eax]
Похоже, что это делает некоторое перемещение стека для перестановки аргументов. Я предполагаю, что это связано с разницей между делегатами, которые используют неявный аргумент 'this' и теми, которые его не используют.
Этот прыжок в конце решается следующим образом:
003c209a ff20 jmp dword ptr [eax] ds:0023:012f7edc=0098c098
0098c098 e963403500 jmp 00ce0100
Остаток кода в 0098c098 выглядит как JIT thunk, начало которого было переписано с jmp
после JIT. Только после этого перехода мы переходим к настоящему коду:
0:000> !u eip
Normal JIT generated code
DynamicClass.TestMethod(Int32, Int32)
Begin 00ce0100, size 5
>>> 00ce0100 03ca add ecx,edx
00ce0102 8bc1 mov eax,ecx
00ce0104 c3 ret
Последовательность вызовов для метода, созданного через Expression<>
, отличается - в ней'отсутствует код сглаживания стека. Вот он, из первого перехода через eax
:
00cc0338 ffd0 call eax {00ce00a8}
0:000> !u eip
Normal JIT generated code
DynamicClass.lambda_method(System.Runtime.CompilerServices.ExecutionScope, Int32, Int32)
Begin 00ce00a8, size b
>>> 00ce00a8 8b442404 mov eax,dword ptr [esp+4]
00ce00ac 03d0 add edx,eax
00ce00ae 8bc2 mov eax,edx
00ce00b0 c20400 ret 4
Итак, как же так получилось?
Я не знаю, как LINQ заставил JIT, но я знаю, как заставить JIT самостоятельно - вызвав функцию хотя бы один раз. UPDATE: Я нашел другой способ принудительного JIT: использовать аргумент restrictedSkipVisibility
в конструкторе и передать true
. Итак, вот модифицированный код, который устраняет сметание стека, используя неявный параметр 'this', и использует альтернативный конструктор для предварительной компиляции, чтобы связанный адрес был реальным адресом, а не thunk:
using System;
using System.Linq.Expressions;
using System.Reflection.Emit;
using System.Diagnostics;
namespace Sandbox
{
public class Program
{
public static void Main(String[] args)
{
DynamicMethod method = new DynamicMethod("TestMethod",
typeof(Int32), new Type[] { typeof(object), typeof(Int32),
typeof(Int32) }, true);
var il = method.GetILGenerator();
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Ldarg_2);
il.Emit(OpCodes.Add);
il.Emit(OpCodes.Ret);
Func<Int32, Int32, Int32> f1 =
(Func<Int32, Int32, Int32>)method.CreateDelegate(
typeof(Func<Int32, Int32, Int32>), null);
Func<Int32, Int32, Int32> f2 = (Int32 a, Int32 b) => a + b;
Func<Int32, Int32, Int32> f3 = Sum;
Expression<Func<Int32, Int32, Int32>> f4x = (a, b) => a + b;
Func<Int32, Int32, Int32> f4 = f4x.Compile();
for (Int32 pass = 1; pass <= 2; pass++)
{
// Pass 1 just runs all the code without writing out anything
// to avoid JIT overhead influencing the results
Time(f1, "DynamicMethod", pass);
Time(f2, "Lambda", pass);
Time(f3, "Method", pass);
Time(f4, "Expression", pass);
}
}
private static void Time(Func<Int32, Int32, Int32> fn,
String name, Int32 pass)
{
Stopwatch sw = new Stopwatch();
sw.Start();
for (Int32 index = 0; index <= 100000000; index++)
{
Int32 result = fn(index, 1);
}
sw.Stop();
if (pass == 2)
Console.WriteLine(name + ": " + sw.ElapsedMilliseconds + " ms");
}
private static Int32 Sum(Int32 a, Int32 b)
{
return a + b;
}
}
}
Вот'ые runtimes на моей системе:
DynamicMethod: 312 ms
Lambda: 417 ms
Method: 417 ms
Expression: 312 ms
ОБНОВЛЕНО ДЛЯ ДОБАВЛЕНИЯ:
Я попробовал запустить этот код на моей новой системе, которая представляет собой Core i7 920 под управлением Windows 7 x64 с установленным .NET 4 beta 2 (mscoree.dll ver. 4.0.30902), и результаты, ну, переменные.
csc 3.5, /platform:x86, runtime v2.0.50727 (via .config)
Run #1
DynamicMethod: 214 ms
Lambda: 571 ms
Method: 570 ms
Expression: 249 ms
Run #2
DynamicMethod: 463 ms
Lambda: 392 ms
Method: 392 ms
Expression: 463 ms
Run #3
DynamicMethod: 463 ms
Lambda: 570 ms
Method: 570 ms
Expression: 463 ms
Возможно, это Intel SpeedStep влияет на результаты, или, возможно, Turbo Boost. В любом случае, это очень раздражает.
csc 3.5, /platform:x64, runtime v2.0.50727 (via .config)
DynamicMethod: 428 ms
Lambda: 392 ms
Method: 392 ms
Expression: 428 ms
csc 3.5, /platform:x64, runtime v4
DynamicMethod: 428 ms
Lambda: 356 ms
Method: 356 ms
Expression: 428 ms
csc 4, /platform:x64, runtime v4
DynamicMethod: 428 ms
Lambda: 356 ms
Method: 356 ms
Expression: 428 ms
csc 4, /platform:x86, runtime v4
DynamicMethod: 463 ms
Lambda: 570 ms
Method: 570 ms
Expression: 463 ms
csc 3.5, /platform:x86, runtime v4
DynamicMethod: 214 ms
Lambda: 570 ms
Method: 571 ms
Expression: 249 ms
Многие из этих результатов будут случайностью, независимо от того, что является причиной случайных ускорений в сценарии C# 3.5 / runtime v2.0. Мне'придется перезагрузиться, чтобы посмотреть, SpeedStep или Turbo Boost ответственны за эти эффекты.