2010-07-27 13 views
11

Immaginate il seguente codice semplice:Come trasmettere un valore di tipo generico T al doppio senza box?

public void F<T>(IList<T> values) where T : struct 
{ 
    foreach (T value in values) 
    { 
    double result; 
    if (TryConvertToDouble((object)value, out result)) 
    { 
     ConsumeValue(result); 
    } 
    } 
} 

public void ConsumeValue(double value) 
{ 
} 

Il problema con il codice di cui sopra è colata di opporsi, che si traduce nel pugilato nel ciclo.

Esiste un modo per ottenere la stessa funzionalità, ovvero alimentare ConsumeValue con tutti i valori senza ricorrere al pugilato nel ciclo foreach? Nota, che F deve essere un metodo generico.

Posso vivere con un codice di preparazione costoso purché venga eseguito all'esterno del ciclo una sola volta. Ad esempio, se è necessario emettere un metodo dinamico elegante, allora va bene se fatto una sola volta.

EDIT

T è garantito per essere di qualche tipo numerico o bool.

Motivazione. Immagina un'applicazione basata sui meta dati, in cui un agente segnala un flusso di dati, in cui il tipo di elemento di dati viene emesso dinamicamente in base ai metadati del flusso di dati. Immaginate anche che ci sia un motore normalizzatore, che sa normalizzare i flussi di dati numerici secondo alcuni algoritmi. Il tipo del flusso di dati numerico in entrata è noto solo in fase di esecuzione e può essere indirizzato a un metodo generico di quel tipo di dati. Il normalizzatore, tuttavia, si aspetta il doppio e produce il doppio. Questa è una descrizione di altissimo livello, per favore non approfondirla.

EDIT2

riguardante l'getto di raddoppiare. In realtà abbiamo un metodo per convertire a raddoppiare con la seguente firma:

bool TryConvertToDouble(object value, out double result); 

avrei usato nell'esempio, in primo luogo, ma ho voluto risparmiare spazio e qualcosa di scritto che non è andare a lavorare. Risolto adesso Grazie per averlo notato

Edit3

ragazzi, l'implementazione corrente inscatolare i valori. E anche se non ho il verdetto del profiler per quanto riguarda la penalizzazione delle prestazioni (se esiste), sono comunque interessante sapere se esiste una soluzione senza boxing (e senza conversione in stringa). Lasciatemelo definire un interesse puramente accademico. Questo mi interessa davvero, perché cose del genere sono banali in C++ con i modelli, ma, naturalmente, non sto iniziando un'altra discussione stupida e inutile su ciò che è meglio dei generici .NET o dei modelli C++. Per favore, ignora quest'ultima frase.

edit4

Grazie a https://stackoverflow.com/users/267/lasse-v-karlsen che ha fornito la risposta. A dire il vero, ho usato il suo codice di esempio per scrivere una classe semplice come questo:

public static class Utils<T> 
{ 
    private static class ToDoubleConverterHolder 
    { 
    internal static Func<T, double> Value = EmitConverter(); 

    private static Func<T, double> EmitConverter() 
    { 
     ThrowIfNotConvertableToDouble(typeof(T)); 

     var method = new DynamicMethod(string.Empty, typeof(double), TypeArray<T>.Value); 
     var il = method.GetILGenerator(); 

     il.Emit(OpCodes.Ldarg_0); 
     if (typeof(T) != typeof(double)) 
     { 
     il.Emit(OpCodes.Conv_R8); 
     } 
     il.Emit(OpCodes.Ret); 

     return (Func<T, double>)method.CreateDelegate(typeof(Func<T, double>)); 
    } 
    } 

    public static double ConvertToDouble(T value) 
    { 
    return ToDoubleConverterHolder.Value(value); 
    } 
} 

Dove:

  • ThrowIfNotConvertableToDouble (Type) è un metodo semplice che consente di verificare il tipo di dato può essere convertito in doppio , cioè qualche tipo numerico o bool.
  • TypeArray è una classe di supporto per produrre new[]{ typeof(T) }

Procedimento Utils.ConvertToDouble converte qualsiasi valore numerico di raddoppiare nel modo più efficiente, dimostra la risposta a questa domanda.

Funziona come un incantesimo - grazie amico.

+7

Il problema di quanto sopra è anche che non ha molto senso. Perché utilizzare un metodo generico, con un vincolo, e quindi eseguirlo con un cast rigido? Puoi spiegare più chiaramente cosa stai cercando di ottenere? –

