2010-07-27 6 views
11

imaginar el siguiente código simple:Cómo convertir un valor de tipo genérico T a double sin boxeo?

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) 
{ 
} 

El problema con el código anterior está lanzando a oponerse, lo que resulta en el boxeo en el bucle.

¿Hay alguna manera de lograr la misma funcionalidad, es decir, alimentar a ConsumeValue con todos los valores sin recurrir al boxeo en el bucle foreach? Tenga en cuenta que F debe ser un método genérico.

Puedo vivir con un código de preparación costoso, siempre que se ejecute fuera del circuito una sola vez. Por ejemplo, si se necesita emitir un método dinámico sofisticado, está bien si se hace solo una vez.

EDITAR

T se garantiza que sea de algún tipo numérico o bool.

Motivación. Imagine una aplicación impulsada por metadatos, donde un agente informa un flujo de datos, donde el tipo de elemento de datos se emite dinámicamente en base a los metadatos del flujo de datos. Imagine también que hay un motor normalizador que sabe normalizar las secuencias de datos numéricos de acuerdo con algún algoritmo. El tipo de flujo de datos numéricos entrantes se conoce solo en tiempo de ejecución y se puede dirigir a un método genérico de ese tipo de datos. El normalizador, sin embargo, espera dobles y produce dobles. Esta es una descripción de muy alto nivel, por favor no profundizar en ella.

Edit2

En cuanto al reparto de duplicar. En realidad tenemos un método para convertir al doble con la firma siguiente:

bool TryConvertToDouble(object value, out double result); 

Debería haber usado en el ejemplo, en primer lugar, pero queríamos ahorrar espacio y algo escrito que no se va a trabajar. Solucionado ahora Gracias por anotar

Edit3

Guys, la implementación actual hace caja los valores. E incluso si no tengo el veredicto del perfilador en cuanto a la penalización del rendimiento (si corresponde), aún así, me interesa saber si hay una solución sin boxeo (y sin convertirla en cadena). Déjame llamarlo interés puramente académico. Esto realmente me interesa, porque cosas así son triviales en C++ con plantillas, pero, por supuesto, no estoy empezando otro argumento estúpido e inútil sobre qué es mejor .NET genéricos o plantillas de C++. Por favor, ignora esta última oración.

EDIT4

Gracias a https://stackoverflow.com/users/267/lasse-v-karlsen que proporcionó la respuesta. En realidad, he utilizado el ejemplo de código para escribir una clase simple como esto:

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); 
    } 
} 

Dónde:

  • ThrowIfNotConvertableToDouble (Tipo) es un método simple que hace que el tipo dado puede ser convertido a duplicar , es decir, algún tipo numérico o bool.
  • TypeArray es una clase de ayuda para producir new[]{ typeof(T) }

El método Utils.ConvertToDouble convierte cualquier valor numérico que se duplique en la forma más eficiente, lo demuestra la respuesta a esta pregunta.

Funciona como un encanto - gracias hombre.

+7

El problema con lo anterior es también que no tiene mucho sentido. ¿Por qué usar un método genérico, con una restricción, y luego convertirlo en un doble? ¿Puedes explicar más claramente lo que estás tratando de lograr? –

+0

Esto me parece extraño. ¿Por qué estás lanzando una estructura genérica a un objeto y luego a un doble? ¿Hay algún problema con este ejemplo? ¿Necesitamos más contexto? Este código parece tan fuera de lugar, que no sé cómo responderlo ... –

+0

He actualizado la pregunta. – mark

Respuesta

7

NOTA: Se produjo un error en mi código inicial para la generación de código basado en instancia. Por favor vuelva a verificar el código a continuación. La parte modificada es el orden de carga de valores en la pila (es decir, las líneas Emit). Tanto el código en la respuesta como el repositorio han sido corregidos.

Si quieres ir a la ruta de la generación de código, como se insinúan en su pregunta, aquí está el código de ejemplo:

Ejecuta ConsumeValue (que no hace nada en mi ejemplo) 10 millones de veces, en una serie de ints y una matriz de booleanos, cronometrando la ejecución (ejecuta todo el código una sola vez, para eliminar la sobrecarga de JIT y evitar el sesgo del tiempo).)

La salida:

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 

Aproximadamente 65% menos sobrecarga con generación de código.

El código está disponible en mi repositorio de Mercurial aquí: http://hg.vkarlsen.no/hgweb.cgi/StackOverflow, navegúelo buscando su número de pregunta.

El código:

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"); 
      } 
     } 
    } 
} 

Tenga en cuenta que si haces GenerationAction, F2/3 y ConsumeValue no estático, usted tiene que cambiar ligeramente el código:

  1. Todos Action<T> declaraciones se convierte en Action<Program, T>
  2. Cambie la creación de DynamicMethod para incluir el parámetro "this":

    DynamicMethod method = new DynamicMethod(
        "action", MethodAttributes.Public | MethodAttributes.Static, 
        CallingConventions.Standard, 
        typeof(void), new Type[] { typeof(Program), typeof(T) }, 
        typeof(Program).Module, 
        false); 
    
  3. Cambiar las instrucciones para cargar los valores correctos en el momento adecuado:

    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 "esto" a la acción cada vez que se llama:

    action(this, value); 
    

