.

Jõudlus objektide omaduste kopeerimisel

07.02.2010  |  Gunnar

Keeruline jutt 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.

public static IList<PropertyMap> GetMatchingProperties(Type sourceType, Type targetType)
{
    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.

public static string GetClassName(Type sourceType,
    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.

public static void CopyProperties(object source, object target)
{
    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.

private static Dictionary<string, PropertyMap[]> _maps =
    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.

private static readonly Dictionary<string, Type> Comp = _
    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.

delegate void CopyPublicPropertiesDelegate<T, TU>
             (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.

Objektide omaduste kopeerimine: meetodite võrdlus

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.

2 kommentaari sissekandele “Jõudlus objektide omaduste kopeerimisel”

  1. Robert Hudjakov

    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.

  2. Gunnar

    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.

Kommenteeri

sulge
Saada link e-postiga

© DT 2006-2008 | Creative Commons Attribution-Noncommercial 3.0 License | WordPress