Дизайн и производительность универсальных интерфейсов C#

У меня есть основной вопрос относительно общих интерфейсов.

Случай 1:

public interface IDataProcesser
{
    TOut Process<Tin,TOut>(Tin input);
}

Случай 2:

public interface IDataProcesser<Tin,TOut>
{
    TOut Process(Tin input);
}

Приводит ли Случай 1 к боксу/распаковке? Является ли он менее производительным по сравнению со случаем 2. Существуют ли какие-либо рекомендации при разработке универсальных интерфейсов?

Спасибо, Рави.


person Ravi    schedule 11.09.2018    source источник
comment
Это сильно зависит от того, какими будут реальные типы этих Tin и TOut, и независимо от этого просто измерьте с помощью какого-нибудь профилировщика.   -  person Tigran    schedule 11.09.2018
comment
Было бы интересно узнать, почему вы думаете, что здесь может быть замешан бокс. Между этими двумя подходами не должно быть различий в производительности, и даже если бы они были, было бы это важным различием, которое повлияло бы на общую производительность вашей системы?   -  person Damien_The_Unbeliever    schedule 11.09.2018
comment
Полезный справочный материал. Ваш вопрос находится в нескольких шагах от того, чтобы даже потребовать ответа, и к тому времени, когда он появится, у вас будет гораздо больше, чем просто эти интерфейсы.   -  person Jeroen Mostert    schedule 11.09.2018
comment
Упаковка — это процесс преобразование типа значения в объект типа.. Одна из целей, для которой были созданы универсальные шаблоны, заключалась в том, чтобы помочь избежать упаковки.   -  person Liam    schedule 11.09.2018
comment
Никакого бокса/распаковки не произойдет, потому что вы указываете тип. Я не вижу недостатка в производительности.   -  person Igor Quirino    schedule 11.09.2018
comment
Что касается вопроса дизайна: если ваш IDataProcessor имеет более одного метода, случай 2, вероятно, будет более кратким. Если реализации IDataProcessor хранят какие-либо данные TIn/TOut, потребуется случай 2. Кроме того, это мудрый дизайн, но случай 2 кажется чище, особенно когда вы добавляете к нему ограничения.   -  person Ian Mercer    schedule 11.09.2018
comment
@IanMercer - если вы не хотите хранить, скажем, List<IDataProcessor>, и каждый процессор будет работать с разными типами, в этом случае будет указан случай 1.   -  person Damien_The_Unbeliever    schedule 11.09.2018
comment
Спасибо за все ответы. Я думаю, что немного запутался между приведением типов и боксом/распаковкой. Теперь я немного прояснился. Мне известно, что бокс/распаковка включает в себя переход между стеком и кучей и наоборот, в то время как приведение типов может быть, а может и нет. Еще раз спасибо   -  person Ravi    schedule 11.09.2018
comment
Упаковка не обязательно подразумевает перемещение данных из стека в кучу. Если вы верите в ложь о том, что типы значений помещаются в стек, то вы принимаете решения на основе ошибочных рассуждений. Предположим, у вас есть int[] с миллионом целых чисел. Стек составляет всего миллион байтов, это массив из четырех миллионов байтов; как вы думаете, он попадает в стек, потому что в стек помещаются целые числа? Конечно, нет! То, что типы значений помещаются в стек, просто неверно. Переменные помещаются в стек, когда их время жизни невелико.   -  person Eric Lippert    schedule 11.09.2018
comment
Поскольку значение типа значения по определению живет непосредственно в его переменной, и мы знаем, что стек является пулом краткосрочного распределения, мы знаем, что типы значений перейти в стек, когда время жизни переменной короткое. Конечно, это предполагает, что пул краткосрочного распределения реализован с использованием стека. Это не обязательно; например, краткосрочные распределения могут быть зарегистрированы, а регистры не являются стеком. Когда переменная недолговечна? Когда это временное, локальное или формальное, которое не закрыто и не охватывает приостановку сопрограммы.   -  person Eric Lippert    schedule 11.09.2018
comment
Спасибо @EricLippert, это было ценно. Я не знал об этом, есть ли какие-нибудь книги, которые вы рекомендуете для лучшего понимания внутренностей?   -  person Ravi    schedule 11.09.2018
comment
Для языкового дизайна очень хорош C# in Depth; скоро выйдет четвертое издание. Essential C# посвящен не столько дизайну языка, сколько тому, как его эффективно использовать. Я редактировал оба. Стандартным справочником по внутренним компонентам является аннотированный справочник CLI Джима Миллера. Я никогда не читал CLR через C# Рихтера, но слышал от других, что это очень хорошо. И вы могли бы сделать хуже, чем читать мой блог. Я не пишу в нем в настоящее время, но надеюсь вернуться к нему в ближайшее время.   -  person Eric Lippert    schedule 11.09.2018


