Сюрприз производительности с типами " as " и nullable
Я просто пересматриваю Главу 4 C#, в которой подробно рассматриваются типы nullable, и я добавляю раздел об использовании оператора "as", который позволяет вам писать:
object o = ...;
int? x = o as int?;
if (x.HasValue)
{
... // Use x.Value in here
}
Я думал, что это было действительно аккуратно, и что это может улучшить производительность по сравнению с эквивалентом C# 1, используя "is" с последующим приведением - в конце концов, таким образом, нам нужно только попросить динамическую проверку типа один раз, а затем простую проверку значения.
это, кажется, не так, однако. Я ниже приведен пример тестового приложения, который в основном суммирует все целые числа в массиве объектов, но массив содержит много пустых ссылок и ссылок на строки, а также целые числа в коробках. Тест измеряет код, который вам нужно будет использовать в C# 1, код, использующий оператор "as", и просто для пинки решение LINQ. К моему удивлению, код C# 1 в этом случае в 20 раз быстрее - и даже код LINQ (который, как я ожидал, будет медленнее, учитывая задействованные итераторы) бьет " as" код.
является реализация .NET isinst
для nullable типов просто очень медленно? Это дополнительный unbox.any
что вызывает проблему? Есть ли другое объяснение этому? На данный момент кажется, что мне придется включить предупреждение против использования этого в ситуациях, чувствительных к производительности...
результаты:
Cast: 10000000: 121
As: 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);
}
}
10 ответов:
The и тест оператора прост, просто проверьте, не является ли объект нулевым и имеет ожидаемый тип, занимает всего несколько машин закодированные инструкции. Приведение также легко, JIT-компилятор знает расположение битов значений в объекте и использует их напрямую. Никакого копирования или преобразования не происходит, весь машинный код встроен и занимает всего около десятка инструкций. Это должно было быть действительно эффективным в .NET 1.0, когда бокс был распространен.
приведение к int? это требует гораздо больше работы. Представление значения коробочного целого числа не совместимо с макетом памяти
Nullable<int>
. Требуется преобразование и код является сложным из-за возможных коробочных типов перечисления. JIT-компилятор создает вызов вспомогательной функции CLR с именем JIT_Unbox_Nullable для выполнения задания. Это функция общего назначения для любого типа значения, там много кода для проверки типов. И значение копируется. Трудно оценить стоимость, так как этот код заблокирован внутри mscorwks.dll, но сотни инструкций машинного кода, вероятно.метод расширения Linq OfType () также использует и оператор и актерский состав. Однако это приведение к универсальному типу. Компилятор JIT генерирует вызов вспомогательной функции JIT_Unbox (), которая может выполнять приведение к произвольному типу значения. У меня нет большого объяснения, почему это так медленно, как бросок в
Nullable<int>
, учитывая, что меньше работы должно быть необходимо. Я подозреваю, что нген.exe может вызвать проблемы здесь.
мне кажется, что
isinst
просто очень медленно на nullable типов. В методеFindSumWithCast
сменилif (o is int)
до
if (o is int?)
что также значительно замедляет выполнение. Единственное отличие в IL я вижу, что
isinst [mscorlib]System.Int32
меняется на
isinst valuetype [mscorlib]System.Nullable`1<int32>
это первоначально началось как комментарий к превосходному ответу Ханса Пассанта, но он стал слишком длинным, поэтому я хочу добавить несколько битов здесь:
во-первых, C#
as
оператор выдаетisinst
Il инструкция (так же как иis
оператор). (Еще одна интересная инструкцияcastclass
, испускаемому, когда вы сделать прямой бросок, и компилятор знает, что проверка выполнения не может быть опущен.)вот что
isinst
у (ECMA 335 раздел III, 4.6):: isinsttypeToktypeTok - это маркер метаданных (a
typeref
,typedef
илиtypespec
), указывая нужный класс.если typeTok это необнуляемый тип значений или параметр универсального типа он интерпретируется как "коробочные" typeTok.
если typeTok является типом, поддерживающим значение null,
Nullable<T>
, это интерпретируется как "в штучной упаковке"T
самое главное:
если фактический тип (не тип отслеживаемого верификатора) obj и verifier-assignable-to тип typeTok тогда
isinst
успешно и obj (как результат) возвращается без изменений, в то время как проверка отслеживает его тип как typeTok. в отличие от принуждения (§1.6) и преобразования (§3.27),isinst
никогда не изменяет фактический тип объекта и сохраняет идентичность объекта (см. Раздел I).Итак, убийца производительности не
isinst
в данном случае, но дополнительныйunbox.any
. Это было неясно из ответа Ганса, поскольку он смотрел только на JITed-код. В общем случае компилятор C# будет выдаватьunbox.any
после aisinst T?
(но опустит его в случае, если вы делаетеisinst T
, когдаT
является ссылочным типом).почему он это делает?
isinst T?
никогда не имеет эффекта, что было бы очевидно, т. е. вы получите обратно aT?
. Вместо этого все эти инструкции гарантируют, что у вас есть"boxed T"
это можно распаковать вT?
. Чтобы получить фактическийT?
, нам все еще нужно распаковать наш"boxed T"
доT?
, поэтому компилятор выдаетunbox.any
послеisinst
. Если вы думаете об этом, это имеет смысл, поскольку "формат" дляT?
это просто"boxed T"
и что делаетcastclass
иisinst
выполнить распаковку было бы непоследовательно.поддержка вверх по находке Ганса с некоторой информацией от стандартный здесь:
(ECMA 335 раздел III, 4.33):
unbox.any
при применении к коробчатой форме типа значения
unbox.any
инструкция извлекает значение, содержащееся в obj (типаO
). (Это эквивалентноunbox
следовал поldobj
.) При применении к ссылочному типу,unbox.any
инструкция имеет тот же эффект, что иcastclass
typeTok.(ECMA 335 раздел III, 4.32):
unbox
как правило,
unbox
просто вычисляет адрес типа значения, который уже присутствует внутри упакованного объекта. Такой подход невозможен при распаковке типов значений с нулевым значением. Потому чтоNullable<T>
значения преобразуются в boxedTs
во время работы коробки реализация часто должна производить новыйNullable<T>
в куче и вычислить адрес для вновь выделенного объект.
интересно, что я передал отзыв о поддержке оператора через
dynamic
будучи на порядок медленнее дляNullable<T>
(аналог этот тест) - подозреваю по очень похожим причинам.любовь
Nullable<T>
. Еще одна забава заключается в том, что даже если JIT пятна (и удаляет)null
для ненулевых структур он борется заNullable<T>
:using System; using System.Diagnostics; static class Program { static void Main() { // JIT TestUnrestricted<int>(1,5); TestUnrestricted<string>("abc",5); TestUnrestricted<int?>(1,5); TestNullable<int>(1, 5); const int LOOP = 100000000; Console.WriteLine(TestUnrestricted<int>(1, LOOP)); Console.WriteLine(TestUnrestricted<string>("abc", LOOP)); Console.WriteLine(TestUnrestricted<int?>(1, LOOP)); Console.WriteLine(TestNullable<int>(1, LOOP)); } static long TestUnrestricted<T>(T x, int loop) { Stopwatch watch = Stopwatch.StartNew(); int count = 0; for (int i = 0; i < loop; i++) { if (x != null) count++; } watch.Stop(); return watch.ElapsedMilliseconds; } static long TestNullable<T>(T? x, int loop) where T : struct { Stopwatch watch = Stopwatch.StartNew(); int count = 0; for (int i = 0; i < loop; i++) { if (x != null) count++; } watch.Stop(); return watch.ElapsedMilliseconds; } }
это результат FindSumWithAsAndHas выше: alt текст http://www.freeimagehosting.net/uploads/9e3c0bfb75.png
это результат FindSumWithCast: alt текст http://www.freeimagehosting.net/uploads/ce8a5a3934.png
выводы:
используя
as
, Он сначала проверяет, является ли объект экземпляром Int32; под капотом он используетisinst Int32
(который похож на рукописный код: if (o имеет тип int) ). И используяas
, Он также Безоговорочно распаковать объект. И это настоящий убийца производительности для вызова свойства (это все еще функция под капотом), IL_0027используя cast, вы сначала проверяете, является ли объект
int
if (o is int)
; под капотом это с помощьюisinst Int32
. Если это экземпляр int, то вы можете безопасно распаковать значение, IL_002Dпроще говоря, это псевдо-код, используя
as
подход:int? x; (x.HasValue, x.Value) = (o isinst Int32, o unbox Int32) if (x.HasValue) sum += x.Value;
и это псевдо-код использования cast подхода:
if (o isinst Int32) sum += (o unbox Int32)
Итак, чугун (
(int)a[i]
, ну синтаксис выглядит как приведение, но на самом деле распаковка, приведение и распаковка имеют один и тот же синтаксис, в следующий раз я буду педантичен с правильной терминологией) подход действительно быстрее, вам нужно было только распаковать значение, когда объект решительноint
. То же самое нельзя сказать об использованииas
подход.
далее профилирования:
using System; using System.Diagnostics; class Program { const int Size = 30000000; static void Main(string[] args) { object[] values = new object[Size]; for (int i = 0; i < Size - 2; i += 3) { values[i] = null; values[i + 1] = ""; values[i + 2] = 1; } FindSumWithIsThenCast(values); FindSumWithAsThenHasThenValue(values); FindSumWithAsThenHasThenCast(values); FindSumWithManualAs(values); FindSumWithAsThenManualHasThenValue(values); Console.ReadLine(); } static void FindSumWithIsThenCast(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("Is then Cast: {0} : {1}", sum, (long)sw.ElapsedMilliseconds); } static void FindSumWithAsThenHasThenValue(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 then Has then Value: {0} : {1}", sum, (long)sw.ElapsedMilliseconds); } static void FindSumWithAsThenHasThenCast(object[] values) { Stopwatch sw = Stopwatch.StartNew(); int sum = 0; foreach (object o in values) { int? x = o as int?; if (x.HasValue) { sum += (int)o; } } sw.Stop(); Console.WriteLine("As then Has then Cast: {0} : {1}", sum, (long)sw.ElapsedMilliseconds); } static void FindSumWithManualAs(object[] values) { Stopwatch sw = Stopwatch.StartNew(); int sum = 0; foreach (object o in values) { bool hasValue = o is int; int x = hasValue ? (int)o : 0; if (hasValue) { sum += x; } } sw.Stop(); Console.WriteLine("Manual As: {0} : {1}", sum, (long)sw.ElapsedMilliseconds); } static void FindSumWithAsThenManualHasThenValue(object[] values) { Stopwatch sw = Stopwatch.StartNew(); int sum = 0; foreach (object o in values) { int? x = o as int?; if (o is int) { sum += x.Value; } } sw.Stop(); Console.WriteLine("As then Manual Has then Value: {0} : {1}", sum, (long)sw.ElapsedMilliseconds); } }
выход:
Is then Cast: 10000000 : 303 As then Has then Value: 10000000 : 3524 As then Has then Cast: 10000000 : 3272 Manual As: 10000000 : 395 As then Manual Has then Value: 10000000 : 3282
что мы можем сделать из этих цифр?
- во-первых, is-then-cast подход значительно быстрее, чем как подход. 303 против 3524
- во-вторых, .Значение немного медленнее, чем литье. 3524 против 3272
- в-третьих, .HasValue немного медленнее, чем при использовании руководства (т. е. с помощью и). 3524 против 3282
- в-четвертых, делать устройствами Apple сравнение(т. е. назначение условного свойство hasvalue и преобразование имитируемых значений происходит вместе) между моделируется и реально, как подходим, видно моделируется по-прежнему значительно быстрее, чем реально, как. 395 против 3524
- наконец, основываясь на первом и четвертом выводе, что-то не так с как реализация ^_^
у меня нет времени, чтобы попробовать его, но вы можете хотеть иметь:
foreach (object o in values) { int? x = o as int?;
как
int? x; foreach (object o in values) { x = o as int?;
вы создаете новый объект каждый раз, которые не объяснить проблему, но может внести свой вклад.
я попробовал точную конструкцию проверки типа
typeof(int) == item.GetType()
, которая выполняет так же быстро, какitem is int
версия, и всегда возвращает число (внимание: даже если вы написалиNullable<int>
для массива, вам нужно будет использоватьtypeof(int)
). Вам также нужен дополнительныйnull != item
регистрация здесь.
typeof(int?) == item.GetType()
остается быстрым (в отличие отitem is int?
), но всегда возвращает false.typeof-construct в моих глазах самый быстрый способ для точно проверка типа, так как он использует RuntimeTypeHandle. Поскольку точные типы в этом случае не совпадают с nullable, я предполагаю,
is/as
здесь нужно сделать дополнительный heavylifting, чтобы убедиться, что это на самом деле экземпляр типа Nullable.и честно: что делает ваш
is Nullable<xxx> plus HasValue
купить можно? Ничего. Вы всегда можете перейти непосредственно к базовому типу (value) (в этом случае). Вы либо получаете значение, либо "нет, не экземпляр типа, который вы просили". Даже если вы написали(int?)null
для массива проверка типа вернет false.
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); FindSumWithAsAndHas(values); FindSumWithAsAndIs(values); FindSumWithIsThenAs(values); FindSumWithIsThenConvert(values); FindSumWithLinq(values); Console.ReadLine(); } 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 FindSumWithAsAndHas(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 and Has: {0} : {1}", sum, (long)sw.ElapsedMilliseconds); } static void FindSumWithAsAndIs(object[] values) { Stopwatch sw = Stopwatch.StartNew(); int sum = 0; foreach (object o in values) { int? x = o as int?; if (o is int) { sum += x.Value; } } sw.Stop(); Console.WriteLine("As and Is: {0} : {1}", sum, (long)sw.ElapsedMilliseconds); } static void FindSumWithIsThenAs(object[] values) { // Apple-to-apple comparison with Cast routine above. // Using the similar steps in Cast routine above, // the AS here cannot be slower than Linq. Stopwatch sw = Stopwatch.StartNew(); int sum = 0; foreach (object o in values) { if (o is int) { int? x = o as int?; sum += x.Value; } } sw.Stop(); Console.WriteLine("Is then As: {0} : {1}", sum, (long)sw.ElapsedMilliseconds); } static void FindSumWithIsThenConvert(object[] values) { Stopwatch sw = Stopwatch.StartNew(); int sum = 0; foreach (object o in values) { if (o is int) { int x = Convert.ToInt32(o); sum += x; } } sw.Stop(); Console.WriteLine("Is then Convert: {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); } }
выходы:
Cast: 10000000 : 456 As and Has: 10000000 : 2103 As and Is: 10000000 : 2029 Is then As: 10000000 : 1376 Is then Convert: 10000000 : 566 LINQ: 10000000 : 1811
[EDIT: 2010-06-19]
Примечание: предыдущий тест был сделан внутри VS, отладка конфигурации, используя VS2009, используя Core i7 (машина разработки компании).
следующее было сделано на моей машине с помощью Core 2 Duo, используя VS2010
Inside VS, Configuration: Debug Cast: 10000000 : 309 As and Has: 10000000 : 3322 As and Is: 10000000 : 3249 Is then As: 10000000 : 1926 Is then Convert: 10000000 : 410 LINQ: 10000000 : 2018 Outside VS, Configuration: Debug Cast: 10000000 : 303 As and Has: 10000000 : 3314 As and Is: 10000000 : 3230 Is then As: 10000000 : 1942 Is then Convert: 10000000 : 418 LINQ: 10000000 : 1944 Inside VS, Configuration: Release Cast: 10000000 : 305 As and Has: 10000000 : 3327 As and Is: 10000000 : 3265 Is then As: 10000000 : 1942 Is then Convert: 10000000 : 414 LINQ: 10000000 : 1932 Outside VS, Configuration: Release Cast: 10000000 : 301 As and Has: 10000000 : 3274 As and Is: 10000000 : 3240 Is then As: 10000000 : 1904 Is then Convert: 10000000 : 414 LINQ: 10000000 : 1936
чтобы сохранить этот ответ в актуальном состоянии, стоит отметить, что большая часть обсуждения на этой странице теперь спорна с C# 7.1 и .NET 4.7 который поддерживает тонкий синтаксис, который также создает лучший код IL.
исходный пример OP...
object o = ...; int? x = o as int?; if (x.HasValue) { // ...use x.Value in here }
становится просто...
if (o is int x) { // ...use x in here }
я обнаружил, что одно общее использование для нового синтаксиса - это когда вы пишете .NET тип значения (т. е.
struct
на C#), что реализуетIEquatable<MyStruct>
(как большинство). После реализации строго типизированногоEquals(MyStruct other)
метод, теперь вы можете изящно перенаправить нетипизированныйEquals(Object obj)
override (наследуется отObject
) к нему следующим образом:public override bool Equals(Object obj) => obj is MyStruct o && Equals(o);
приложение: The
Release
построить IL здесь приведен код для первых двух примеров функций, показанных выше в этом ответе (соответственно). В то время как код IL для новый синтаксис действительно на 1 байт меньше, он в основном выигрывает большой, делая нулевые вызовы (против двух) и избегаяunbox
операции в целом, когда это возможно.// static void test1(Object o, ref int y) // { // int? x = o as int?; // if (x.HasValue) // y = x.Value; // } [0] valuetype [mscorlib]Nullable`1<int32> x ldarg.0 isinst [mscorlib]Nullable`1<int32> unbox.any [mscorlib]Nullable`1<int32> stloc.0 ldloca.s x call instance bool [mscorlib]Nullable`1<int32>::get_HasValue() brfalse.s L_001e ldarg.1 ldloca.s x call instance !0 [mscorlib]Nullable`1<int32>::get_Value() stind.i4 L_001e: ret
// static void test2(Object o, ref int y) // { // if (o is int x) // y = x; // } [0] int32 x, [1] object obj2 ldarg.0 stloc.1 ldloc.1 isinst int32 ldnull cgt.un dup brtrue.s L_0011 ldc.i4.0 br.s L_0017 L_0011: ldloc.1 unbox.any int32 L_0017: stloc.0 brfalse.s L_001d ldarg.1 ldloc.0 stind.i4 L_001d: ret
для дальнейшего тестирования, которое подтверждает мое замечание о производительности нового C#7 синтаксис, превосходящий ранее доступные параметры, см. здесь (в частности, пример 'D').