Właśnie przeglądam rozdział 4 C # w Depth, który dotyczy typów zerowalnych, i dodaję sekcję o używaniu operatora „as”, który pozwala pisać:
object o = ...;
int? x = o as int?;
if (x.HasValue)
{
... // Use x.Value in here
}
Myślałem, że to naprawdę fajne i że może poprawić wydajność w stosunku do C # 1, używając „jest”, a następnie rzutowania - w końcu w ten sposób musimy poprosić tylko o dynamiczne sprawdzenie typu, a następnie proste sprawdzenie wartości .
Jednak wydaje się, że tak nie jest. Poniżej zamieściłem przykładową aplikację testową, która w zasadzie sumuje wszystkie liczby całkowite w tablicy obiektów - ale tablica zawiera wiele odwołań zerowych i odniesień łańcuchowych, a także liczb całkowitych w ramkach. Benchmark mierzy kod, którego należy użyć w języku C # 1, kod za pomocą operatora „as” i tylko dla rozwiązania LINQ. Ku mojemu zdziwieniu kod C # 1 jest w tym przypadku 20 razy szybszy - a nawet kod LINQ (który, jak się spodziewałam, byłby wolniejszy, biorąc pod uwagę zaangażowane iteratory) bije kod „jak”.
Czy implementacja .NET isinst
dla typów zerowalnych jest po prostu bardzo wolna? Czy to dodatkoweunbox.any
problem powoduje problem? Czy jest na to inne wytłumaczenie? W tej chwili wydaje mi się, że będę musiał dołączyć ostrzeżenie przed użyciem tego w sytuacjach wrażliwych na wydajność ...
Wyniki:
Obsada: 10000000: 121
Jako: 10000000: 2211
LINQ: 10000000: 2143
Kod:
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
na typach zerowalnych. Interesujące, ponieważ nie można go stosować w przypadku innych typów wartości. Właściwie bardziej zaskakujące.as
próbuje rzutować na typ, a jeśli się nie powiedzie, to zwraca null. Nie można ustawić typów wartości na nullOdpowiedzi:
Oczywiście kod maszynowy, który kompilator JIT może wygenerować dla pierwszego przypadku, jest znacznie bardziej wydajny. Jedną z zasad, która naprawdę tam pomaga, jest to, że obiekt można rozpakować tylko do zmiennej, która ma ten sam typ, co wartość pudełkowa. Dzięki temu kompilator JIT może generować bardzo wydajny kod, nie trzeba brać pod uwagę konwersji wartości.
jest badanie operator jest proste, wystarczy sprawdzić, czy obiekt nie jest nieważna i jest oczekiwanego typu, ale trwa kilka instrukcje kodu maszynowego. Rzutowanie jest również łatwe, kompilator JIT zna położenie bitów wartości w obiekcie i używa ich bezpośrednio. Kopiowanie lub konwersja nie występuje, cały kod maszynowy jest wbudowany i zajmuje około tuzina instrukcji. To musiało być naprawdę wydajne w .NET 1.0, gdy boks był powszechny.
Przesyłam na int? zajmuje dużo więcej pracy. Reprezentacja wartości pudełkowatej liczby całkowitej jest niezgodna z układem pamięci
Nullable<int>
. Wymagana jest konwersja, a kod jest trudny ze względu na możliwe typy wyliczeń w pudełku. Kompilator JIT generuje wywołanie funkcji pomocniczej CLR o nazwie JIT_Unbox_Nullable, aby wykonać zadanie. Jest to funkcja ogólnego przeznaczenia dla dowolnego typu wartości, z dużą ilością kodu do sprawdzania typów. Wartość jest kopiowana. Trudno oszacować koszt, ponieważ ten kod jest zamknięty w pliku mscorwks.dll, ale prawdopodobnie setki instrukcji kodu maszynowego.Metoda rozszerzenia Linq OfType () również używa operatora is i rzutowania. Jest to jednak obsada typu ogólnego. Kompilator JIT generuje wywołanie funkcji pomocniczej JIT_Unbox (), która może wykonać rzutowanie na dowolny typ wartości. Nie mam wielkiego wyjaśnienia, dlaczego jest tak powolny jak obsada
Nullable<int>
, biorąc pod uwagę, że mniej pracy powinno być konieczne. Podejrzewam, że ngen.exe może powodować problemy tutaj.źródło
Wydaje mi się, że w przypadku
isinst
typów zerowalnych jest to bardzo powolne. W metodzieFindSumWithCast
zmieniłemdo
co również znacznie spowalnia wykonanie. Jedyne różnice w IL, jakie widzę, to to
zmienia się na
źródło
isinst
następuje test nieważności a następnie warunkowounbox.any
. W przypadku zerowym istnieje bezwarunkowyunbox.any
.isinst
iunbox.any
są wolniejsze na typach zerowalnych.Zaczęło się to od komentarza do doskonałej odpowiedzi Hansa Passanta, ale trwało zbyt długo, więc chcę tutaj dodać kilka bitów:
Po pierwsze,
as
operator C # wyśleisinst
instrukcję IL (podobnie jakis
operator). (Kolejna interesująca instrukcja jestcastclass
emitowana podczas bezpośredniego przesyłania, a kompilator wie, że nie można pominąć sprawdzania czasu wykonywania).Oto, co
isinst
robi ( ECMA 335 Partition III, 4.6 ):Najważniejsze:
Tak więc zabójca wydajności nie jest
isinst
w tym przypadku, ale dodatkowymunbox.any
. Nie było to jasne w odpowiedzi Hansa, gdy spojrzał tylko na kod JITed. Ogólnie rzecz biorąc, kompilator C # będzie emitowałunbox.any
po nimisinst T?
(ale pominie go w przypadkuisinst T
, gdyT
to zrobisz , kiedy jest typem odniesienia).Dlaczego to robi?
isinst T?
nigdy nie ma efektu, który byłby oczywisty, tzn. odzyskaszT?
. Zamiast tego, wszystkie te instrukcje zapewniają, że masz do"boxed T"
rozpakowaniaT?
. Aby uzyskać rzeczywistyT?
, nadal musimy rozpakować nasze"boxed T"
doT?
, i dlatego kompilator emitujeunbox.any
poisinst
. Jeśli się nad tym zastanowić, ma to sens, ponieważ „format skrzynki”T?
to po prostu"boxed T"
a tworzeniecastclass
iisinst
wykonywanie rozpakowywania byłoby niespójne.Kopię zapasową odkrycia Hansa z pewnymi informacjami ze standardu , oto:
(ECMA 335 Partition III, 4.33):
unbox.any
(ECMA 335 partycja III, 4.32):
unbox
źródło
Co ciekawe, przekazałem informację zwrotną na temat wsparcia operatora,
dynamic
ponieważ byłem wolniejszy o rząd wielkościNullable<T>
(podobnie jak w tym wczesnym teście ) - podejrzewam z bardzo podobnych powodów.Musisz kochać
Nullable<T>
. Kolejną zabawną rzeczą jest to, że chociaż JITnull
rozpoznaje (i usuwa) struktury niedozwolone, to rozdziela go naNullable<T>
:źródło
null
dla struktur, które nie mają wartości zerowych”? Czy masz na myśli, że zastępujenull
on wartość domyślną lub coś w czasie wykonywania?T
itp.). Wymagania dotyczące stosu itp. Zależą od argumentów (ilość miejsca stosu dla lokalnego itp.), Więc otrzymujesz jeden JIT dla każdej unikalnej permutacji obejmującej typ wartości. Jednak referencje są tego samego rozmiaru, więc udostępnij JIT. Wykonując JIT według wartości, może sprawdzić kilka oczywistych scenariuszy i próbuje wyciąć nieosiągalny kod z powodu takich rzeczy, jak niemożliwe wartości zerowe. Uwaga, to nie jest idealne. Ponadto ignoruję AOT dla powyższego.count
zmiennej. DodanieConsole.Write(count.ToString()+" ");
powatch.Stop();
obu przypadkach spowalnia pozostałe testy o prawie rząd wielkości, ale nieograniczony test zerowania nie ulega zmianie. Zauważ, że nastąpiły również zmiany podczas testowania przypadków, w którychnull
przekazanie zostało potwierdzone, potwierdzając, że oryginalny kod tak naprawdę nie wykonuje sprawdzania wartości zerowej i przyrostu dla innych testów. LinqpadJest to wynik FindSumWithAsAndHas powyżej:
Jest to wynik FindSumWithCast:
Wyniki:
Używając
as
, najpierw sprawdza, czy obiekt jest instancją Int32; pod maską używaisinst Int32
(który jest podobny do odręcznego kodu: if (o jest int)). I przy użyciuas
bezwarunkowo rozpakowuje również obiekt. I jest prawdziwym zabójcą wydajności nazywanie właściwości (wciąż jest to funkcja pod maską), IL_0027Za pomocą rzutowania najpierw testujesz, czy obiekt jest
int
if (o is int)
; używa tego pod maskąisinst Int32
. Jeśli jest to instancja int, możesz bezpiecznie rozpakować wartość IL_002DMówiąc najprościej, jest to pseudo-kod użycia
as
metody:A to pseudo-kod użycia metody rzutowania:
Więc rzutowanie (
(int)a[i]
no cóż, składnia wygląda jak rzutowanie, ale tak naprawdę rozpakowywanie, rzutowanie i rozpakowywanie mają tę samą składnię, następnym razem będę pedantyczny z właściwą terminologią) podejście jest naprawdę szybsze, wystarczy tylko rozpakować wartość gdy obiekt jest zdecydowanieint
. Tego samego nie można powiedzieć o zastosowaniuas
podejścia.źródło
Aby zachować aktualność tej odpowiedzi, warto wspomnieć, że większość dyskusji na tej stronie jest teraz dyskusyjna dzięki C # 7.1 i .NET 4.7, które obsługują wąską składnię, która również wytwarza najlepszy kod IL.
Oryginalny przykład PO ...
staje się po prostu ...
Odkryłem, że jednym z powszechnych zastosowań nowej składni jest pisanie typu wartości .NET (tj.
struct
W języku C # ), który implementujeIEquatable<MyStruct>
(jak większość powinna). Po zaimplementowaniu metody o silnym typieEquals(MyStruct other)
możesz teraz z wdziękiem przekierować do niej niepopisaneEquals(Object obj)
zastąpienie (dziedziczone zObject
) w następujący sposób:Dodatek: Tutaj podano kod
Release
kompilacji IL dla pierwszych dwóch przykładowych funkcji pokazanych powyżej w tej odpowiedzi (odpowiednio). Chociaż kod IL dla nowej składni jest rzeczywiście o 1 bajt mniejszy, to najczęściej wygrywa duży, wykonując zero wywołań (w porównaniu do dwóch) i unikającunbox
operacji, jeśli to możliwe.Dalsze testy, które potwierdzają moją uwagę na temat wydajności nowej składni C # 7 przewyższającej wcześniej dostępne opcje, zobacz tutaj (w szczególności przykład „D”).
źródło
Dalsze profilowanie:
Wynik:
Co możemy wywnioskować z tych liczb?
źródło
Nie mam czasu, aby spróbować, ale możesz chcieć mieć:
tak jak
Za każdym razem tworzysz nowy obiekt, który nie do końca wyjaśni problem, ale może się przydać.
źródło
int?
na stosie przy użyciuunbox.any
. Podejrzewam, że na tym polega problem - domyślam się, że ręcznie wykonana IL mogłaby pokonać obie opcje tutaj ... chociaż możliwe jest również, że JIT jest zoptymalizowany do rozpoznawania w przypadku is / cast i sprawdza tylko raz.Wypróbowałem dokładną konstrukcję sprawdzania typu
typeof(int) == item.GetType()
, który działa tak szybko, jakitem is int
wersja i zawsze zwraca liczbę (podkreślenie: nawet jeśli napisałeśNullable<int>
tablicę, musisz użyćtypeof(int)
). Potrzebujesz także dodatkowegonull != item
czeku tutaj.jednak
typeof(int?) == item.GetType()
pozostaje szybki (w przeciwieństwie doitem is int?
), ale zawsze zwraca false.Typeof-construct jest moim zdaniem najszybszym sposobem na dokładne sprawdzenie typu, ponieważ wykorzystuje on RuntimeTypeHandle. Ponieważ dokładne typy w tym przypadku nie pasują do wartości zerowej, sądzę,
is/as
że muszę tutaj wykonać dodatkowe podnoszenie ciężarów, upewniając się, że jest to rzeczywiście przypadek typu zerowalnego.I szczerze mówiąc: co
is Nullable<xxx> plus HasValue
kupujesz? Nic. Zawsze możesz przejść bezpośrednio do bazowego (wartości) typu (w tym przypadku). Dostajesz wartość lub „nie, nie instancję typu, o który prosiłeś”. Nawet jeśli napisałeś(int?)null
do tablicy, sprawdzanie typu zwróci false.źródło
int?
- jeśli boksujeszint?
wartość, to kończy się jako boks int lubnull
odniesienie.Wyjścia:
[EDIT: 2010-06-19]
Uwaga: poprzedni test został przeprowadzony wewnątrz VS, debugowania konfiguracji, przy użyciu VS2009, przy użyciu Core i7 (firmowa maszyna programistyczna).
Poniższe czynności zostały wykonane na moim komputerze przy użyciu Core 2 Duo i VS2010
źródło