+0

Questo mi sembra strano. Perché stai lanciando una struttura generica a un oggetto e poi a un doppio? C'è qualcosa di sbagliato in questo esempio? Abbiamo bisogno di più contesto? Questo codice sembra così fuori luogo, che non so come rispondere ... –

+0

Ho aggiornato la domanda. – mark

risposta

7

NOTA: c'era un errore nel mio codice iniziale per la generazione di codice basata su istanze. Si prega di ricontrollare il codice qui sotto. La parte modificata è l'ordine di caricamento dei valori nello stack (ad esempio le righe .Emit). Sia il codice nella risposta che il repository sono stati corretti.

Se si vuole andare il percorso di generazione del codice, come suggerimento per la tua domanda, ecco il codice di esempio:

Esegue ConsumeValue (che non fa nulla nel mio esempio) 10 milioni di volte, su un array di ints e una matrice di booleani, cronometrando l'esecuzione (esegue tutto il codice una sola volta, per rimuovere il sovraccarico JIT dall'inflessione dei tempi.)

L'output:

F1 ints = 445ms   <-- uses Convert.ToDouble 
F1 bools = 351ms 
F2 ints = 159ms   <-- generates code on each call 
F2 bools = 167ms 
F3 ints = 158ms   <-- caches generated code between calls 
F3 bools = 163ms 

Circa il 65% meno overhead con generazione del codice.

Il codice è disponibile dal mio repository Mercurial qui: http://hg.vkarlsen.no/hgweb.cgi/StackOverflow, sfogliatelo trovando il proprio numero di domanda SO.

Il codice:

using System; 
using System.Collections.Generic; 
using System.Diagnostics; 
using System.Linq; 
using System.Reflection; 
using System.Reflection.Emit; 

namespace ConsoleApplication15 
{ 
    class Program 
    { 
     public static void F1<T>(IList<T> values) where T : struct 
     { 
      foreach (T value in values) 
       ConsumeValue(Convert.ToDouble(value)); 
     } 

     public static Action<T> GenerateAction<T>() 
     { 
      DynamicMethod method = new DynamicMethod(
       "action", MethodAttributes.Public | MethodAttributes.Static, 
       CallingConventions.Standard, 
       typeof(void), new Type[] { typeof(T) }, typeof(Program).Module, 
       false); 
      ILGenerator il = method.GetILGenerator(); 

      il.Emit(OpCodes.Ldarg_0); // get value passed to action 
      il.Emit(OpCodes.Conv_R8); 
      il.Emit(OpCodes.Call, typeof(Program).GetMethod("ConsumeValue")); 
      il.Emit(OpCodes.Ret); 

      return (Action<T>)method.CreateDelegate(typeof(Action<T>)); 
     } 

     public static void F2<T>(IList<T> values) where T : struct 
     { 
      Action<T> action = GenerateAction<T>(); 
      foreach (T value in values) 
       action(value); 
     } 

     private static Dictionary<Type, object> _Actions = 
      new Dictionary<Type, object>(); 
     public static void F3<T>(IList<T> values) where T : struct 
     { 
      Object actionObject; 
      if (!_Actions.TryGetValue(typeof(T), out actionObject)) 
      { 
       actionObject = GenerateAction<T>(); 
       _Actions[typeof (T)] = actionObject; 
      } 
      Action<T> action = (Action<T>)actionObject; 
      foreach (T value in values) 
       action(value); 
     } 

     public static void ConsumeValue(double value) 
     { 
     } 

     static void Main(string[] args) 
     { 
      Stopwatch sw = new Stopwatch(); 

      int[] ints = Enumerable.Range(1, 10000000).ToArray(); 
      bool[] bools = ints.Select(i => i % 2 == 0).ToArray(); 

      for (int pass = 1; pass <= 2; pass++) 
      { 
       sw.Reset(); 
       sw.Start(); 
       F1(ints); 
       sw.Stop(); 
       if (pass == 2) 
        Console.Out.WriteLine("F1 ints = " 
         + sw.ElapsedMilliseconds + "ms"); 

       sw.Reset(); 
       sw.Start(); 
       F1(bools); 
       sw.Stop(); 
       if (pass == 2) 
        Console.Out.WriteLine("F1 bools = " 
         + sw.ElapsedMilliseconds + "ms"); 

       sw.Reset(); 
       sw.Start(); 
       F2(ints); 
       sw.Stop(); 
       if (pass == 2) 
        Console.Out.WriteLine("F2 ints = " 
         + sw.ElapsedMilliseconds + "ms"); 

       sw.Reset(); 
       sw.Start(); 
       F2(bools); 
       sw.Stop(); 
       if (pass == 2) 
        Console.Out.WriteLine("F2 bools = " 
         + sw.ElapsedMilliseconds + "ms"); 

       sw.Reset(); 
       sw.Start(); 
       F3(ints); 
       sw.Stop(); 
       if (pass == 2) 
        Console.Out.WriteLine("F3 ints = " 
         + sw.ElapsedMilliseconds + "ms"); 

       sw.Reset(); 
       sw.Start(); 
       F3(bools); 
       sw.Stop(); 
       if (pass == 2) 
        Console.Out.WriteLine("F3 bools = " 
         + sw.ElapsedMilliseconds + "ms"); 
      } 
     } 
    } 
} 