Aquí está el programa completo modificado para métodos no estáticos:

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. Muy interesante, me tomará un tiempo verificarlo, pero suena prometedor. – mark

+0

Asegúrese de verificar que está utilizando la versión correcta del código. Acabo de arreglar un error en el orden de las instrucciones emitidas. –

+0

Incluso podría aumentar el rendimiento almacenando en caché la acción generada en alguna ranura de tipo singleton á la 'ConvertAction .Instance'. De esta forma, no tendrá que lidiar con un diccionario y preocuparse por la seguridad de los hilos. – herzmeister

0

Usted puede utilizar la clase Convert.

ConsumeValue(Convert.ToDouble(value)); 

No estoy seguro sobre los elementos internos de ToDouble ... pero probablemente sea lo mejor que puede hacer.

+0

No, no puedo. Convert.ToDouble no es un método genérico. – mark

+0

Funciona bien en C# 3.0 (NET 3.5) –

+0

Esto se debe a que T se convierte en objeto y luego se llama sobrecarga Convert.ToDouble (objeto): ha cambiado el boxeo explícito a uno implícito. Compruébalo tú mismo. – mark

0

¿Por qué no acaba de agregar una sobrecarga específica de double para F junto con la versión genérica?

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

Ahora si usted llama F(someDoubleList) que va a llamar a la versión no genérica, y con cualquier otra lista de la genérica será llamado.

+0

Tengo IList instancia. Es un hecho. ¿Cómo lo hago IList ? – mark

+0

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

+0

¿Y qué si typeof (T) == typeof (int)? o typeof (float) o typeof (uint)? – mark

0

Aunque el escenario todavía no está muy claro (ver mi comentario), esto nunca va a funcionar. Deberá proporcionar una clase o método personalizado que pueda convertir su T genérico a doble.

El unboxing ni siquiera es relevante, ya que el elenco de

ConsumeValue((double)(object)value); 

arrojará una InvalidCastException si value no es un double sí. (vea this entrada de blog de Eric Lippert por las razones por las cuales)

Tendrá que preprocesar la entrada, la variante genérica no funcionará.

Editar:

yo elegiría Convert.ToDouble. Sólo si el rendimiento es ab-so-lu-te-ly máxima prioridad, me gustaría ir con el método dinámico. Agrega suficiente complejidad para evitarlo, si es posible. La ganancia de rendimiento de alrededor del 50% parece importante, por supuesto, pero, en el escenario dado por Lasse, en mi máquina gano alrededor de 150 ms cuando se repite más de 10.000.000 (diez millones) de artículos, ahorrándole 0,000015 milisegundos cada iteración.

+0

Tienes razón. Solo quería dar el ejemplo más simple posible y terminé simplemente con un código malo. Editó la pregunta para corregir este desafortunado error. – mark

+0

Puede usar Convert.ToDouble sugerido por otras respuestas. Pero como entiendo, le preocupa el problema de rendimiento causado por el boxeo/unboxing. En ese caso Convert.ToDouble podría ser una opción peor. Aún así, a menos que las pruebas ya hayan demostrado que las conversiones son realmente un cuello de botella, lo intentaré. –

+0

Editado mi pregunta una vez más. – mark

4

Es una buena pregunta, también tuve esta tarea y surgió utilizando expresiones compiladas de Linq para realizar conversiones arbitrarias de tipos de valores desde y hacia parámetros de tipo genéricos, evitando el boxeo. La solución es muy efectiva y rápida. Almacena un lambda compilado por tipo de valor en un singleton. El uso es limpio y legible.

Aquí está una clase simple que hace el trabajo muy bien:

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(); 
    } 
} 

Ahora imagine que usted quiere tener algunos de almacenamiento de objetos y dobles y que no quiere que sus dobles a estar dentro de cajas. Se puede escribir dicha clase con captadores genéricos y definidores de la siguiente manera:

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 
    } 
} 

Aquí hay algunas pruebas de rendimiento y memoria simples de MiClase que muestran que el uso de valores unboxed le puede ahorrar una gran cantidad de memoria, reducir la presión GC y la sobrecarga de rendimiento es muy pequeña: solo alrededor del 5-10%.

1. Con el boxeo:

 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); 

Resultados:

Time: 130 ms 
Memory: 19.7345771789551 MB 

2.Sin boxeo

 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); 

Resultados:

Time: 144 ms 
Memory: 12.4955024719238 MB 
+0

Esto se ve muy bien para mí. Estoy sorprendido de ver el (ligero) rendimiento general. Esperaba que el lambda compilado lo hiciera más rápido que el boxeo. ¿Alguna idea de por qué no lo es? – Timo

Cuestiones relacionadas