Friday, October 10, 2008

Самый быстрый способ создания объектов в .NET 3.5

В этой заметке я хочу рассмотреть способы создания объектов для использования их в фабрике объектов и выяснить, какой из способов самый быстрый. Для многочисленных попыток создания будет использован подопытный тип:

public class MyType
{
public MyType(){}

public MyType(string s){}

public MyType(int i, string s){}

public MyType(int i){}

public MyType(object obj, string s){}
}

Вызова натурального конструктора.

var obj= new MyType(10000);

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

Вызов метода  CreateInstance у класса Activator.

var obj = (MyType)Activator.CreateInstance(typeof(MyType), 10000);

Этот способ оказался самым медленным из всех рассмотренных (об этом, я думаю, многие догадывались). Его отставание очень значительное относительно других способов (результаты см. ниже). Однако, стоит отметить, что данный способ не требует предварительных затрат, как следующие, и может быть использован в универсальной фабрике объектов, хотя я этого делать не советую, по причине его медлительности.

Вызов метода Invoke у объекта класса ConstructorInfo.

//предварительные затраты, результат должен кэшироваться
ConstructorInfo constructorInfo = typeof(MyType).GetConstructor(new[] { typeof(int) });
//вызов конструктора
var obj = (MyType)constructorInfo.Invoke(new object[] { 10000 });

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

Вызов делегата на динамический метод.

//предварительные затраты, результат должен кэшироваться
var dm = new DynamicMethod("MyCtor", typeof(MyType), new[] { typeof(int) }, typeof(MyType).Module, true);
ILGenerator ilgen = dm.GetILGenerator();
ilgen.Emit(OpCodes.Ldarg_0);
ilgen.Emit(OpCodes.Newobj, type.GetConstructor(new[] { typeof(int) }));
ilgen.Emit(OpCodes.Ret);
var ctor = (Func<int, MyType>)dm.CreateDelegate(typeof(Func<int, MyType>));
//вызов конструктора
var obj = ctor(10000);

В этом случае с помощью класса DynamicMethod мы динамически создаем статический метод "MyCtor" у нашего класса MyType, единственной задачей которого является создание экземпляра класса MyType через нужный конструктор. Далее мы получаем, делегат на сгенерированный метод и можем его вызывать. Этот метод достаточно быстр: не велики предварительные затраты и время вызова приблизительно в 2-3 раза больше, чем время вызова натурального конструктора, что на много быстрее, в сравнении с ранее рассмотренными способами. Минусом способа является его сложность, немногие разработчики смогут понять этот код.

Вызов динамически скомпилированного лямбда-выражения.

//предварительные затраты, результат должен кэшироваться
var param = Expression.Parameter(typeof(int), "param");
var constructorCall = Expression.New(typeof(MyType).GetConstructor(new[] { typeof(int) }), param);
var constructorLambda = Expression.Lambda(constructorCall, param);
var ctor = (Func<int, MyType>)constructorLambdaTest.Compile();
//вызов конструктора
var obj = ctor(10000);

Основной идеей способа является динамическое создание лямбды вида:

param => new MyType(param)

что вообщем и делает приведенный выше код, и дальнейшее ее использование. Время создания объекта у этого метода совсем на немного отличается от времени создания с помощью натурального конструктора. Таким образом, данный способ - чемпион по времени создания объекта среди способов суррогатного фабрикования. Так же, имеется мнение, что api для создания выражений все же проще чем легкая кодогенерация (Reflection.Emit) из предыдущего способа.  Есть, конечно же,  и ложка дегтя, время, затрачиваемое на подготовку лямбда-выражения в среднем в 9-10 раз больше, чем у предыдущего способа (у которого оно было самым большим из перечисленных).

Какую роль играет время затрачиваемое на подготовку конструктора?

Если в вашем приложении создаются в основном уникальные типы с уникальными конструкторами(т.е. вероятность повторного создания экземпляра класса через уже использованный конструктор низка) время вызова конструктора вырастает ровно на столько, сколько нужно для предварительной генерации конструктора:

// время генерации + время вызова
var ctorForT1 = ObjectFactory.Create<int, TContract>(typeof(T1));
var objOfT1 = ctorForT1(100);
// время генерации + время вызова
var ctor2ForT1 = ObjectFactory.Create<int, string, TContract>(typeof(T1));
objOfT1 = ctor2ForT1(100,"test");
// время генерации + время вызова
var ctorForT2 = ObjectFactory.Create<int, string, TContract>(typeof(T2));
var objOfT2 = ctorForT2(100,"test");

Этот случай далек от реального, но имеет место быть, поэтому в этом случае лучше выбрать 4-ый способ (динамический метод). Если же вероятность повторного создания экземпляра класса через уже использованный конструктор высока(обычно именно так и бывает):

// время генерации + время вызова
var ctorForT1 = ObjectFactory.Create<int, string, TContract>(typeof(T1));
var objOfT1 = ctorForT1(100,"test");

for (int i = 0; i < 10000; i++)
{
// время изъятия конструктора из кэша(пренебрежительно мало и одинаково для всех способов) + время вызова
var obj = ObjectFactory.Create<int, string, TContract>(typeof(T1))(i, "test" + i)
}