Si noti che se si fanno GenerationAction, F2/3 e ConsumeValue non statici, è necessario modificare il codice un po ':

  1. Tutti Action<T> dichiarazioni diventa Action<Program, T>
  2. Modificare la creazione di DynamicMethod per includere il parametro "questo":

    DynamicMethod method = new DynamicMethod(
        "action", MethodAttributes.Public | MethodAttributes.Static, 
        CallingConventions.Standard, 
        typeof(void), new Type[] { typeof(Program), typeof(T) }, 
        typeof(Program).Module, 
        false); 
    
  3. Cambiare le istruzioni per caricare i valori giusti al momento giusto:

    il.Emit(OpCodes.Ldarg_0); // get "this" 
    il.Emit(OpCodes.Ldarg_1); // get value passed to action 
    il.Emit(OpCodes.Conv_R8); 
    il.Emit(OpCodes.Call, typeof(Program).GetMethod("ConsumeValue")); 
    il.Emit(OpCodes.Ret); 
    
  4. Pass "questo" per l'azione ogni volta che si chiama:

    action(this, value); 
    

Ecco il programma completo modificato per i metodi non statici:

using System; 
using System.Collections.Generic; 
using System.Diagnostics; 
using System.Linq; 
using System.Reflection; 
using System.Reflection.Emit; 

namespace ConsoleApplication15 
{ 
    class Program 
    { 
     public void F1<T>(IList<T> values) where T : struct 
     { 
      foreach (T value in values) 
       ConsumeValue(Convert.ToDouble(value)); 
     } 

     public Action<Program, T> GenerateAction<T>() 
     { 
      DynamicMethod method = new DynamicMethod(
       "action", MethodAttributes.Public | MethodAttributes.Static, 
       CallingConventions.Standard, 
       typeof(void), new Type[] { typeof(Program), typeof(T) }, 
       typeof(Program).Module, 
       false); 
      ILGenerator il = method.GetILGenerator(); 

      il.Emit(OpCodes.Ldarg_0); // get "this" 
      il.Emit(OpCodes.Ldarg_1); // get value passed to action 
      il.Emit(OpCodes.Conv_R8); 
      il.Emit(OpCodes.Call, typeof(Program).GetMethod("ConsumeValue")); 
      il.Emit(OpCodes.Ret); 

      return (Action<Program, T>)method.CreateDelegate(
       typeof(Action<Program, T>)); 
     } 

     public void F2<T>(IList<T> values) where T : struct 
     { 
      Action<Program, T> action = GenerateAction<T>(); 
      foreach (T value in values) 
       action(this, value); 
     } 

     private static Dictionary<Type, object> _Actions = 
      new Dictionary<Type, object>(); 
     public void F3<T>(IList<T> values) where T : struct 
     { 
      Object actionObject; 
      if (!_Actions.TryGetValue(typeof(T), out actionObject)) 
      { 
       actionObject = GenerateAction<T>(); 
       _Actions[typeof (T)] = actionObject; 
      } 
      Action<Program, T> action = (Action<Program, T>)actionObject; 
      foreach (T value in values) 
       action(this, value); 
     } 

     public void ConsumeValue(double value) 
     { 
     } 

