代码之家  ›  专栏  ›  技术社区  ›  mark

如何在没有装箱的情况下将泛型类型T的值强制转换为double?

  •  13
  • mark  · 技术社区  · 15 年前

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

    上述代码的问题是强制转换到对象,这会导致循环中的装箱。

    有没有一种方法可以实现相同的功能,也就是说,在foreach循环中不借助于装箱,而是将所有的值提供给consumervalue?注意,F必须是泛型方法。

    编辑

    动机。想象一下元数据驱动的应用程序,其中一个代理报告一个数据流,其中数据项类型基于数据流元数据动态地发出。想象一下,还有一个normalizer引擎,它知道根据某种算法对数字数据流进行规范化。传入数值数据流的类型仅在运行时已知,并且可以定向到该数据类型的泛型方法。然而,规格化器期望双倍并产生双倍。这是一个非常高级的描述,请不要深究。

    关于演员加倍。实际上,我们有一个方法可以用以下签名转换为double:

    bool TryConvertToDouble(object value, out double result);
    

    我本来应该在示例中使用它的,但是我想节省空间,并且写了一些行不通的东西。现在修好了。谢谢你的提醒。

    编辑3

    当前的实现不会将值装箱。即使我没有profiler关于它的性能损失的结论(如果有的话),我仍然很想知道是否有一个没有装箱(也没有转换成字符串)的解决方案。让我称之为纯粹的学术兴趣。

    这真的让我感兴趣,因为这样的事情在C++中是很普通的,但是,当然,我还没有开始讨论什么是更好的.NET泛型或者C++模板。

    编辑4

    感谢 https://stackoverflow.com/users/267/lasse-v-karlsen

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

    哪里:

    • ThrowIfNotConvertableToDouble(Type) 是一个简单的方法,确保给定的类型可以转换为double,即一些数字类型或bool。
    • TypeArray<T> 是要生成的助手类 new[]{ typeof(T) }

    Utils<T>.ConvertToDouble 方法以最有效的方式将任何数值转换为双精度,如此问题的答案所示。

    5 回复  |  直到 5 年前
        1
  •  9
  •   halfer    4 年前

    注意:我的初始代码中有一个bug,用于基于实例的代码生成。请重新检查下面的代码。更改的部分是将值加载到堆栈上的顺序(即.Emit行)。答案和存储库中的代码都已修复。

    它在一个int数组和一个boolean数组上执行ConsumeValue 1000万次(在我的示例中它什么也不做),对执行进行计时(它将所有代码运行一次,以消除JIT开销,从而使计时发生偏差)

    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
    

    代码生成的开销大约减少65%。

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

    请注意,如果使GenerationAction、F2/3和ConsumeValue非静态,则必须稍微更改代码:

    1. Action<T> 声明变成 Action<Program, T>

    2. 更改DynamicMethod的创建以包含“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.  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.  action(this, value);
      

    以下是非静态方法的完整更改程序:

    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");
                }
            }
        }
    }
    
        2
  •  6
  •   Alexey Larchenko    10 年前

    这是一个很好的问题,我也有这个任务,我使用编译的Linq表达式来执行值类型与泛型类型参数之间的任意转换,避免了装箱。解决方法非常有效和快速。它在单例中为每个值类型存储一个编译的lambda。用法清晰易读。

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

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

    下面是MyClass的一些简单的性能和内存测试,它们表明使用非固定值可以节省大量内存,减少GC压力,而且性能开销非常小:只有5-10%左右。

    :

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

    结果:

    Time: 130 ms
    Memory: 19.7345771789551 MB
    

    2没有拳击

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

    Time: 144 ms
    Memory: 12.4955024719238 MB
    
        3
  •  0
  •   tzaman    15 年前

    为什么不添加一个 double -特定过载 F 和通用版本一起?

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

    现在如果你打电话 F(someDoubleList) 它将调用非泛型版本,而对于任何其他列表,都将调用泛型版本。

        4
  •  0
  •   Willem van Rumpt    15 年前

    拆箱甚至与演员阵容无关

    ConsumeValue((double)(object)value);
    

    将抛出一个 InvalidCastException 如果 value double this Eric Lippert的博客文章,解释原因。)

    您必须对输入进行预处理,通用变量将不起作用。

    编辑:

    我会选择Convert.ToDouble。 只有在性能 最重要的是,我会用动态方法。

        5
  •  -1
  •   Janus Tøndering    15 年前

    你可以用 Convert 班级。

    ConsumeValue(Convert.ToDouble(value));
    

    不确定ToDouble的内部结构。。。但也许你能做到最好。