Performance Überraschung mit „as“ und Nullable Types
-
21-09-2019 - |
Frage
Ich bin Überarbeitung nur Kapitel 4 von C # in Tiefe, die sie mit Nullable-Typen, und ich hinzufüge, einen Abschnitt über die Verwendung des „als“ Operator, die Sie schreiben kann:
object o = ...;
int? x = o as int?;
if (x.HasValue)
{
... // Use x.Value in here
}
Ich dachte, das wirklich ordentlich war, und dass es könnte Verbesserung der Leistung über die C # 1 Äquivalent unter Verwendung „wird“, gefolgt von einer Besetzung - immerhin auf diese Weise brauchen wir nur fragen dynamischen Typ einmal überprüft, und dann ein einfache Wertprüfung.
Dies scheint nicht der Fall zu sein, aber. Ich habe eine Probe-Test app unten enthalten, die im Grunde alle summiert die Zahlen innerhalb eines Objekts Array - aber das Array enthält eine Menge von null Referenzen und String Verweise sowie boxed ganze Zahlen. Die Benchmark misst den Code, den Sie in C # 1 zu verwenden haben würden, der Code die „als“ Operator, und nur eine LINQ-Lösung für Kicks. Zu meinem Erstaunen ist der C # 1 Code 20-mal schneller in diesem Fall - und auch der LINQ-Code (die ich langsamer sein erwartet haben würde, da die Iteratoren beteiligt) schlägt den „als“ Code
. Ist die .NET-Implementierung von isinst
für Nullable Types nur sehr langsam? Ist es die zusätzlichen unbox.any
, das das Problem verursacht? Gibt es eine andere Erklärung dafür? Im Moment fühlt es sich wie ich werde eine Warnung gegen die Verwendung dieser Leistungs sensiblen Situationen müssen schließen ...
Ergebnisse:
Darsteller: 10000000: 121
As: 10000000: 2211
LINQ: 10000000: 2143
Code:
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);
}
}
Lösung
Klar, dass der Maschinencode der JIT-Compiler für den ersten Fall erzeugen können, ist viel effizienter. Eine Regel, die wirklich hilft, ist, dass ein Objekt nur unboxed sein kann, um eine Variable, die den gleichen Typ wie der Box-Wert hat. Das ermöglicht die JIT-Compiler sehr effizienten Code zu generieren, keine Wert Umwandlungen in Betracht gezogen werden müssen.
Der ist Operator Test ist einfach, nur überprüfen, ob das Objekt nicht null ist und von dem erwarteten Typ, dauert aber ein paar Maschinencode Anweisungen. Die Besetzung ist auch leicht, der JIT-Compiler kennt den Standort des Wert Bits in dem Objekt und verwendet sie direkt. Vervielfältigung oder Umwandlung auftritt, werden alle Maschinencode ist inline und dauert aber etwa ein Dutzend Anweisungen. Dies benötigt wirklich effizient wieder in .NET 1.0 sein, wenn Boxen üblich waren.
Casting auf int? nimmt viel mehr Arbeit. Der Wert Darstellung der geschachtelte Ganzzahl ist mit dem Speicher-Layout von Nullable<int>
nicht kompatibel. Eine Konvertierung ist erforderlich, und der Code ist schwierig wegen möglichen boxed Enum-Typen. Der JIT-Compiler generiert einen Aufruf an eine CLR Hilfsfunktion namens JIT_Unbox_Nullable den Job zu erledigen. Dies ist ein Mehrzweck-Funktion für jeden Werttyp, eine Menge Code dort Typen zu überprüfen. Und der Wert wird kopiert. Schwer, die Kosten abzuschätzen, da dieser Code innerhalb mscorwks.dll eingesperrt, aber Hunderte von Anweisungen Maschinencode wahrscheinlich ist.
Die Linq OfType () Erweiterungsmethode verwendet auch die is Operator und die Besetzung. Dies ist jedoch eine Umwandlung in einen generischen Typ. Der JIT-Compiler erzeugt einen Aufruf einer Hilfsfunktion, JIT_Unbox (), die ein gegossenes auf einen beliebigen Wert Typ durchführen kann. Ich habe keine große Erklärung, warum es so langsam wie die Besetzung zu Nullable<int>
ist, da weniger Arbeit sollte erforderlich sein. Ich vermute, dass ngen.exe könnte Probleme verursachen hier.
Andere Tipps
Es scheint mir, dass die isinst
nur sehr langsam auf Nullable-Typen. In Verfahren FindSumWithCast
geändert I
if (o is int)
if (o is int?)
, die deutlich verlangsamt auch die Ausführung. Der einzige differenc in IL Ich kann sehen, ist, dass
isinst [mscorlib]System.Int32
wird geändert
isinst valuetype [mscorlib]System.Nullable`1<int32>
Das ursprünglich begann als Kommentar zu Hans Passant die ausgezeichnete Antwort, aber es hat zu lange, damit ich hier ein paar Bits hinzufügen möchten:
Zuerst wird der C # as
Operator einen isinst
IL Befehl (so auch den is
operator) emittiert. (Eine weitere interessante Anweisung ist castclass
, emited, wenn Sie tun, um eine direkte Besetzung und der Compiler weiß, dass Laufzeitprüfung nicht ommited werden kann.)
Hier ist, was isinst
tut ( ECMA 335 Partition III, 4.6 ):
Format: isinst typeTok
typeTok ein Metadatentoken (a
typeref
,typedef
odertypespec
), die gewünschte Klasse angibt.Wenn typeTok ist ein Nicht-Nullable-Wertetyp oder ein generischer Parameter Typ interpretiert wird als „boxed“ typeTok .
Wenn typeTok ist ein Nullable Type,
Nullable<T>
, wird es als „boxed“T
interpretiert
Am wichtigsten ist:
Wenn die Ist-Typ (nicht der Verifizierer verfolgt Typ) von obj ist Verifizierer zuweisbare-to der Typ typeTok dann
isinst
erfolgreich ist und obj (wie Ergebnis ) unverändert zurückgegeben wird, während Überprüfung verfolgt seine Art als typeTok . Im Gegensatz zu Nötigungen (§1.6) und Conversions (§3.27),isinst
ändert sich nie die tatsächliche Typ eines Objekts und Konserven Objektidentität (siehe Partition I).
So ist der Leistungskiller nicht isinst
in diesem Fall, aber die zusätzlichen unbox.any
. Dies war nicht klar, von Hans' Antwort, als er nur am JITed Code aussah. In der Regel wird der C # -Compiler eine unbox.any
nach einem isinst T?
emittieren (aber wird es für den Fall auslassen Sie tun isinst T
, wenn T
ist ein Referenz-Typ).
Warum tut sie das? isinst T?
hat nie den Effekt, dass offensichtlich gewesen wäre, das heißt Sie wieder eine T?
. Stattdessen werden alle diese Anweisungen sicherstellen, dass Sie eine "boxed T"
haben, die T?
unboxed sein kann. Um eine tatsächliche T?
zu erhalten, müssen wir noch unsere "boxed T"
zu T?
unbox, weshalb der Compiler einen unbox.any
nach isinst
emittiert. Wenn man darüber nachdenkt, das macht Sinn, weil das „Box-Format“ für T?
nur "boxed T"
und machen castclass
und isinst
die unbox führen wäre inkonsistent.
Sichern von Hans' zu finden, mit einigen Informationen aus dem Standard , hier geht es:
(ECMA 335 Partition III, 4,33): unbox.any
Wenn an die geschachtelte Form eines Werttyp angewendet wird, die Anweisung
unbox.any
extrahiert die enthaltenen Wert innerhalb obj (vom TypO
). (Es ist äquivalent zuunbox
vonldobj
gefolgt.) Wenn auf einen Referenztyp angewandt, dieunbox.any
Anweisung hat die gleiche Wirkung wiecastclass
typeTok.
(ECMA 335 Partition III, 4,32): unbox
Typischerweise berechnet
unbox
einfach die Adresse des Werttypen, die bereits innerhalb des geschachtelten Objekts ist. Dieser Ansatz ist nicht möglich, wenn Nullable-Wertetypen Unboxing. DaNullable<T>
Werte boxedTs
während des Box-Betrieb umgewandelt werden, oft eine Implementierung eines neuenNullable<T>
auf dem Heap herstellen müssen und die Adresse an den neu zugeordneten Objekts berechnen.
Interessanterweise kam ich auf Feedback über Bedienerunterstützung über dynamic
eine Auftrag-of-Größenordnung langsamer für Nullable<T>
sein (ähnlich diese frühe Test ) - ich vermute, dass für sehr ähnlichen Gründen
Gotta love Nullable<T>
. Ein weiterer Spaß ist, dass, obwohl die JIT-Spots (und entfernt) null
für Nicht-Nullable-Strukturen, es Borks es für 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;
}
}
Dies ist das Ergebnis von FindSumWithAsAndHas oben:
Dies ist das Ergebnis von FindSumWithCast:
Ergebnisse:
-
Mit
as
, ist es zunächst testen, ob ein Objekt eine Instanz von Int32 ist; unter der Haube ist es mitisinst Int32
(der handgeschriebenen Code ähnlich ist: if (o int)). Und mitas
, es unbox auch bedingungslos das Objekt. Und es ist ein echter Performance-Killer eine Eigenschaft zu nennen (es ist immer noch eine Funktion unter der Haube), IL_0027 -
Mit gegossen, testen Sie zuerst, wenn das Objekt ein
int
if (o is int)
ist; unter der Haube wird mit diesemisinst Int32
. Wenn es eine Instanz von int ist, dann kann man sicher unbox den Wert, IL_002D
Einfach ausgedrückt, ist dies der Pseudo-Code von as
Ansatz:
int? x;
(x.HasValue, x.Value) = (o isinst Int32, o unbox Int32)
if (x.HasValue)
sum += x.Value;
Und das ist der Pseudo-Code aus gegossenem Ansatz:
if (o isinst Int32)
sum += (o unbox Int32)
So der Besetzung ((int)a[i]
, auch die Syntax sieht aus wie ein Guss, aber es ist tatsächlich Unboxing, Guss- und Unboxing die gleiche Syntax, das nächste Mal, wenn ich pedantisch mit der richtigen Terminologie sein wird) Ansatz ist wirklich schneller, nur benötigt einen Wert unbox wenn ein Objekt entscheidet eine int
ist. Das gleiche kann man nicht mit einem as
Ansatz gesagt werden.
Profil weiter:
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);
}
}
Ausgabe:
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
Was können wir aus diesen Zahlen ableiten?
- Erstens ist-then-Guss Ansatz ist deutlich schneller als wie Ansatz. 303 vs 3524
- Zweitens .Wert ist geringfügig langsamer als Guss. 3524 vs 3272
- Drittens .HasValue ist geringfügig langsamer als die manuelle Verwendung hat (d. Mit ). 3524 vs 3282
- Viertens macht einen Apfel zu Apfel Vergleich (dh beide simulierten HasValue Zuordnung und Umwandlung simulierten Wert zusammen geschieht) zwischen simuliert wie und real wie Ansatz, wir sehen kann simuliert wie ist immer noch deutlich schneller als real wie . 395 vs 3524
- Schließlich basierend auf erste und vierte Schlussfolgerung, es ist etwas falsch mit wie Implementierung ^ _ ^
Um diese Antwort zu halten up-to-date, es ist erwähnenswert, dass die meisten der Diskussion auf dieser Seite ist jetzt strittig jetzt mit C # 7.1 und .NET 4.7 , die eine schlanke Syntax unterstützt, dass auch die beste IL-Code erzeugt.
Die ursprüngliche Beispiel der OP ...
object o = ...;
int? x = o as int?;
if (x.HasValue)
{
// ...use x.Value in here
}
wird einfach ...
if (o is int x)
{
// ...use x in here
}
Ich habe festgestellt, dass eine gemeinsame Nutzung für die neue Syntax, wenn Sie eine .NET Werttyp Schreiben (dh struct
in C # ), dass Geräte IEquatable<MyStruct>
(wie die meisten sollte). die stark typisierte Equals(MyStruct other)
Methode Nach der Implementierung können Sie jetzt umleiten anmutig die untypisierten Equals(Object obj)
Korrektur (von Object
geerbt), um es wie folgt:
public override bool Equals(Object obj) => obj is MyStruct o && Equals(o);
Anhang: Die Release
build IL Code für die ersten zwei Beispielfunktionen in dieser Antwort oben gezeigt (jeweils) ist hier gegeben. Während der IL-Code für die neue Syntax in der Tat 1 Byte kleiner ist, ist es meist großes gewinnt durch Null Anrufe (gegenüber zwei) und der Vermeidung des unbox
Betriebes insgesamt, wenn möglich.
// 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
Für weitere Tests, die untermauern meine Bemerkung über die Leistung des neuen C # 7 Syntax, um die bisher verfügbaren Optionen übertreffen, finden Sie unter hier (insbesondere Beispiel 'D').
Ich habe keine Zeit, es zu versuchen, aber Sie können haben wollen:
foreach (object o in values)
{
int? x = o as int?;
als
int? x;
foreach (object o in values)
{
x = o as int?;
Sie werden jedes Mal ein neues Objekt zu erzeugen, die das Problem nicht vollständig erklären, kann aber dazu beitragen.
Ich habe versucht, den genauen Typ Prüfung Konstrukt
typeof(int) == item.GetType()
, das führt so schnell wie die item is int
Version, und gibt immer die Nummer (Schwerpunkt: auch wenn Sie eine Nullable<int>
auf das Array geschrieben, die Sie verwenden typeof(int)
benötigen würde). Sie müssen auch ein zusätzliches null != item
Check hier.
Allerdings
typeof(int?) == item.GetType()
Aufenthalte schnell (im Gegensatz zu item is int?
), aber immer false zurück.
Das typeof-Konstrukt ist in meinen Augen der schnellste Weg für genau Typprüfung, da sie die Runtimetypehandle verwendet. Da die genauen Typen in diesem Fall nicht mit NULL festlegbare übereinstimmen, meine Vermutung ist, haben is/as
hier zusätzliche heavylifting tun zu gewährleisten, dass es in der Tat ist eine Instanz eines Nullable-Typ.
Und ehrlich gesagt: was macht Ihr is Nullable<xxx> plus HasValue
Sie kaufen? Nichts. Sie können jederzeit direkt auf den darunter liegenden (Wert) Typen (in diesem Fall) gehen. Sie erhalten entweder den Wert oder „nein, nicht eine Instanz des Typs Sie fragen“. Auch wenn Sie (int?)null
auf das Array geschrieben, wird die Typprüfung false zurück.
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);
}
}
Ausgänge:
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]
Hinweis: Zurück Test innerhalb VS, Konfiguration Debug getan wurde, VS2009 verwenden, mit Core i7 (Unternehmensentwicklung Maschine).
Das Folgende wurde auf meinem Rechner getan Core 2 Duo mit, mit 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