     static void Main(string[] args) 
     { 
      Stopwatch sw = new Stopwatch(); 

      Program p = new Program(); 
      int[] ints = Enumerable.Range(1, 10000000).ToArray(); 
      bool[] bools = ints.Select(i => i % 2 == 0).ToArray(); 

      for (int pass = 1; pass <= 2; pass++) 
      { 
       sw.Reset(); 
       sw.Start(); 
       p.F1(ints); 
       sw.Stop(); 
       if (pass == 2) 
        Console.Out.WriteLine("F1 ints = " 
         + sw.ElapsedMilliseconds + "ms"); 

       sw.Reset(); 
       sw.Start(); 
       p.F1(bools); 
       sw.Stop(); 
       if (pass == 2) 
        Console.Out.WriteLine("F1 bools = " 
         + sw.ElapsedMilliseconds + "ms"); 

       sw.Reset(); 
       sw.Start(); 
       p.F2(ints); 
       sw.Stop(); 
       if (pass == 2) 
        Console.Out.WriteLine("F2 ints = " 
         + sw.ElapsedMilliseconds + "ms"); 

       sw.Reset(); 
       sw.Start(); 
       p.F2(bools); 
       sw.Stop(); 
       if (pass == 2) 
        Console.Out.WriteLine("F2 bools = " 
         + sw.ElapsedMilliseconds + "ms"); 

       sw.Reset(); 
       sw.Start(); 
       p.F3(ints); 
       sw.Stop(); 
       if (pass == 2) 
        Console.Out.WriteLine("F3 ints = " 
         + sw.ElapsedMilliseconds + "ms"); 

       sw.Reset(); 
       sw.Start(); 
       p.F3(bools); 
       sw.Stop(); 
       if (pass == 2) 
        Console.Out.WriteLine("F3 bools = " 
         + sw.ElapsedMilliseconds + "ms"); 
      } 
     } 
    } 
} 
+0

Wow. Molto interessante, mi ci vorrà del tempo per verificarlo, ma sembra promettente. – mark

+0

Assicurati di verificare che stai utilizzando la versione corretta del codice. Ho corretto un bug nell'ordine delle istruzioni emesse. –

+0

È persino possibile aumentare le prestazioni memorizzando nella cache l'azione generata in uno slot simile a un singleton alla "ConvertAction .Instance". In questo modo non dovrai preoccuparti di un dizionario e preoccuparti della sicurezza dei thread. – herzmeister

0

È possibile utilizzare la classe Convert.

ConsumeValue(Convert.ToDouble(value)); 

Non sono sicuro degli interni di ToDouble ... ma probabilmente il meglio che si possa fare.

+0

No, non posso. Convert.ToDouble non è un metodo generico. – mark

+0

Funziona bene in C# 3.0 (NET 3.5) –

+0

Questo perché T viene lanciato su oggetto e quindi viene chiamato il sovraccarico Convert.ToDouble (oggetto) - hai scambiato il boxing esplicito con uno implicito. Controlla tu stesso. – mark

0

Perché non aggiungere solo un sovraccarico specifico double per F insieme alla versione generica?

public void F(IList<double> values) 
{ 
    foreach (double value in values) 
    { 
     ConsumeValue(value); 
    } 
} 

Ora, se si chiamano F(someDoubleList) si chiamerà la versione non generica, e con qualsiasi altra lista verrà chiamato quello generico.

+0

Ho istanza di IList . È un dato. Come faccio a renderlo IList ? – mark

+0

'if (typeof (T) == typeof (double)) IList dlist = (IList ) list' – thecoop

+0

E se typeof (T) == typeof (int)? o typeof (float) o typeof (uint)? – mark

0

Sebbene lo scenario non sia ancora chiaro (vedi il mio commento), questo non funzionerà mai. Dovrai fornire una classe o un metodo personalizzato in grado di convertire dal tuo T generico al doppio.

L'unboxing non è nemmeno rilevante, come il cast in

ConsumeValue((double)(object)value); 

getterà un InvalidCastException se value non è un double sé. (vedi this blog di Eric Lippert per le ragioni perché.)

Dovrete pre-elaborazione l'ingresso, la variante generica non funzionerà.

Edit:

sceglierei Convert.ToDouble. Solo se la prestazione è ab-so-lu-te-ly priorità assoluta, sceglierei il metodo dinamico. Aggiunge abbastanza complessità per evitarlo, se possibile. Il guadagno in termini di prestazioni di circa il 50% sembra ovviamente significativo, ma, nello scenario dato da Lasse, sulla mia macchina guadagno circa 150 ms quando iterando oltre 10000000 (dieci milioni) articoli, risparmiando 0,000015 millisecondi per ogni iterazione.

+0

Hai ragione. Volevo solo dare un esempio il più semplice possibile e ho finito con un semplice codice cattivo. Modificato la domanda per correggere questo sfortunato errore. – mark

