Ускорение производительности с типами "как" и "с нулевыми значениями"
Я просто пересматриваю главу 4 С# в Depth, которая имеет дело с типами NULL, и я добавляю раздел об использовании оператора "as", который позволяет вам написать:
object o = ...;
int? x = o as int?;
if (x.HasValue)
{
... // Use x.Value in here
}
Я думал, что это было очень аккуратно, и что это может повысить производительность над эквивалентом С# 1, используя "is", за которым следует бросок. В конце концов, нам нужно только один раз запросить проверку динамического типа, а затем простая проверка значения.
Это, похоже, не так. Я включил тестовое приложение ниже, которое в основном суммирует все целые числа в массиве объектов, но массив содержит множество нулевых ссылок и ссылок на строки, а также целые числа в штучной упаковке. Эталонный показатель измеряет код, который вы должны использовать в С# 1, код с использованием оператора "as" и просто для решения LINQ. К моему удивлению, код С# 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);
}
}
Ответы
Ответ 1
Очевидно, что машинный код, который компилятор JIT может генерировать для первого случая, намного эффективнее. Одно правило, которое действительно помогает, состоит в том, что объект может быть только unboxed для переменной, которая имеет тот же тип, что и размер в штучной упаковке. Это позволяет компилятору JIT генерировать очень эффективный код, не нужно учитывать преобразования значений.
Операторский тест прост, просто проверьте, не является ли объект нулевым и имеет ожидаемый тип, берет только несколько инструкций машинного кода. Литье также легко, компилятор JIT знает расположение битов значения в объекте и использует их напрямую. Никакое копирование или преобразование не происходит, весь машинный код является встроенным и занимает всего около десятка инструкций. Это должно было быть действительно эффективным в .NET 1.0, когда бокс был обычным явлением.
Кастинг для int? занимает гораздо больше работы. Представление значения целого числа в штучной упаковке несовместимо с макетом памяти Nullable<int>
. Требуется преобразование, и код является сложным из-за возможных типов переименованных коробок. Компилятор JIT генерирует вызов вспомогательной функции CLR с именем JIT_Unbox_Nullable, чтобы выполнить задание. Это функция общего назначения для любого типа значений, много кода для проверки типов. И значение копируется. Трудно оценить стоимость, так как этот код заблокирован внутри mscorwks.dll, но, вероятно, сотни инструкций машинного кода.
Метод расширения Linq OfType() также использует оператор is и cast. Это, однако, приведение к родовому типу. Компилятор JIT генерирует вызов вспомогательной функции JIT_Unbox(), которая может выполнять приведение к произвольному типу значений. У меня нет отличного объяснения, почему оно так же медленное, как приведение к Nullable<int>
, учитывая, что нужно меньше работать. Я подозреваю, что ngen.exe может вызвать проблемы здесь.
Ответ 2
Мне кажется, что isinst
просто слишком медленный для типов с нулевым значением. В методе FindSumWithCast
я изменил
if (o is int)
к
if (o is int?)
что также значительно замедляет выполнение. Единственное отличие в IL, которое я вижу, состоит в том, что
isinst [mscorlib]System.Int32
изменяется на
isinst valuetype [mscorlib]System.Nullable`1<int32>
Ответ 3
Это изначально появилось в качестве комментария к замечательному ответу Ханса Пассана, но оно слишком долго, поэтому я хочу добавить несколько бит здесь:
Во-первых, оператор С# as
выдает команду isinst
IL (так же работает оператор is
). (Еще одна интересная инструкция - castclass
, которая выдается, когда вы выполняете прямой трансляции, и компилятор знает, что проверка выполнения не может быть исключена.)
Вот что isinst
делает (ECMA 335 Partition III, 4.6):
Формат: isinst typeTok
typeTok - токен метаданных (a typeref
, typedef
или typespec
), указывающий желаемый класс.
Если typeTok - тип значения, не допускающий нулевой или общий тип параметра, он интерпретируется как "boxed" typeTok.
Если типTok является типом с нулевым значением, Nullable<T>
, он интерпретируется как "в штучной упаковке" T
Самое главное:
Если фактический тип (не отслеживаемый верификатором типа) объекта obj является присваивающим верификатору тип typeTok, то isinst
завершается успешно, а obj (как результат) возвращается без изменений, а проверка отслеживает его тип как typeTok. В отличие от принуждений (§ 1.6) и преобразований (§3.27), isinst
никогда не изменяет фактический тип объекта и не сохраняет идентичность объекта (см. раздел I).
Таким образом, убийца производительности в этом случае не isinst
, а дополнительный unbox.any
. Это не было ясно из ответа Ганса, поскольку он смотрел только на код JITed. В общем случае компилятор С# будет выдавать 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
выполнения unbox будет непоследовательным.
Резервное копирование поиска Ханса с помощью некоторой информации из стандарта, вот оно:
(ECMA 335, раздел III, 4.33): unbox.any
При применении к коробчатой форме типа значения команда unbox.any
извлекает значение, содержащееся в obj (типа O
). (Это эквивалентно unbox
, за которым следует ldobj
.) При применении к ссылочному типу команда unbox.any
имеет тот же эффект, что и castclass
typeTok.
(ECMA 335, раздел III, 4.32): unbox
Как правило, unbox
просто вычисляет адрес типа значения, который уже присутствует внутри объекта в штучной упаковке. Такой подход невозможен при распаковке типов значений NULL. Поскольку значения Nullable<T>
преобразуются в коробку Ts
во время операции с ящиком, реализация часто должна производить новый Nullable<T>
в куче и вычислять адрес для вновь выделенного объекта.
Ответ 4
Интересно, что я передал обратную связь о поддержке оператора через 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;
}
}
Ответ 5
Это результат поиска 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? 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
.
Ответ 6
Профилирование далее:
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
- Во-вторых, значение будет немного медленнее, чем литье. 3524 против 3272
- В-третьих,.HasValue немного медленнее, чем использование ручного (т.е. использование ). 3524 против 3282
- В-четвертых, сравнение apple-to-apple (т.е. как назначение симулированного HasValue, так и преобразование моделируемого значения происходит вместе) между имитируемым как и реальным как, мы может видеть, что моделируется как по-прежнему значительно быстрее, чем реальный как. 395 против 3524
- Наконец, основываясь на первом и четвертом выводах, что-то не так с как
реализация ^ _ ^
Ответ 7
Чтобы этот ответ был актуальным, стоит упомянуть, что большая часть обсуждения на этой странице теперь спорна с С# 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
в С#), который реализует IEquatable<MyStruct>
( как большинство должно). После реализации строго типизированного метода Equals(MyStruct other)
вы можете теперь изящно перенаправить нетипизированное переопределение Equals(Object obj)
(унаследованное от Object
) следующим образом:
public override bool Equals(Object obj) => obj is MyStruct o && Equals(o);
Приложение: Код Release
build 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
Для дальнейшего тестирования, подтверждающего мое замечание о производительности нового синтаксиса С# 7, превосходящего ранее доступные параметры, см. здесь (в частности, пример "D").
Ответ 8
У меня нет времени попробовать, но вы можете захотеть:
foreach (object o in values)
{
int? x = o as int?;
а
int? x;
foreach (object o in values)
{
x = o as int?;
Каждый раз вы создаете новый объект, который не будет полностью объяснять проблему, но может способствовать.
Ответ 9
Я попытался построить конструкцию проверки типа
typeof(int) == item.GetType()
, который работает так же быстро, как версия item is int
, и всегда возвращает номер (выделение: даже если вы написали Nullable<int>
в массив, вам нужно будет использовать typeof(int)
). Вам также нужна дополнительная проверка null != item
здесь.
Однако
typeof(int?) == item.GetType()
остается быстрым (в отличие от item is int?
), но всегда возвращает false.
Тип-конструкция в моих глазах - самый быстрый способ точной проверки типов, поскольку он использует RuntimeTypeHandle. Поскольку точные типы в этом случае не совпадают с нулевыми значениями, я полагаю, is/as
должен выполнить дополнительную потерю внимания здесь, гарантируя, что это фактически экземпляр типа Nullable.
И честно: что ваш is Nullable<xxx> plus HasValue
покупает вас? Ничего. Вы всегда можете перейти непосредственно к базовому (значению) типу (в данном случае). Вы либо получаете значение, либо "нет, а не экземпляр типа, о котором вы просите". Даже если вы написали (int?)null
в массив, проверка типа вернет false.
Ответ 10
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