2016-07-21 3 views
1

Рассмотрим следующий C# код:С # дженерики производительности против интерфейса

interface IFace 
{ 
    void Do(); 
} 

class Foo: IFace 
{ 
    public void Do() { /* some action */ } 
} 

class Bar 
{ 
    public void A(Foo foo) 
    { 
     foo.Do(); 
    } 

    public void B<T>(T foo) 
     where T: IFace 
    { 
     foo.Do(); 
    } 

    public void C(IFace foo) 
    { 
     foo.Do(); 
    } 

    public void D<T>(T foo) 
     where T: class, IFace 
    { 
     foo.Do(); 
    } 
} 

со следующим использованием:

Foo foo = new Foo(); 
Bar bar = new Bar(); 

MeasureExecutionTime(() => bar.A(foo), "A"); 
MeasureExecutionTime(() => bar.B(foo), "B"); 
MeasureExecutionTime(() => bar.C(foo), "C"); 
MeasureExecutionTime(() => bar.D(foo), "D"); 

Результаты (VS2015, .NET 4.5.2) являются:

A: 3,00 ns/op, 333,4 m/c

B: 5,74 ns/op, 174,3 mop/s

C: 5,55 нс/оп, 180,3 швабра/s

D: 5,64 нс/оп, 177,4 швабра/s

Я хочу знать, почему использование общий метод B не имеет никакого преимущества перед использованием интерфейса в режимах x86 и x64 (например, шаблоны C++ и виртуальные вызовы). Общий метод даже немного медленнее, чем не общий метод на основе интерфейса (этот эффект является устойчивым и остается при замене измерений B и C).

Приложение: Код MeasureExecutionTime можно найти здесь: https://gist.github.com/anonymous/9d60f5d09868ed3a00ec00f413f6afb0

Обновление: Я проверил код на Mono, результаты следующие:

андрей @ Ubuntu-NAS:/данные/моно/JSON/64 $ моно Test.exe

А: 3.40 нс/оп, 294,0 швабры/с

В: 3.40 нс/оп, 293,7 швабры/с

С: 6.80 нс/оп, 147,1 шваброй/с

Д: 3.40 нс/оп, 294,2 швабры/с

Сгенерированный IL-код может быть найден здесь: https://gist.github.com/anonymous/58df84eda906e83c64ce1b4fdc5497fb

MS и Mono генерируют один и тот же IL-код, за исключением метода D. Тем не менее, он не может объяснить разницу для метода B. Если я буду запускать MS-сгенерированный код Mono без перекомпиляции, результаты для метода D будут такими же, как для B.

+0

Почему * должен * быть какая-то разница? Общая реализация имеет те же условия, что и ваш интерфейсный подход, а именно, что оба экземпляра, предоставленные в качестве аргумента *, имеют * тип 'IFace'. Что означает 'где T: IFace' дает вам то, что' (IFace foo) 'doesn't? – HimBromBeere

+0

Дженерики не похожи на шаблоны C++; Они не компилируются в их используемые формы во время компиляции. –

+0

Потому что я ожидаю, что 'T' будет конкретным классом' Foo' и 'IFace', чтобы быть контрактом. Почему CLR, который знает тип во время выполнения, не генерирует эффективный код? –

ответ

5

Я хочу знать, почему использование общего метода B не имеет никакого преимущества перед использованием интерфейса в режимах x86 и x64 (например, шаблоны C++ или виртуальные вызовы).

CLR generics не являются шаблонами на C++.

Шаблоны - это в основном механизм поиска и замены; если у вас есть десять экземпляров шаблона, то генерируются десять копий исходного кода и все скомпилированы и оптимизированы. Это ускоряет оптимизацию во время компиляции с увеличением времени компиляции и увеличением двоичного размера.

Generics, напротив, скомпилированы один раз в IL с помощью компилятора C#, а затем генерируется код для каждого экземпляра родового джиттера. Однако, как деталь реализации, все экземпляры, которые предоставляют ссылочные типы для аргументов типа, используют один и тот же сгенерированный код. Поэтому, если у вас есть метод C<T>.M(T t), и он вызывается с тем, что T является как строкой, так и IList, тогда генерируется код x86 (или любой другой) после и используется для обоих случаев.

Таким образом, не происходит никакого штрафа, налагаемого вызовами виртуальной функции или вызовами интерфейса. (Которые используют схожие, но несколько иные механизмы.) Если, скажем, T.ToString() вызывается внутри метода, то дрожание не говорит «о, я знаю, что если T является строкой, то ToString - это тождество, я вернусь к виртуальной функции звоните ", или стройте тело, или любую вещь.

Эта оптимизация отменяет уменьшенное время jit и меньшее использование памяти для более медленных вызовов.

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

+0

Это дает мне понять. Теперь я считаю, что наблюдаемый эффект был вызван только тем, что был сделан Mono JIT. –

0

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

+0

Я хочу запустить дорогостоящий код CPU на сервере (сравнение и сравнение больших структур) и в 2-3 раза медленнее производительность с дженериками и интерфейсами. –

+0

Да, прямая печать быстрее, но я имел в виду разницу между общей версией и версией интерфейса, так как я предположил, что ваш вопрос больше связан с этой частью. –

+0

Вопрос в том, почему CLR не генерирует эффективный код из IL при знании конкретных типов. –

0

Если вы скомпилируете и посмотрите на IL, вы увидите, что общая версия точно такая же, как версия универсального интерфейса, с первой проверкой ограничения типа, что делает ее немного медленнее, хотя разница вероятна пренебрежимо малый в реальном коде. Даже виртуальный вызов интерфейса имеет гораздо большую разницу, и чистый код обычно будет намного более важным, чем наносекунда здесь или там.

https://msdn.microsoft.com/en-us/library/system.reflection.emit.opcodes.constrained(v=vs.110).aspx

Bar.A: 
IL_0000: ldarg.1  
IL_0001: callvirt UserQuery+Foo.Do 
IL_0006: ret   

Bar.B: 
IL_0000: ldarga.s 01 
IL_0002: constrained. 01 00 00 1B 
IL_0008: callvirt UserQuery+IFace.Do 
IL_000D: ret   

Bar.C: 
IL_0000: ldarg.1  
IL_0001: callvirt UserQuery+IFace.Do 
IL_0006: ret 

Дженерики в .net не то же самое, что шаблоны в C++.

+0

Я просмотрел сгенерированный код сборки и попытался запустить программу на Mono. Кажется, проблема в том, что Microsoft CLR не выполняет инкрустировку, даже если установлен метод MethodImplOptions.AggressiveInlining. –

+0

Пожалуйста, проверьте обновленный вопрос с большим количеством результатов. –

+0

«Ограниченный» на самом деле не является проверкой времени исполнения; это подсказка для JIT, которая помогает оптимизировать вызов (в некоторых случаях callvirt может быть заменен вызовом), особенно при использовании типов значений. Поскольку JIT один раз для всех ссылочных типов и один раз для T типов значений, * check * не должен иметь никакого влияния - особенно, поскольку T является ссылочным типом в этом случае, поэтому ограниченный вызов фактически станет обычный callvirt –

Смежные вопросы