Почему быстрый член не кажется здесь быстрее, чем отражение?
(это через вопрос в твиттере, переписан здесь с разрешения)
Я пытаюсь быстро проверить некоторые объекты (для проверки на нулевые значения), и я подумал, что FastMember мог бы помочь - однако в тестах, показанных ниже, я вижу гораздо худшую производительность. Я делаю что-то неправильно?
public class ValidateStuffTests
{
[Test]
public void Benchmark_speed()
{
var player = CreateValidStuffToTest();
_stopwatch.Start();
CharacterActions.IsValid(player);
_stopwatch.Stop();
Console.WriteLine(_stopwatch.Elapsed);
Assert.Less(_stopwatch.ElapsedMilliseconds, 10, string.Format("IsValid took {0} mileseconds", _stopwatch.Elapsed));
}
[Test]
public void When_Benchmark_fastMember()
{
var player = CreateValidStuffToTest();
_stopwatch.Start();
CharacterActions.IsValidFastMember(player);
_stopwatch.Stop();
Assert.Less(_stopwatch.ElapsedMilliseconds, 10, string.Format("IsValid took {0} mileseconds", _stopwatch.Elapsed));
}
}
public static class ValidateStuff
{
public static bool IsValid<T>(T actions)
{
var propertyInfos = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
foreach (var property in propertyInfos)
{
if (property.GetValue(actions, null) == null)
return false;
}
return true;
}
public static bool IsValidFastMember<T>(T actions)
{
var typeAccessor = TypeAccessor.Create(typeof(T));
foreach (var property in typeAccessor.GetMembers())
{
if (typeAccessor[actions, property.Name] == null)
return false;
}
return true;
}
}
1 ответ
Основная проблема заключается в том, что вы включаете единовременную стоимость метапрограммирования во время. FastMember несет некоторые накладные расходы, обрабатывая типы и генерируя подходящий IL, и, конечно: все уровни генерации IL тогда нуждаются в JIT. Так что да, используется один раз: FastMember может показаться дороже. И действительно, вы бы не использовали что-то вроде FastMember, если бы собирались выполнить эту работу только один раз (рефлексия была бы в порядке). Хитрость заключается в том, чтобы сделать все один раз (в обоих тестах) вне времени, чтобы производительность первого запуска не влияла на результаты. А в производительности вам обычно нужно запускать вещи намного более одного раза. Вот моя установка:
const int CYCLES = 500000;
[Test]
public void Benchmark_speed()
{
var player = CreateValidStuffToTest();
ValidateStuff.IsValid(player); // warm up
var _stopwatch = Stopwatch.StartNew();
for (int i = 0; i < CYCLES; i++)
{
ValidateStuff.IsValid(player);
}
_stopwatch.Stop();
Console.WriteLine(_stopwatch.Elapsed);
Console.WriteLine("Reflection: {0}ms", _stopwatch.ElapsedMilliseconds);
}
[Test]
public void When_Benchmark_fastMember()
{
var player = CreateValidStuffToTest();
ValidateStuff.IsValidFastMember(player); // warm up
var _stopwatch = Stopwatch.StartNew();
for (int i = 0; i < CYCLES; i++)
{
ValidateStuff.IsValidFastMember(player);
}
_stopwatch.Stop();
Console.WriteLine("FastMember: {0}ms", _stopwatch.ElapsedMilliseconds);
}
Что показывает fast-member немного быстрее, но не так сильно, как хотелось бы - 600 мс (отражение) против 200 мс (FastMember); вполне возможно, что 1.0.11 слишком сильно изменяет положение в сторону больших классов (использование 1.0.10 занимает всего 130 мс). Я мог бы выпустить 1.0.12, которая использует различные стратегии для небольших и больших классов для компенсации.
Тем не мение! В вашем случае, если все, что вы хотите проверить, это null
Я бы на самом деле серьезно подумал об оптимизации этого случая через IL.
Например, следующее займет всего 45 мсек для того же теста:
[Test]
public void When_Benchmark_Metaprogramming()
{
var player = CreateValidStuffToTest();
Console.WriteLine(ValidateStuff.IsValidMetaprogramming(player)); // warm up
var _stopwatch = Stopwatch.StartNew();
for (int i = 0; i < CYCLES; i++)
{
ValidateStuff.IsValidMetaprogramming(player);
}
_stopwatch.Stop();
Console.WriteLine("Metaprogramming: {0}ms", _stopwatch.ElapsedMilliseconds);
}
с помощью:
public static bool IsValidMetaprogramming<T>(T actions)
{
return !NullTester<T>.HasNulls(actions);
}
и немного сумасшедший метапрограммирующий код, который делает тест для любого данного T
все в одном месте:
static class NullTester<T>
{
public static readonly Func<T, bool> HasNulls;
static NullTester()
{
if (typeof(T).IsValueType)
throw new InvalidOperationException("Exercise for reader: value-type T");
var props = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public);
var dm = new DynamicMethod("HasNulls", typeof(bool), new[] { typeof(T) });
var il = dm.GetILGenerator();
Label next, foundNull;
foundNull = il.DefineLabel();
Dictionary<Type, LocalBuilder> locals = new Dictionary<Type, LocalBuilder>();
foreach (var prop in props)
{
if (!prop.CanRead) continue;
var getter = prop.GetGetMethod(false);
if (getter == null) continue;
if (prop.PropertyType.IsValueType
&& Nullable.GetUnderlyingType(prop.PropertyType) == null)
{ // non-nullable value-type; can never be null
continue;
}
next = il.DefineLabel();
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Callvirt, getter);
if (prop.PropertyType.IsValueType)
{
// have a nullable-value-type on the stack; need
// to call HasValue, which means we need it as a local
LocalBuilder local;
if (!locals.TryGetValue(prop.PropertyType, out local))
{
local = il.DeclareLocal(prop.PropertyType);
locals.Add(prop.PropertyType, local);
}
il.Emit(OpCodes.Stloc, local);
il.Emit(OpCodes.Ldloca, local);
il.Emit(OpCodes.Call,
prop.PropertyType.GetProperty("HasValue").GetGetMethod(false));
il.Emit(OpCodes.Brtrue_S, next);
}
else
{
// is a class; fine if non-zero
il.Emit(OpCodes.Brtrue_S, next);
}
il.Emit(OpCodes.Br, foundNull);
il.MarkLabel(next);
}
il.Emit(OpCodes.Ldc_I4_0);
il.Emit(OpCodes.Ret);
il.MarkLabel(foundNull);
il.Emit(OpCodes.Ldc_I4_1);
il.Emit(OpCodes.Ret);
HasNulls = (Func<T, bool>)dm.CreateDelegate(typeof(Func<T, bool>));
}
}