то следует выбрать 5-й способ (лямбда-выражение).
Существует еще несколько способов создания объектов в .NET. Я привел не все способы создания объектов в этой статье намеренно, выбрав лишь, по-моему мнению, самые подходящие для использования в фабрике объектов.

Результаты измерений.

Конфигурация:

Intel Core™ Duo Processor T2000 Sequence
2048 Mb of RAM
Windows XP SP3 .NET 3.5 SP1

Время на предварительные затраты(10000 итераций) - Время на создание экземпляра класса(1000000 итераций).

Конструктор с параметрами

Вызова натурального конструктора
00:00:00 - 00:00:00.0242500

Вызов метода CreateInstance у класса Activator
00:00:00 - 00:00:06.5156250

Вызов метода Invoke у объекта класса ConstructorInfo
00:00:00.0156250 - 00:00:02.3593750

Вызов делегата на динамический метод
00:00:00.3281250 - 00:00:00.0615500

Вызов динамически скомпилированного лямбда-выражения
00:00:02.7968750 - 00:00:00.0312500

Конструктор без параметров

Вызова натурального конструктора
00:00:00 - 00:00:00.0222500

Вызов метода CreateInstance у класса Activator
00:00:00 - 00:00:00.2812500

Вызов метода Invoke у объекта класса ConstructorInfo
00:00:00.0076250 - 00:00:01.7812500

Вызов делегата на динамический метод
00:00:00.2968750 - 00:00:00.0625000

Вызов динамически скомпилированного лямбда-выражения
00:00:02.5468750 - 00:00:00.0312500

Код тестового примера:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;