Ответы (1)


Первый метод более эффективен, чем второй (как минимум один мой ноутбук — см. код ниже). Тем не менее, это не очень важная часть здесь.

  1. Если универсальные типы TIn и TOut имеют какое-то семантическое значение для интерфейса или большинства методов интерфейса, вам следует включить общее описание в интерфейс.

  2. Если только один или несколько методов, определенных в интерфейсе, используют универсальные типы, то вместо этого в методе(ах) следует использовать универсальное описание.

Однако, поскольку вы специально спросили о том, что было более производительным, я набрал небольшой код и протестировал его. К моему удивлению, первый метод оказался более производительным.

Выполнение первого метода заняло 3669 миллисекунд, а метода 2 — 2715.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Text;
using System.Threading.Tasks;

namespace Sandbox
{
    public interface IDataProcesser
    {
        TOut Process<TIn, TOut>(TIn input);
    }

    public interface IDataProcesser2<TIn, TOut>
    {
        TOut Process(TIn input);
    }


    class Class1 : IDataProcesser
    {
        public TOut Process<Tin, TOut>(Tin input)
        {
            return default(TOut);
        }
    }

    class Class2 : IDataProcesser2<int, long>
    {
        public long Process(int input)
        {
            return default(long);
        }
    }

    class Program
    {
        private static int _loopCount = 1000000000;

        static void Main(string[] args)
        {
            var warmupEquals = false;
            var equals1 = false;
            var equals2 = false;

            for (long i = 0; i < _loopCount; i++)
            {
                Class1 warmup = new Class1();
                var w1 = warmup.Process<int, long>(default(int)) == 0;
                warmupEquals = w1;
            }

            var sw = new Stopwatch();
            sw.Start();
            for (long i = 0; i < _loopCount; i++)
            {
                Class1 c1 = new Class1();
                var t1 = c1.Process<int, long>(default(int)) == 0;
                if (t1)
                {
                    equals1 = true;
                }
            }

            sw.Stop();
            Console.WriteLine("Method 1");
            Console.WriteLine(sw.ElapsedMilliseconds);

            sw.Restart();
            sw.Start();
            for (long i = 0; i < _loopCount; i++)
            {
                Class2 c2 = new Class2();
                var t2 = c2.Process(default(int)) == 0;
                if (t2)
                {
                    equals2 = true;
                }
            }

            sw.Stop();
            Console.WriteLine("Method 2");
            Console.WriteLine(sw.ElapsedMilliseconds);
            Console.WriteLine(warmupEquals);
            Console.WriteLine(equals1);
            Console.WriteLine(equals2);
            Console.ReadLine();
        }
    }
}
person Andersnk    schedule 11.09.2018
comment
Большое спасибо, что нашли время и закодировали его, очень ценю это. Как вы думаете, производительность класса 2 улучшится, если вы также разогреете? Поскольку вы прогрели Class1, будет ли процессор кэшировать инструкции во время прогрева? - person Ravi; 11.09.2018
comment
Нет, я не думаю, что это повлияет. Я был бы слегка удивлен, если бы это имело значение, но вы можете легко протестировать это, запустив код на своей собственной машине. В конце концов - железо имеет значение :) - person Andersnk; 11.09.2018
comment
Где в своем тесте вы учитывали тот факт, что вы выделяете объекты и взимаете плату за выделение за один временной интервал, но освобождаете эти объекты, возможно, через другой временной интервал? В вашем первом интервале секундомера может быть ноль коллекций, а затем во втором интервале секундомера сборщик останавливает мир и собирает все объекты, выделенные в первом интервале, сбивая ваши тайминги. Получить правильную атрибуцию времени с помощью секундомера довольно сложно. - person Eric Lippert; 11.09.2018