Очевидно, что машинный код, который JIT-компилятор может сгенерировать для первого случая, намного более эффективен. Одно правило, которое действительно помогает, заключается в том, что объект можно распаковать только в переменную, имеющую тот же тип, что и значение в коробке. Это позволяет JIT-компилятору генерировать очень эффективный код без необходимости преобразования значений.
Проверка оператора is проста, просто проверьте, не является ли объект нулевым и имеет ли объект ожидаемый тип, требует лишь нескольких инструкций машинного кода. Приведение также простое, JIT-компилятор знает расположение битов значения в объекте и использует их напрямую. Никакого копирования или преобразования не происходит, весь машинный код является встроенным и требует около дюжины инструкций. Это должно было быть действительно эффективным в .NET 1.0, когда бокс был обычным явлением.
Преобразование в int? требует намного больше работы. Представление значения упакованного целого числа несовместимо с разметкой памяти Nullable
. Требуется преобразование, а код сложен из-за возможных типов перечислений в штучной упаковке. Компилятор JIT генерирует вызов вспомогательной функции CLR с именем JIT_Unbox_Nullable, чтобы выполнить задание. Это функция общего назначения для любого типа значения, там много кода для проверки типов. И значение копируется. Трудно оценить стоимость, поскольку этот код заперт внутри mscorwks.dll, но, вероятно, сотни инструкций машинного кода.
Метод расширения Linq OfType () также использует оператор is и приведение типов. Однако это приведение к универсальному типу.Компилятор JIT генерирует вызов вспомогательной функции JIT_Unbox (), которая может выполнять приведение к произвольному типу значения. У меня нет четкого объяснения, почему это так медленно, как приведение к Nullable
, учитывая, что требуется меньше работы. Я подозреваю, что здесь ngen.exe может вызвать проблемы.
Интересно, что я передал отзыв о поддержке операторов через dynamic
, который на порядок медленнее для Nullable
(аналогично этот ранний тест ) - подозреваю по очень похожим причинам.
Должен любить Nullable
. Еще один интересный момент заключается в том, что даже несмотря на то, что JIT выявляет (и удаляет) null
для структур, не допускающих значения NULL, он исключает его для Nullable
:
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;
}
}
У меня нет времени попробовать, но вы можете иметь:
foreach (object o in values)
{
int? x = o as int?;
as
int? x;
foreach (object o in values)
{
x = o as int?;
Вы каждый раз создаете новый объект, что не полностью объясняет проблема, но может внести свой вклад.
Мне кажется, что isinst
очень медленно работает с типами, допускающими значение NULL. В методе FindSumWithCast
я изменил
if (o is int)
на
if (o is int?)
, что также значительно замедляет выполнение. Единственное отличие в IL, которое я вижу, это то, что
isinst [mscorlib]System.Int32
заменяется на
isinst valuetype [mscorlib]System.Nullable`1<int32>
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
Я попробовал точную конструкцию проверки типа
typeof (int) == item.GetType ()
, которая работает так же быстро, как элемент имеет версию int
, и всегда возвращает число (выделение: даже если вы записали в массив Nullable
, вам нужно будет использовать typeof (int)
). Вам также потребуется дополнительная проверка null! = Item
.
Однако
typeof (int?) == item.GetType ()
остается быстрым (в отличие от item is int?
), но всегда возвращает false.
Конструкция typeof, на мой взгляд, является самым быстрым способом точной проверки типов, поскольку она использует RuntimeTypeHandle. Поскольку точные типы в этом случае не совпадают с типом, допускающим значение NULL, я предполагаю, что is / as
должны сделать здесь дополнительный тяжелый лифтинг, чтобы убедиться, что это на самом деле экземпляр типа Nullable.
И честно говоря: что дает вам Nullable
? Ничего такого. Вы всегда можете перейти непосредственно к базовому (значению) типу (в этом случае). Вы либо получаете значение, либо «нет, не экземпляр того типа, который вы просили». Даже если вы записали в массив (int?) Null
, проверка типа вернет false.
Это результат FindSumWithAsAndHas выше:
Это результат FindSumWithCast:
Выводы:
Используя как
, сначала проверяется, является ли объект экземпляром Int32; под капотом он использует isinst Int32
(который похож на рукописный код: if (o is int)). И используя как
, он также безоговорочно распаковывает объект. И это настоящий убийца производительности - вызвать свойство (это все еще функция под капотом), IL_0027
Используя приведение, вы сначала проверяете, является ли объект int
if (o is int)
; под капотом используется isinst Int32
. Если это экземпляр int, то вы можете безопасно распаковать значение, IL_002D
Проще говоря, это псевдокод использования подхода как
:
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
. Нельзя сказать то же самое об использовании подхода как
.
Профилирование дальше:
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
Что мы можем заключить из этих цифр?