Jõudlus objektide omaduste kopeerimisel
07.02.2010 | Gunnar
Tegelesin ühe lahendusega, kus tuli kopeerida objektide omadusi ühelt objektilt teisele. Selle käigus sai läbi tehtud päris juhitav jada katseid ning mõõdetud erinevate meetodite jõudlust. Saavutasin lõpptulemuseks võrreldes algsega päris arvestatava jõudluse kasvu. Järgnevalt väike ülevaade tehtust.
Selle töö teeb tegelikult ära ka AutoMapper, kuid minu juhul jäi AutoMapper veidikene liiga jäigaks ja puiseks – vaja oli üldisemat lähenemist. Järgnevates peatükkides on vaja kahte meetodi. Esimene neist koostab omaduste kaardi.
{
var sourceProperties = sourceType.GetProperties();
var targetProperties = targetType.GetProperties();
var properties = (from s in sourceProperties
from t in targetProperties
where s.Name == t.Name &&
s.CanRead &&
t.CanWrite &&
s.PropertyType.IsPublic &&
t.PropertyType.IsPublic
s.PropertyType == t.PropertyType
select new PropertyMap
{
SourceProperty = s,
TargetProperty = t
}).ToList();
return properties;
}
public class PropertyMap
{
public PropertyInfo SourceProperty { get; set; }
public PropertyInfo TargetProperty { get; set; }
}
Teine meetod tagastab antud tüüpide põhjal unikaalse nimetuse puhvri võtmete jaoks.
Type targetType)
{
var className = "Copy_";
className += sourceType.FullName.Replace(".", "_");
className += "_";
className += targetType.FullName.Replace(".", "_");
return className;
}
Selgituseks veel nii palju, et omaduste kaardi loomise kood, mille siin andsin, on suhteliselt lihtne ja primitiivne. See, mis lõpuks kokku tuli, on oluliselt mahukam ja keerukam. Aga siintoodu sobib alustamiseks suurepäraselt.
Mõõtmised
Mõõtmised viisin läbi selliselt. On kaks objekti, millel on nii kattuvaid kui ka mittekattuvaid omadusi. Mõõtsin iga meetodi keskmist ajakulu ühe kopeerimise kohta. Kopeerimisi viisin ühe meetodi kohta läbi 100.000 tükki.
Optimeerimata versioon
Esimene versioon kopeerimise koodist oli optimeeritama, sest esimene asi on lahendus tööle panna ja alles seejärel tegeleda iluasjadega. Optimeerimata versioon – nagu oodata võib – ei ole just eeskujuliku jõudlusega, kuigi kasutajatele märgatav pudelikael see minu juhul ei oleks.
{
var sourceType = source.GetType();
var targetType = target.GetType();
var propMap = GetMatchingProperties(sourceType, targetType);
for(var i=0; i<propmap .Count; i++)
{
var prop = propMap[i];
var sourceValue = prop.SourceProperty.GetValue(source,
null);
prop.TargetProperty.SetValue(target, sourceValue, null);
}
}
Tulemus: 0,0403 millisekundit operatsiooni kohta.
Omaduste seoste puhverdamine
Esimese optimeerimisena viisin läbi selle, et korra tüüpide jaoks leitud omaduste kaart puhverdatakse ära. Sel juhul on vaja see ainult korra luua kahe konkreetse tüübi jaoks ja kõikidel edasistel juhtudel saame kasutada kaarti otse puhvrist. Enne mõõtmist tuleb kutsuda AddPropertyMap() meetodi, et kaart puhvrisse saaks lisatud.
new Dictionary<string, PropertyMap[]>();
public static void AddPropertyMap<T, TU>()
{
var props = GetMatchingProperties(typeof (T), typeof (TU));
var className = GetClassName(typeof(T), typeof(TU));
_maps.Add(className, props.ToArray());
}
public static void CopyMatchingCachedProperties(object source,
object target)
{
var className = GetClassName(source.GetType(),
target.GetType());
var propMap = _maps[className];
for (var i = 0; i <propMap.Length; i++)
{
var prop = propMap[i];
var sourceValue = prop.SourceProperty.GetValue(source,
null);
prop.TargetProperty.SetValue(target, sourceValue, null);
}
}
Tulemus: 0,0247 millisekundit operatsiooni kohta. See on 1,6 korda parem tulemus kui optimeerimata koodil.
Dünaamiline C# kood
Järgmiseks proovisin järgi dünaamiliselt genereeritud koodi. See tähendab siis seda, et iga tüüpide paari jaoks luuakse C# meetod, mis omaduste kopeerimise läbi viib. Meetod kompileeritakse kokku ja puhverdatakse ära. Enne mõõtmist tuleb kutsuda GenerateCopyClass() meetodi.
new Dictionary<string, Type>();
public static void CopyWithDom<T, TU>(T source, TU target)
{
var className = GetClassName(typeof (T), typeof (TU));
var flags = BindingFlags.Public | BindingFlags.Static |
BindingFlags.InvokeMethod;
var args = new object[] {source, target};
Comp[className].InvokeMember("CopyProps", flags, null,
null, args);
}
public static void GenerateCopyClass<T, TU>()
{
var sourceType = typeof(T);
var targetType = typeof (TU);
var className = GetClassName(typeof (T), typeof (TU));
if (Comp.ContainsKey(className))
return;
var builder = new StringBuilder();
builder.Append("namespace Copy {\r\n");
builder.Append(" public class ");
builder.Append(className);
builder.Append(" {\r\n");
builder.Append(" public static void CopyProps(");
builder.Append(sourceType.FullName);
builder.Append(" source, ");
builder.Append(targetType.FullName);
builder.Append(" target) {\r\n");
var map = GetMatchingProperties(sourceType, targetType);
foreach (var item in map)
{
builder.Append(" target.");
builder.Append(item.TargetProperty.Name);
builder.Append(" = ");
builder.Append("source.");
builder.Append(item.SourceProperty.Name);
builder.Append(";\r\n");
}
builder.Append(" }\r\n }\r\n}");
// Write out method body
Debug.WriteLine(builder.ToString());
var myCodeProvider = new CSharpCodeProvider();
var myCodeCompiler = myCodeProvider.CreateCompiler();
var myCompilerParameters = new CompilerParameters();
myCompilerParameters.ReferencedAssemblies.Add(
typeof (LinqReflectionPerf).Assembly.Location
);
myCompilerParameters.GenerateInMemory = true;
var results = myCodeCompiler.CompileAssemblyFromSource
(myCompilerParameters, builder.ToString());
// Compiler output
foreach (var line in results.Output)
Debug.WriteLine(line);
var copierType = results.CompiledAssembly.GetType(
"Copy." + className);
Comp.Add(className, copierType);
}
Tulemus: 0,0055 millisekundit operatsiooni kohta, mis on eelmisest 7,3 korda kiirem.
Dünaamilised IL instruktsioonid
Eelmine versioon koodist jäi siiski kohmakas ja suur. .Net Framework pakub aga välja midagi oluliselt pisemat – Lightweight Code Generation (LCG). See tähendab seda, et me saame luua dünaamilise koodi vahekeele (Intermediate Language – IL) opkoodidest ja instruktsioonidest. Enne mõõtmisi tuleb kutsuda GenerateCopyDelegate() meetodi, et puhvris oleks delegaat olemas.
(T source, TU target);
static Dictionary<string , object> _del =
new Dictionary</string><string , object>();
static void GenerateCopyDelegate<T, TU>()
{
var className = GetClassName(typeof(T), typeof(TU));
var args = new[] {typeof(T), typeof(TU)};
var mod = typeof(Program).Module;
var dm = new DynamicMethod(className, null, args, mod);
var il = dm.GetILGenerator();
var maps = _maps[className];
foreach (var map in maps)
{
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Ldarg_0);
il.EmitCall(OpCodes.Callvirt,
map.SourceProperty.GetGetMethod(), null);
il.EmitCall(OpCodes.Callvirt,
map.TargetProperty.GetSetMethod(), null);
}
il.Emit(OpCodes.Ret);
var del = (CopyPublicPropertiesDelegate<T,TU>)
dm.CreateDelegate(
typeof(CopyPublicPropertiesDelegate<T,TU>)
);
_del.Add(className, del);
}
static void CopyUsingLcg<T, TU>(T source, TU target)
{
var sourceType = source.GetType();
var targetType = target.GetType();
var className = GetClassName(sourceType, targetType);
var del = (CopyPublicPropertiesDelegate<T, TU>)
_del[className];
del.Invoke(source, target);
}
Tulemus: 0,0018 millisekundit operatsiooni kohta, mis on võrreldes eelmisega 3 korda kiirem.
Kokkuvõte
Ma panen nüüd tulemused tabelisse ja lisan juurde ka tulpdiagrammi, mis annab aimu sellest, kui tubli üks või teine versioon mu kirjatööst oli. Tabeli kolmas veerg on kiiruse erinevus võrreldes eelmise optimeerimisega, neljas veerg on kiiruse erinevus võrreldes esimese optimeerimata versiooniga.
Kuigi reflektsiooni abil saame me kirjutada oluliselt ilusamat koodi kui seda võimaldavad dünaamilised koodi genereerimise meetodid, on dünaamiliselt genereeritud kood oluliselt kiirem, kuid seda on keerukam siluda ja parandada.
Kui võrrelda kahte dünaamiliselt koodi genereerivat meetodi, siis siingi on analoogsed käärid sees. Dünaamiliselt genereeritud C# kood on arvatavasti suuremale audientsile tuttav kui vahekeele instruktsioonidest koosnev kood. Kuid jõudluse vahe on jällegi päris suur.
Ma jäin lõpptulemusega päris hästi rahule, sest jõudluse kasv on 22-kordne. Antud juhul võib rahulikult jääda LCG juurde, sest LCG kood on siin lihtne ja sellest aru saamiseks ei ole vaja ka võhikul kuigi palju pingutada.

08.02.2010 kell 17:39
Huvitav lugemine, eriti see viimane osa.
Koopiaga võib siiski probleeme tulla, kuna kopeeritakse ainult byValue väljad. ByReference väljad jäävad ikkagi samale objektile viitama.
08.02.2010 kell 21:57
Mul endal oligi just vaja primitiivsete tüüpide pluss kindlat tüüpi objektide ületoomist. Ehk siis shallow copy. Kui tahta byref omadused ka üle tuua koopiatena, siis tuleb deep copy appi võtta. Kõige lihtsam lahendus oleks binary serialiseerimise ja deserialiseerimine. Nii saab deep copy kõige kiiremini ja lihtsamini tehtud.