namespace ObjectFactoryTest2
{

public class MyType
{
public MyType() { }

public MyType(string s) { }

public MyType(int i, string s) { }

public MyType(int i) { }

public MyType(object obj, string s) { }
}

public class ObjectFactoryTest
{
delegate void Approach(out DateTime prepareStart, out DateTime executeStart, out DateTime finish);
static int LOOP=1;
static int PREPARE_LOOP=1;
static readonly Type type = typeof(MyType);

public static void Main(string[] args)
{
const int TEST_LOOP = 10;
LOOP = 1000000;
PREPARE_LOOP = 10000;

for (int i = 0; i < TEST_LOOP; i++)
{
Console.WriteLine();
Console.WriteLine("--------Ctor with Parameters #" + (i+1) + "--------");
Console.WriteLine();
Console.WriteLine("ParameterAndCtor");
Measure(ParameterAndCtor);
Console.WriteLine();
Console.WriteLine("ParameterAndActivator");
Measure(ParameterAndActivator);
Console.WriteLine();
Console.WriteLine("ParameterAndCtorInfo");
Measure(ParameterAndCtorInfo);
Console.WriteLine();
Console.WriteLine("ParameterAndDynamicMethodAndDelegate");
Measure(ParameterAndDynamicMethod);
Console.WriteLine();
Console.WriteLine("ParameterAndExpressionAndDelegate");
Measure(ParameterAndExpressionAndDelegate);
}

Console.ReadLine();

for (int i = 0; i < TEST_LOOP; i++)
{
Console.WriteLine();
Console.WriteLine("--------Parameterless Ctor #" + (i+1) + "--------");
Console.WriteLine();
Console.WriteLine("ParameterlessAndCtor");
Measure(ParameterlessAndCtor);
Console.WriteLine();
Console.WriteLine("ParameterlessAndActivator");
Measure(ParameterlessAndActivator);
Console.WriteLine();
Console.WriteLine("ParameterlessAndCtorInfo");
Measure(ParameterlessAndCtorInfo);
Console.WriteLine();
Console.WriteLine("ParameterlessAndDynamicMethod");
Measure(ParameterlessAndDynamicMethod);
Console.WriteLine();
Console.WriteLine("ParameterlessAndExpressionAndDelegate");
Measure(ParameterlessAndExpressionAndDelegate);
}

Console.ReadLine();

}

static void Measure(Approach approach)
{
DateTime prepareStart, executeStart, finish;
approach(out prepareStart, out executeStart, out finish);
Console.WriteLine("{0} - {1}", executeStart - prepareStart, finish - executeStart);
GC.Collect(2);
GC.WaitForPendingFinalizers();
}

private static void ParameterAndCtor(out DateTime prepareStart, out DateTime executeStart, out DateTime finish)
{
prepareStart = DateTime.Now;
executeStart = prepareStart;
for (int i = 0; i < LOOP; i++)
new MyType(i);
finish = DateTime.Now;
}

private static void ParameterAndActivator(out DateTime prepareStart, out DateTime executeStart, out DateTime finish)
{
prepareStart = DateTime.Now;
executeStart = prepareStart;
for (int i = 0; i < LOOP; i++)
Activator.CreateInstance(type, i);
finish = DateTime.Now;
}

private static void ParameterAndCtorInfo(out DateTime prepareStart, out DateTime executeStart, out DateTime finish)
{
ConstructorInfo ci = null;
prepareStart = DateTime.Now;
for (int i = 0; i < PREPARE_LOOP; i++)
ci = type.GetConstructor(new[] { typeof(int) });

executeStart = DateTime.Now;
for (int i = 0; i < LOOP; i++)
ci.Invoke(new object[] { i });
finish = DateTime.Now;
}

private static void ParameterAndDynamicMethod(out DateTime prepareStart, out DateTime executeStart, out DateTime finish)
{
Func<int, MyType> dlTest = null;
prepareStart = DateTime.Now;
for (int i = 0; i < PREPARE_LOOP; i++)
{
var dmTest = new DynamicMethod("MyCtor", type, new[] { typeof(int) }, type.Module, true);
ILGenerator ilgenTest = dmTest.GetILGenerator();
ilgenTest.Emit(OpCodes.Ldarg_0);
ilgenTest.Emit(OpCodes.Newobj, type.GetConstructor(new[] { typeof(int) }));
ilgenTest.Emit(OpCodes.Ret);
dlTest = (Func<int, MyType>)dmTest.CreateDelegate(typeof(Func<int, MyType>));
}
executeStart = DateTime.Now;
for (int i = 0; i < LOOP; i++)
dlTest(i);
finish = DateTime.Now;
}

private static void ParameterAndExpressionAndDelegate(out DateTime prepareStart, out DateTime executeStart, out DateTime finish)
{
Func<int, MyType> dlTest = null;
prepareStart = DateTime.Now;
var paramTest = Expression.Parameter(typeof(Int32), "n");
for (int i = 0; i < PREPARE_LOOP; i++)
{
var constructorCallTest = Expression.New(type.GetConstructor(new[] { typeof(int) }), paramTest);
var constructorLambdaTest = Expression.Lambda(constructorCallTest, paramTest);
dlTest = (Func<int, MyType>)constructorLambdaTest.Compile();
}
executeStart = DateTime.Now;
for (int i = 0; i < LOOP; i++)
dlTest(i);
finish = DateTime.Now;
}

private static void ParameterlessAndCtor(out DateTime prepareStart, out DateTime executeStart, out DateTime finish)
{
prepareStart = DateTime.Now;
executeStart = prepareStart;
for (int i = 0; i < LOOP; i++)
new MyType();
finish = DateTime.Now;
}

private static void ParameterlessAndActivator(out DateTime prepareStart, out DateTime executeStart, out DateTime finish)
{
prepareStart = DateTime.Now;
executeStart = prepareStart;
for (int i = 0; i < LOOP; i++)
Activator.CreateInstance(type);
finish = DateTime.Now;
}

private static void ParameterlessAndCtorInfo(out DateTime prepareStart, out DateTime executeStart, out DateTime finish)
{
ConstructorInfo ci = null;
prepareStart = DateTime.Now;
for (int i = 0; i < PREPARE_LOOP; i++)
ci = type.GetConstructor(Type.EmptyTypes);

executeStart = DateTime.Now;
for (int i = 0; i < LOOP; i++)
ci.Invoke(null);
finish = DateTime.Now;
}

private static void ParameterlessAndDynamicMethod(out DateTime prepareStart, out DateTime executeStart, out DateTime finish)
{
Func<MyType> dlTest = null;
prepareStart = DateTime.Now;
for (int i = 0; i < PREPARE_LOOP; i++)
{
var dmTest = new DynamicMethod("MyCtor", type, Type.EmptyTypes, type.Module, true);
ILGenerator ilgenTest = dmTest.GetILGenerator();
ilgenTest.Emit(OpCodes.Newobj, type.GetConstructor(Type.EmptyTypes));
ilgenTest.Emit(OpCodes.Ret);
dlTest = (Func<MyType>)dmTest.CreateDelegate(typeof(Func<MyType>));
}
executeStart = DateTime.Now;
for (int i = 0; i < LOOP; i++)
dlTest();
finish = DateTime.Now;
}

private static void ParameterlessAndExpressionAndDelegate(out DateTime prepareStart, out DateTime executeStart, out DateTime finish)
{
Func<MyType> dlTest = null;
prepareStart = DateTime.Now;
for (int i = 0; i < PREPARE_LOOP; i++)
{
var constructorCallTest = Expression.New(type.GetConstructor(Type.EmptyTypes));
var constructorLambdaTest = Expression.Lambda(constructorCallTest);
dlTest = (Func<MyType>)constructorLambdaTest.Compile();
}
executeStart = DateTime.Now;
for (int i = 0; i < LOOP; i++)
dlTest();
finish = DateTime.Now;
}

}
}









3 comments:

  1. Как насчет реализации паттерна Prototype? (ну, т.е. сделать клонирующую фабрику). Мне кажется, ее инициализация может быть не самой быстрой, но далее работать она должна достаточно шустро.

    ReplyDelete
  2. Для того что бы клонировать, необходим прототип, откуда его взять?

    ReplyDelete
  3. Да хоть вызовом натурального конструктора. А дальше можно держать пул таких объектов.
    Хотя да, это уже некоторое отклонение от темы

    ReplyDelete