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

mar*_*ark 15 .net

想象一下以下简单的代码:

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)
{
}
Run Code Online (Sandbox Code Playgroud)

上面代码的问题是转换为对象,这导致在循环中装箱.

有没有办法实现相同的功能,即为ConsumeValue提供所有值而不需要在foreach循环中使用装箱?注意,F必须是通用方法.

只要在循环外执行一次,我就可以使用昂贵的准备代码.例如,如果需要发出一个奇特的动态方法,那么只做一次就可以了.

编辑

T保证是某种数字类型或bool.

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

EDIT2

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

bool TryConvertToDouble(object value, out double result);
Run Code Online (Sandbox Code Playgroud)

我应该首先在示例中使用它,但我想节省空间并编写一些不起作用的东西.现在修好了.谢谢你的注意.

EDIT3

伙计们,目前的实施确实包装了价值观.即使我没有关于它的性能惩罚的探查者的判断(如果有的话),我仍然有兴趣知道是否有没有装箱的解决方案(并且没有转换为字符串).让我称之为纯粹的学术兴趣.这真的让我感兴趣,因为在C++中使用模板这样的东西是微不足道的,但是,当然,我还没有开始关于什么是更好的.NET泛型或C++模板的另一个愚蠢和毫无意义的论点.请忽略这最后一句话.

EDIT4

感谢/sf/users/18721/提供了答案.实际上,我已经使用他的代码示例编写了一个这样的简单类:

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);
  }
}
Run Code Online (Sandbox Code Playgroud)

哪里:

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

Utils.ConvertToDouble方法以最有效的方式将任何数值转换为double,由此问题的答案显示.

它就像一个魅力 - 谢谢你.

ang*_*son 8

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

如果你想要代码生成的路径,正如你在你的问题中提示的那样,这里是示例代码:

它执行ConsumeValue(在我的例子中什么都不做)1000万次,在一个int数组和一个布尔数组上,计时执行(它运行所有代码一次,以消除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
Run Code Online (Sandbox Code Playgroud)

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

该代码可以从我的Mercurial存储库获得:http://hg.vkarlsen.no/hgweb.cgi/StackOverflow,通过查找您的SO问题编号来浏览它.

代码:

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");
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

请注意,如果您使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);
    
    Run Code Online (Sandbox Code Playgroud)
  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);
    
    Run Code Online (Sandbox Code Playgroud)
  4. 无论何时调用,都将"this"传递给动作:

    action(this, value);
    
    Run Code Online (Sandbox Code Playgroud)

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

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");
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)


Ale*_*nko 5

这是一个很好的问题,我也有这个任务,我想出了使用编译的Linq Expressions进行值类型与通用类型参数之间的任意转换,从而避免了装箱。该解决方案非常有效且快速。它在单例中为每个值类型存储一个已编译的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();
    }
}
Run Code Online (Sandbox Code Playgroud)

现在想像一下,您想存储一些对象和双打,而又不想将双打装箱。您可以通过以下方式使用通用的getter和setter编写此类:

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
    }
}
Run Code Online (Sandbox Code Playgroud)

以下是MyClass的一些简单性能和内存测试,这些测试表明使用取消装箱的值可以节省大量内存,减少GC压力,并且性能开销非常小:大约5-10%。

1.装箱

        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);
Run Code Online (Sandbox Code Playgroud)

结果:

Time: 130 ms
Memory: 19.7345771789551 MB
Run Code Online (Sandbox Code Playgroud)

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);
Run Code Online (Sandbox Code Playgroud)

结果:

Time: 144 ms
Memory: 12.4955024719238 MB
Run Code Online (Sandbox Code Playgroud)