+0

È possibile utilizzare Convert.ToDouble suggerito da altre risposte. Ma, come ho capito, sei preoccupato per il problema di prestazioni causato dal pugilato/unboxing. In tal caso Convert.ToDouble potrebbe essere una scelta peggiore. Tuttavia, a meno che i test non abbiano già dimostrato che le conversioni sono davvero un collo di bottiglia, vorrei provarci. –

+0

Modificato ancora la mia domanda. – mark

4

È una buona domanda, ho anche avuto questo compito e sono arrivato usando Linq Expressions compilate per fare conversioni arbitrarie di tipi di valore da e verso parametri di tipo generico evitando il pugilato. La soluzione è molto efficace e veloce. Memorizza un lambda per tipo di valore compilato in un singleton. L'utilizzo è pulito e leggibile.

Ecco una semplice classe che fa il lavoro molto bene:

public sealed class BoxingSafeConverter<TIn, TOut>   
{ 
    public static readonly BoxingSafeConverter<TIn, TOut> Instance = new BoxingSafeConverter<TIn, TOut>(); 
    private readonly Func<TIn, TOut> convert;   

    public Func<TIn, TOut> Convert 
    { 
     get { return convert; } 
    } 

    private BoxingSafeConverter() 
    { 
     if (typeof (TIn) != typeof (TOut)) 
     { 
      throw new InvalidOperationException("Both generic type parameters must represent the same type."); 
     } 
     var paramExpr = Expression.Parameter(typeof (TIn)); 
     convert = 
      Expression.Lambda<Func<TIn, TOut>>(paramExpr, // this conversion is legal as typeof(TIn) = typeof(TOut) 
       paramExpr) 
       .Compile(); 
    } 
} 

Ora immaginate che si vuole avere un po 'di stoccaggio di oggetti e doppie e non volete che i vostri doppi per essere inscatolati. Si potrebbe scrivere tale classe con getter generici e setter nel seguente modo:

public class MyClass 
{ 
    readonly List<double> doubles = new List<double>(); // not boxed doubles 
    readonly List<object> objects = new List<object>(); // all other objects 

    public void BoxingSafeAdd<T>(T val) 
    { 
     if (typeof (T) == typeof (double)) 
     { 
      // T to double conversion 
      doubles.Add(BoxingSafeConverter<T, double>.Instance.Convert(val)); 
      return; 
     } 

     objects.Add(val); 
    } 

    public T BoxingSafeGet<T>(int index) 
    { 
     if (typeof (T) == typeof (double)) 
     { 
      // double to T conversion 
      return BoxingSafeConverter<double, T>.Instance.Convert(doubles[index]); 
     } 

     return (T) objects[index]; // boxing-unsage conversion 
    } 
} 

Ecco alcuni semplici test di performance e di memoria di MyClass che dimostrano che utilizzando i valori disimballati si può risparmiare un sacco di memoria, ridurre la pressione GC e l'overhead delle prestazioni è molto piccolo: solo circa il 5-10%.

1. Con la boxe:

 const int N = 1000000; 
     MyClass myClass = new MyClass(); 

     double d = 0.0; 
     var sw = Stopwatch.StartNew(); 
     for (int i = 0; i < N; i++, d += 0.1) 
     { 
      myClass.BoxingSafeAdd((object)d); 
     } 
     Console.WriteLine("Time: {0} ms", sw.ElapsedMilliseconds); 

     Console.WriteLine("Memory: {0} MB.", (double)GC.GetTotalMemory(false)/1024/1024); 

Risultati:

Time: 130 ms 
Memory: 19.7345771789551 MB 

2.Senza boxe

 const int N = 1000000; 
     MyClass myClass = new MyClass(); 

     double d = 0.0; 
     var sw = Stopwatch.StartNew(); 
     for (int i = 0; i < N; i++, d += 0.1) 
     { 
      myClass.BoxingSafeAdd(d); 
     } 
     Console.WriteLine("Time: {0} ms", sw.ElapsedMilliseconds); 

     Console.WriteLine("Memory: {0} MB", (double)GC.GetTotalMemory(false)/1024/1024); 

Risultati:

Time: 144 ms 
Memory: 12.4955024719238 MB 
+0

Questo mi sembra molto buono. Sono sorpreso di vedere il (leggero) sovraccarico delle prestazioni. Mi aspettavo che il lambda compilato avrebbe reso più veloce della boxe. Qualche idea sul perché non lo è? – Timo

Problemi correlati