Производительность сюрприз с "как" и обнуляемые типы
Я просто пересматриваю четвертую главу 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
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 ответов
Очевидно, машинный код, который 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
Я изменился
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):
Формат: isinst typeTok
typeTok - это токен метаданных (
typeref
,typedef
или жеtypespec
), указывая на желаемый класс.Если typeTok является ненулевым типом значения или универсальным типом параметра, он интерпретируется как "упакованный" typeTok.
Если typeTok является обнуляемым типом,
Nullable<T>
, это интерпретируется как "в штучной упаковке"T
Самое главное:
Если фактический тип (не отслеживаемый типом для верификатора) объекта obj является верифицируемым для типа typeTok, то
isinst
успешно и obj (как результат) возвращается без изменений, в то время как проверка отслеживает его тип как typeTok. В отличие от принуждений (§1.6) и преобразований (§3.27),isinst
никогда не изменяет фактический тип объекта и сохраняет идентичность объекта (см. Раздел I).
Таким образом, убийца производительности не isinst
в этом случае, но дополнительный unbox.any
, Это не было ясно из ответа Ханса, поскольку он смотрел только на код JITed. В общем, компилятор C# будет выдавать unbox.any
после isinst 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
Применительно к штучной форме типа значения,
unbox.any
инструкция извлекает значение, содержащееся в объекте (типаO
). (Это эквивалентноunbox
с последующимldobj
.) Применительно к ссылочному типуunbox.any
инструкция имеет тот же эффект, что иcastclass
typeTok.
(ECMA 335, раздел III, 4.32): unbox
Как правило,
unbox
просто вычисляет адрес типа значения, который уже присутствует внутри упакованного объекта. Этот подход невозможен при распаковке типов значений, допускающих значение NULL. Так какNullable<T>
значения преобразуются в штучной упаковкеTs
во время работы с боксом реализация часто должна производить новый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;
}
}
Для того чтобы этот ответ был актуальным, стоит упомянуть, что большая часть обсуждения на этой странице теперь спорна с C# 7.1 и .NET 4.7, которые поддерживают тонкий синтаксис, который также производит лучший код IL.
Оригинальный пример ОП...
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)
переопределить (наследуется от Object
) к нему следующим образом:
public override bool Equals(Object obj) => obj is MyStruct o && Equals(o);
Приложение: 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").
Это результат FindSumWithAsAndHas выше: http://www.freeimagehosting.net/uploads/9e3c0bfb75.png
Это результат FindSumWithCast: http://www.freeimagehosting.net/uploads/ce8a5a3934.png
Выводы:
С помощью
as
сначала проверяется, является ли объект экземпляром Int32; под капотом он используетisinst Int32
(что аналогично рукописному коду: if (o is int)). И используяas
, это также безусловно распаковывает объект. И это настоящий убийца производительности для вызова свойства (это все еще функция под капотом), IL_0027Используя приведение, вы сначала проверяете, является ли объект
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;
И это псевдокод использования приведения:
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
Что мы можем сделать из этих цифр?
- Во-первых, подход "потом бросок" значительно быстрее, чем подход. 303 против 3524
- Во-вторых,.Value незначительно медленнее, чем приведение. 3524 против 3272
- В-третьих,.HasValue немного медленнее, чем при использовании вручную (то есть с использованием is). 3524 против 3282
- В-четвертых, выполняя сравнение между яблоками (т. Е. Как присвоение имитированного значения 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
здесь нужно сделать дополнительную тяжелую атлетику, чтобы убедиться, что это на самом деле экземпляр типа Nullable.
И честно: чем занимается ваш is Nullable<xxx> plus HasValue
купить вас? Ничего такого. Вы всегда можете перейти непосредственно к базовому типу (значению) (в данном случае). Вы либо получаете значение, либо "нет, не экземпляр того типа, о котором вы просили". Даже если ты написал (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
[РЕДАКТИРОВАТЬ: 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