2008-12-15 5 views
15

Wenn Sie Code wie den folgenden haben:Warum gibt der C# -Compiler Activator.CreateInstance aus, wenn er new mit einem generischen Typ mit einer neuen() Einschränkung aufruft?

static T GenericConstruct<T>() where T : new() 
{ 
    return new T(); 
} 

Der C# -Compiler besteht darauf einen Anruf zu Activator.CreateInstance auf emittieren, die wesentlich langsamer als ein nativer Konstruktor ist.

Ich habe die folgende Abhilfe:

public static class ParameterlessConstructor<T> 
    where T : new() 
{ 
    public static T Create() 
    { 
     return _func(); 
    } 

    private static Func<T> CreateFunc() 
    { 
     return Expression.Lambda<Func<T>>(Expression.New(typeof(T))).Compile(); 
    } 

    private static Func<T> _func = CreateFunc(); 
} 

// Example: 
// Foo foo = ParameterlessConstructor<Foo>.Create(); 

Aber es macht keinen Sinn für mich machen, warum diese Abhilfe erforderlich sein sollte.

+0

Ich bemerkte das gleiche ... aber ich weiß nicht warum. –

+0

Ich benutze Snippet Compiler und der Compiler wirft keinen Fehler. Außerdem wird der Konstruktor aufgerufen, wenn new T() aufgerufen wird. – shahkalpesh

+1

@shahkalpesh: Niemand hat gesagt, dass ein Fehler auftreten würde. Der Punkt ist, dass Activator.CreateInstance langsamer ist als das Delegate-Formular. –

Antwort

9

I Verdacht es ist ein JITting-Problem. Zur Zeit verwendet der JIT den gleichen generierten Code für alle Argumente des Referenztyps - der Vtable einer List<string> verweist also auf denselben Maschinencode wie der von List<Stream>. Das würde nicht funktionieren, wenn jeder new T() Aufruf in dem JITted-Code aufgelöst werden musste.

Nur eine Vermutung, aber es macht eine bestimmte Menge an Sinn.

Ein interessanter kleiner Punkt: in weder Fall wird der parameterlose Konstruktor eines Werttyps aufgerufen, wenn es einen gibt (was verschwindend selten ist). Einzelheiten finden Sie unter my recent blog post. Ich weiß nicht, ob man es in Ausdrucksbäumen zwingen kann.

8

Dies ist wahrscheinlich, weil nicht klar ist, ob T ein Werttyp oder Referenztyp ist. Die Erstellung dieser beiden Typen in einem nicht-generischen Szenario erzeugt sehr unterschiedliche IL. Angesichts dieser Mehrdeutigkeit ist C# gezwungen, eine universelle Methode der Typentstehung zu verwenden. Activator.CreateInstance passt zur Rechnung.

Schnelles Experimentieren scheint diese Idee zu unterstützen. Wenn Sie den folgenden Code eingeben und die IL untersuchen, wird initobj anstelle von CreateInstance verwendet, da es keine Mehrdeutigkeit für den Typ gibt.

Umschalten auf eine Klasse und neue() Einschränkung obwohl noch ein Activator.CreateInstance erzwingt.

+5

Ich vermute, die unmittelbare Folgefrage wäre "Warum gibt es keinen passenden IL-Befehl zum Erstellen einer Instanz eines generischen Typs mit einer geeigneten Einschränkung?" Es ist nicht so, als hätten sie das nicht von Anfang an einbauen können. –

+0

Einverstanden, es scheint wirklich so, als hätten sie eine API anstelle einer IL-Anweisung implementiert. Der Kommentar auf der MSDN-Dokumentseite für Activator.CreateInstance besagt ausdrücklich, dass es für dieses Szenario aufgerufen werden sollte. Seltsame Wahl, ich bin mir sicher, dass es einen guten Grund gibt. – JaredPar

+0

Ich vermute, der Grund ist, JIT Code-Sharing zu erhöhen. Wenn Sie im JIT'd-Code einen direkten Aufruf an den Konstruktor eines Typs hatten, könnten Sie diesen JIT-Code nicht mit einer anderen Instanz für einen anderen Typ teilen, z. 'T Create <T>() wobei T: new() {return new T();} würde Maschinencode für Erstellen <Zeichenfolge>() und Erstellen <ArrayList>() teilen. – jonp

2

Interessante Beobachtung :)

Hier ist eine einfachere Variante Ihre Lösung:

static T Create<T>() where T : new() 
{ 
    Expression<Func<T>> e =() => new T(); 
    return e.Compile()(); 
} 

Offensichtlich naiv (und möglicherweise langsam) :)

+2

Ich glaube nicht, dass das funktioniert, weil es speziell "new T()" ist, das seine Workaround versucht zu vermeiden. –

+1

@Joel Mueller Eigentlich funktioniert es. Ausdrucksbaum enthält NewExpression hier. – ghord

+1

Ja, es ist ein Ausdruck von Func , kein Func . Das "() => new T()" produziert nicht IL (wodurch Activator.CreateInstance() erzeugt wird), sondern ein Ausdrucksbaum, der seinerseits zur Laufzeit kompiliert wird, wenn das T bekannt ist. Das einzige Problem hier ist, dass jedes Mal, wenn Sie diese Funktion aufrufen, diese Anweisung erneut kompiliert wird. –

3

Warum ist diese Abhilfe notwendig?

Da die neue() generische Einschränkung C# 2.0 in .NET 2.0 hinzugefügt wurde.

Ausdruck <T> und Freunde wurden inzwischen zu .NET 3.5 hinzugefügt.

Also Ihre Problemumgehung ist notwendig, weil es in .NET 2.0 nicht möglich war. In der Zwischenzeit war (1) die Verwendung von Activator.CreateInstance() möglich und (2) IL fehlt eine Möglichkeit, 'new T()' zu implementieren, daher wurde Activator.CreateInstance() verwendet, um dieses Verhalten zu implementieren.

2

Dies ist ein wenig schneller, da der Ausdruck nur einmal kompiliert wird:

public class Foo<T> where T : new() 
{ 
    static Expression<Func<T>> x =() => new T(); 
    static Func<T> f = x.Compile(); 

    public static T build() 
    { 
     return f(); 
    } 
} 

die Leistung analysiert, ist diese Methode nur so schnell wie der ausführlichen zusammengestellt Ausdruck ist und viel, viel schneller als new T() (160 mal schneller auf meinem Test PC).

Für ein bisschen bessere Leistung kann der Aufruf der Build-Methode eliminiert werden und stattdessen der Funktor zurückgegeben werden, den der Client zwischenspeichern und direkt aufrufen kann.

public static Func<T> BuildFn { get { return f; } }