Я просто пересматриваю четвертую главу C # in Depth, которая посвящена обнуляемым типам, и добавляю раздел об использовании оператора «as», который позволяет писать:
object o = ...;
int? x = o as int?;
if (x.HasValue)
{
... // Use x.Value in here
}
Я подумал, что это действительно здорово, и что это может улучшить производительность по сравнению с эквивалентом C # 1, используя «is» с последующим приведением - в конце концов, таким образом, нам нужно только один раз запросить динамическую проверку типа, а затем простую проверку значения ,
Однако, похоже, это не так. Ниже я включил пример тестового приложения, которое в основном суммирует все целые числа в массиве объектов, но массив содержит множество пустых ссылок и ссылок на строки, а также целые числа в штучной упаковке. Тест измеряет код, который вы должны использовать в C # 1, код, использующий оператор «как», и просто для решения LINQ. К моему удивлению, код C # 1 в этом случае работает в 20 раз быстрее, и даже код LINQ (который я бы ожидал сделать медленнее, учитывая задействованные итераторы) превосходит код «как».
Реализована ли реализация .NET isinst
для обнуляемых типов просто медленной? Это дополнительныйunbox.any
которое вызывает проблему? Есть ли другое объяснение этому? На данный момент мне кажется, что мне придется включить предупреждение против использования этого в ситуациях, чувствительных к производительности ...
Полученные результаты:
В ролях: 10000000: 121
Как: 10000000: 2211
LINQ: 10000000: 2143
Код:
using System;
using System.Diagnostics;
using System.Linq;
class Test
{
const int Size = 30000000;
static void Main()
{
object[] values = new object[Size];
for (int i = 0; i < Size - 2; i += 3)
{
values[i] = null;
values[i+1] = "";
values[i+2] = 1;
}
FindSumWithCast(values);
FindSumWithAs(values);
FindSumWithLinq(values);
}
static void FindSumWithCast(object[] values)
{
Stopwatch sw = Stopwatch.StartNew();
int sum = 0;
foreach (object o in values)
{
if (o is int)
{
int x = (int) o;
sum += x;
}
}
sw.Stop();
Console.WriteLine("Cast: {0} : {1}", sum,
(long) sw.ElapsedMilliseconds);
}
static void FindSumWithAs(object[] values)
{
Stopwatch sw = Stopwatch.StartNew();
int sum = 0;
foreach (object o in values)
{
int? x = o as int?;
if (x.HasValue)
{
sum += x.Value;
}
}
sw.Stop();
Console.WriteLine("As: {0} : {1}", sum,
(long) sw.ElapsedMilliseconds);
}
static void FindSumWithLinq(object[] values)
{
Stopwatch sw = Stopwatch.StartNew();
int sum = values.OfType<int>().Sum();
sw.Stop();
Console.WriteLine("LINQ: {0} : {1}", sum,
(long) sw.ElapsedMilliseconds);
}
}
as
на обнуляемых типов. Интересно, так как его нельзя использовать для других типов значений. На самом деле, более удивительно.as
попытайтесь привести к типу, и если он потерпит неудачу, он вернет ноль. Вы не можете установить типы значений в nullОтветы:
Ясно, что машинный код, который JIT-компилятор может сгенерировать для первого случая, гораздо эффективнее. Здесь действительно помогает одно правило: распаковывать объект можно только в переменную того же типа, что и значение в штучной упаковке. Это позволяет JIT-компилятору генерировать очень эффективный код, не нужно рассматривать преобразование значений.
является тест оператора легко, просто проверить , если объект не является нулевым и ожидаемым типа, занимает всего лишь несколько инструкций машинного кода. Приведение также легко, JIT-компилятор знает расположение битов значения в объекте и использует их напрямую. Никакого копирования или преобразования не происходит, весь машинный код является встроенным и занимает всего около десятка инструкций. Это должно было быть действительно эффективным в .NET 1.0, когда бокс был обычным делом.
Приведение к int? занимает намного больше работы. Представление значения целого в штучной упаковке не совместимо с макетом памяти
Nullable<int>
. Требуется преобразование, и код сложен из-за возможных типов перечисленных в штучной упаковке. JIT-компилятор генерирует вызов вспомогательной функции CLR с именем JIT_Unbox_Nullable, чтобы выполнить работу. Это функция общего назначения для любого типа значения, там много кода для проверки типов. И значение копируется. Трудно оценить стоимость, так как этот код заблокирован внутри mscorwks.dll, но, вероятно, сотни инструкций машинного кода.Метод расширения Linq OfType () также использует оператор is и приведение типов . Это, однако, приведение к универсальному типу. JIT-компилятор генерирует вызов вспомогательной функции JIT_Unbox (), которая может выполнять приведение к произвольному типу значения. У меня нет отличного объяснения, почему это так медленно, как приведение
Nullable<int>
, учитывая, что меньше работы должно быть необходимо. Я подозреваю, что ngen.exe может вызвать проблемы здесь.источник
Мне кажется, что
isinst
это просто очень медленно для обнуляемых типов. В методеFindSumWithCast
я поменялв
что также значительно замедляет выполнение. Единственное различие в IL, которое я вижу, состоит в том, что
меняется на
источник
isinst
следует проверка на ничтожность, а затем условно anunbox.any
. В обнуляемом случае есть безусловноеunbox.any
.isinst
иunbox.any
медленнее на обнуляемых типах.Первоначально это началось как Комментарий к превосходному ответу Ханса Пассанта, но это слишком долго, поэтому я хочу добавить несколько слов здесь:
Во-первых,
as
оператор C # будет выдаватьisinst
инструкцию IL (как иis
оператор). (Еще одна интересная инструкция -castclass
генерируется, когда вы выполняете прямое приведение, и компилятор знает, что проверка во время выполнения не может быть опущена.)Вот что
isinst
делает ( ECMA 335, Раздел III, 4.6 ):Самое главное:
Таким образом, убийца производительности не
isinst
в этом случае, а в дополнительномunbox.any
. Это не было ясно из ответа Ханса, поскольку он смотрел только на код JITed. В общем, компилятор C # выдастunbox.any
после aisinst T?
(но пропустит его, если вы это сделаетеisinst T
, когдаT
это ссылочный тип).Почему это так?
isinst T?
никогда не имеет эффекта , который был бы очевиден, то вы получите обратноT?
. Вместо этого все эти инструкции гарантируют, что у вас есть,"boxed T"
который можно распаковатьT?
. Для того, чтобы получить действительноеT?
, мы все еще должны распаковывать наши"boxed T"
кT?
, поэтому компилятор выдаетunbox.any
после того, какisinst
. Если вы думаете об этом, это имеет смысл, потому что «формат коробки» для «T?
просто»,"boxed T"
а созданиеcastclass
иisinst
выполнение распаковки было бы несовместимым.Подкрепляя находку Ганса некоторой информацией из стандарта , можно сказать:
(ECMA 335, раздел III, 4.33):
unbox.any
(ECMA 335, раздел III, 4.32):
unbox
источник
Интересно, что я передал отзыв о поддержке оператора, так как
dynamic
он был на порядок медленнееNullable<T>
(аналогично этому раннему тесту ) - подозреваю, по очень похожим причинам.Должен любить
Nullable<T>
. Еще один интересный момент: несмотря на то, что JIT обнаруживает (и удаляет)null
неструктурируемые структуры, он скрываетNullable<T>
:источник
null
для не допускающих обнуление структур»? Вы имеете в виду, что он заменяетnull
значение по умолчанию или что-то во время выполнения?T
т. Д.). Требования к стеку и т. Д. Зависят от аргументов (количество стекового пространства для локального и т. Д.), Поэтому вы получаете один JIT для любой уникальной перестановки, включающей тип значения. Тем не менее, ссылки имеют одинаковый размер, поэтому используйте JIT. Выполняя JIT для каждого значения, он может проверить несколько очевидных сценариев и попытаться вырезать недоступный код из-за таких вещей, как невозможные нули. Это не идеально, заметьте. Кроме того, я игнорирую AOT для вышеупомянутого.count
переменную. ДобавлениеConsole.Write(count.ToString()+" ");
послеwatch.Stop();
в обоих случаях замедляет другие тесты почти на порядок, но неограниченный обнуляемый тест не изменяется. Обратите внимание, что есть также изменения при тестировании случаев, когдаnull
он пройден, подтверждая, что исходный код на самом деле не выполняет нулевую проверку и приращение для других тестов. LinqpadЭто результат FindSumWithAsAndHas выше:
Это результат FindSumWithCast:
Выводы:
Используя
as
, он сначала проверяет, является ли объект экземпляром Int32; под капотом он используетisinst Int32
(что похоже на рукописный код: if (o is int)). И используяas
, это также безоговорочно распаковывает объект. И это настоящий убийца производительности для вызова свойства (это все еще функция под капотом), IL_0027Используя приведение, вы сначала проверяете, является ли объект
int
if (o is int)
; под капотом это используетisinst Int32
. Если это экземпляр типа int, вы можете безопасно распаковать значение IL_002DПроще говоря, это псевдокод использования
as
подхода:И это псевдокод использования приведения:
Таким образом, приведение (
(int)a[i]
ну, синтаксис выглядит как приведение, но на самом деле это распаковка, приведение и распаковка с одинаковым синтаксисом, в следующий раз, когда я буду педантичен с правильной терминологией), действительно быстрее, вам нужно только распаковать значение когда объект определенно являетсяint
. То же самое нельзя сказать, используяas
подход.источник
Чтобы этот ответ был актуальным, стоит упомянуть, что большая часть обсуждения на этой странице теперь спорна с C # 7.1 и .NET 4.7, которые поддерживают тонкий синтаксис, который также производит лучший код IL.
Оригинальный пример ОП ...
становится просто ...
Я обнаружил, что одним из распространенных применений нового синтаксиса является ситуация, когда вы пишете тип значения .NET (то есть
struct
в C # ), который реализуетIEquatable<MyStruct>
(как это следует делать большинству). После реализации строго типизированногоEquals(MyStruct other)
метода вы можете теперь изящно перенаправить нетипизированноеEquals(Object obj)
переопределение (унаследованное отObject
) к нему следующим образом:Приложение:
Release
сборки IL - код для первых двух примеров функций , приведенных выше в этом ответе (соответственно) приведено здесь. Несмотря на то, что код IL для нового синтаксиса действительно на 1 байт меньше, он в основном выигрывает, делая нулевые вызовы (против двух) иunbox
вообще избегая операции, когда это возможно.Дальнейшее тестирование, которое подтверждает мое замечание о производительности нового синтаксиса C # 7, превосходящего ранее доступные параметры, см. Здесь (в частности, пример «D»).
источник
Профилирование дальше:
Вывод:
Что мы можем сделать из этих цифр?
источник
У меня нет времени, чтобы попробовать это, но вы можете иметь:
так как
Каждый раз вы создаете новый объект, который не полностью объясняет проблему, но может внести свой вклад.
источник
int?
в стеке с помощьюunbox.any
. Я подозреваю, что это проблема - я предполагаю, что созданный вручную IL может побить оба варианта здесь ... хотя также возможно, что JIT оптимизирован для распознавания для случая is / cast и проверяется только один раз.Я попробовал точный тип проверки конструкции
typeof(int) == item.GetType()
, которая работает так же быстро, как иitem is int
версия, и всегда возвращает число (выделение: даже если вы записали aNullable<int>
в массив, вам нужно будет использоватьtypeof(int)
). Вам также нужна дополнительнаяnull != item
проверка здесь.тем не мение
typeof(int?) == item.GetType()
остается быстрым (в отличие отitem is int?
), но всегда возвращает false.Typeof-construct - это, на мой взгляд, самый быстрый способ точной проверки типов, поскольку он использует RuntimeTypeHandle. Поскольку точные типы в этом случае не совпадают с nullable, я предполагаю,
is/as
что здесь нужно выполнить дополнительное усиление , чтобы убедиться, что это на самом деле экземпляр типа Nullable.И честно: что ты
is Nullable<xxx> plus HasValue
покупаешь? Ничего. Вы всегда можете перейти непосредственно к базовому типу (значению) (в данном случае). Вы либо получаете значение, либо «нет, не экземпляр того типа, который вы запрашивали». Даже если вы записали(int?)null
в массив, проверка типа вернет false.источник
int?
- если вы укажетеint?
значение, оно в конечном итоге будет заключено в рамку как int илиnull
ссылка.Выходы:
[РЕДАКТИРОВАТЬ: 2010-06-19]
Примечание. Предыдущий тест проводился внутри VS, отладка конфигурации с использованием VS2009, с использованием Core i7 (машина разработки компании).
Следующее было сделано на моей машине с использованием Core 2 Duo с использованием VS2010
источник