Как отличить значение родового типа T к удвоению без бокса?

Представьте себе следующий простой код:

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

Проблема с приведенным выше кодом заключается в том, что он бросает объект, что приводит к боксу в цикле.

Есть ли способ достичь той же функциональности, то есть загружать ConsumeValue со всеми значениями, не прибегая к боксу в цикле foreach? Обратите внимание, что F должен быть общим методом.

Я могу жить с дорогим кодом подготовки, если он выполняется за пределами цикла только один раз. Например, если требуется искушать динамический метод, то это нормально, если сделать только один раз.

ИЗМЕНИТЬ

T гарантированно имеет некоторый числовой тип или bool.

Мотивация. Представьте себе приложение, управляемое метаданными, где агент сообщает поток данных, где тип элемента данных динамически испускается на основе метаданных потока данных. Представьте себе, что есть механизм нормализатора, который знает нормализовать числовые потоки данных в соответствии с некоторым алгоритмом. Тип входящего числового потока данных известен только во время выполнения и может быть направлен на общий метод этого типа данных. Однако нормализатор ожидает удвоения и производит удвоение. Это описание очень высокого уровня, пожалуйста, не вникайте в него.

EDIT2

Отнесение к двойному. На самом деле у нас есть способ преобразования в double со следующей сигнатурой:

bool TryConvertToDouble(object value, out double result);

Я должен был использовать его в примере в первую очередь, но я хотел сэкономить место и написал что-то, что не сработает. Исправлено. Спасибо, что заметили.

EDIT3

Ребята, текущая реализация блокирует значения. И даже если у меня нет вердикта профайлера относительно его исполнения (если есть), мне все же интересно узнать, есть ли решение без бокса (и без преобразования в строку). Позвольте мне называть это чисто академическим интересом. Это действительно меня интересует, потому что подобные вещи тривиальны в С++ с шаблонами, но, конечно, я не начинаю еще один глупый и бессмысленный аргумент в отношении того, что лучше .NET-дженериков или шаблонов С++. Пожалуйста, проигнорируйте это последнее предложение.

EDIT4

Благодаря 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 (Тип) - это простой метод, который гарантирует, что данный тип может быть преобразован в двойной, то есть некоторый числовой тип или bool.
  • TypeArray - это вспомогательный класс для создания new[]{ typeof(T) }

Метод Utils.ConvertToDouble преобразует любое числовое значение, чтобы удвоить наиболее эффективный способ, показанный ответом на этот вопрос.

Это работает как шарм - спасибо человеку.

Ответ 1

ПРИМЕЧАНИЕ. В исходном коде была ошибка при генерации кода на основе экземпляра. Пожалуйста, проверьте код ниже. Измененная часть - это порядок загрузки значений в стек (т.е. строки .Emit). Код в ответе и хранилище были исправлены.

Если вы хотите пойти по пути генерации кода, как вы намекнули в своем вопросе, вот пример кода:

Он выполняет ConsumeValue (который ничего не делает в моем примере) 10 миллионов раз для массива 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

Затраты на генерацию кода примерно на 65% меньше.

Код доступен в моем репозитории Mercurial здесь: http://hg.vkarlsen.no/hgweb.cgi/Qaru, просмотрите его, найдя номер своего 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");
            }
        }
    }
}

Обратите внимание, что если вы сделаете 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. Передайте "this" действию, когда оно вызывается:

    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

Хороший вопрос, у меня также была эта задача, и я придумал скомпилированные выражения Linq, чтобы делать произвольные преобразования типов значений в и из параметров типового типа, избегая бокса. Решение очень эффективно и быстро. Он хранит один скомпилированный лямбда на тип значения в одноэлементном режиме. Использование является чистым и читаемым.

Вот простой класс, который делает работу очень хорошо:

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%.

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

Результаты:

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

Вы можете использовать класс Convert.

ConsumeValue(Convert.ToDouble(value));

Не уверен о внутренних функциях ToDouble... но, вероятно, лучше всего вы можете сделать.

Ответ 4

Почему бы просто не добавить double -специфическую перегрузку для F наряду с общей версией?

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

Теперь, если вы вызываете F(someDoubleList), он вызовет не-общую версию, а с любым другим списком будет вызван общий вызов.

Ответ 5

Хотя сценарий все еще не очень ясен (см. мой комментарий), это никогда не сработает. Вам нужно будет предоставить собственный класс или метод, который может конвертировать из вашего общего Т в двойной.

Распаковка не имеет особого значения, так как приведение в

ConsumeValue((double)(object)value);

будет бросать InvalidCastException, если value не является double. (см. эту запись в блоге Эрика Липперта по причинам.)

Вам нужно предварительно обработать вход, общий вариант не будет работать.

Edit:

Я бы выбрал Convert.ToDouble. Только если производительность ab-so-lu-te-ly высшего приоритета, я бы пошел с динамическим методом. Он добавляет достаточно сложно, чтобы избежать его, если это возможно. Очевидно, что прирост производительности около 50% выглядит значительным, но, по сценарию, представленному Lasse, на моей машине я получаю около 150 мс при повторении более 10000000 (десять миллионов) предметов, экономя вас на 0,000015 миллисекундах за каждую итерацию.