2015-09-17 21 views
29

Betrachten wir eine Schleife wie folgt aus:Gibt es Kosten für das Eingeben und Verlassen eines C# geprüften Blocks?

for (int i = 0; i < end; ++i) 
    // do something 

Wenn ich weiß, dass i nicht überlaufen wird, aber ich möchte einen Scheck gegen Überlauf, Verkürzung, etc. in der „etwas tun“ Teil, ich bin besser mit dem checked Block innerhalb oder außerhalb der Schleife?

for (int i = 0; i < end; ++i) 
    checked { 
     // do something 
    } 

oder

checked { 
    for (int i = 0; i < end; ++i) 
     // do something 
} 

Generell gibt es eine Kosten zwischen aktivierten und deaktivierten Modus zu wechseln?

+14

Ich bezweifle, dass "checked" und "unchecked" es als Blöcke in die IL machen. Sie sagen dem Compiler möglicherweise einfach, in arithmetischen Anweisungen, die direkt in ihnen erscheinen, einen anderen Code auszugeben. Aber lass mich ... nachsehen. –

+2

@TheodorosChatzigiannakis Ich stimme zu, der "checked" Block gibt nur die '.ovf' Anweisungen aus. –

+1

https://en.wikipedia.org/wiki/List_of_CIL_instructions Heh, @EldarDordzhiev hat mich dazu gedrängt - die '.ovf' Versionen der arithmetischen Befehle werden ausgegeben - sie fügen ein wenig Overhead hinzu, anstatt die" Ebene "zu verwenden arithmetische Anweisungen. – xxbbcc

Antwort

9

checked und unchecked Blöcke erscheinen nicht auf der IL-Ebene. Sie werden nur im C# -Quellcode verwendet, um dem Compiler mitzuteilen, ob die überprüfenden oder nicht überprüfenden IL-Anweisungen ausgewählt werden sollen, wenn die Standardpräferenz der Erstellungskonfiguration überschrieben wird (die über ein Compiler-Flag festgelegt wird).

Natürlich wird es einen Leistungsunterschied geben, weil verschiedene Opcodes für die arithmetischen Operationen ausgegeben wurden (aber nicht, weil der Block betreten oder verlassen wurde). Überprüfte Arithmetik wird im Allgemeinen mit einem Overhead verglichen mit entsprechenden ungeprüften Arithmetikdaten erwartet.

als eine Angelegenheit der Tatsache, dieses C# Programm betrachten:

class Program 
{ 
    static void Main(string[] args) 
    { 
     var a = 1; 
     var b = 2; 
     int u1, c1, u2, c2; 

     Console.Write("unchecked add "); 
     unchecked 
     { 
      u1 = a + b; 
     } 
     Console.WriteLine(u1); 

     Console.Write("checked add "); 
     checked 
     { 
      c1 = a + b; 
     } 
     Console.WriteLine(c1); 

     Console.Write("unchecked call "); 
     unchecked 
     { 
      u2 = Add(a, b); 
     } 
     Console.WriteLine(u2); 

     Console.Write("checked call "); 
     checked 
     { 
      c2 = Add(a, b); 
     } 
     Console.WriteLine(c2); 
    } 

    static int Add(int a, int b) 
    { 
     return a + b; 
    } 
} 

Dies ist die erzeugte IL, mit Optimierungen auf und mit ungeprüften Arithmetik standardmäßig aktiviert:

.class private auto ansi beforefieldinit Checked.Program 
    extends [mscorlib]System.Object 
{  
    .method private hidebysig static int32 Add (
      int32 a, 
      int32 b 
     ) cil managed 
    { 
     IL_0000: ldarg.0 
     IL_0001: ldarg.1 
     IL_0002: add 
     IL_0003: ret 
    } 

    .method private hidebysig static void Main (
      string[] args 
     ) cil managed 
    { 
     .entrypoint 
     .locals init (
      [0] int32 b 
     ) 

     IL_0000: ldc.i4.1 
     IL_0001: ldc.i4.2 
     IL_0002: stloc.0 

     IL_0003: ldstr "unchecked add " 
     IL_0008: call void [mscorlib]System.Console::Write(string) 
     IL_000d: dup 
     IL_000e: ldloc.0 
     IL_000f: add 
     IL_0010: call void [mscorlib]System.Console::WriteLine(int32) 

     IL_0015: ldstr "checked add " 
     IL_001a: call void [mscorlib]System.Console::Write(string) 
     IL_001f: dup 
     IL_0020: ldloc.0 
     IL_0021: add.ovf 
     IL_0022: call void [mscorlib]System.Console::WriteLine(int32) 

     IL_0027: ldstr "unchecked call " 
     IL_002c: call void [mscorlib]System.Console::Write(string) 
     IL_0031: dup 
     IL_0032: ldloc.0 
     IL_0033: call int32 Checked.Program::Add(int32, int32) 
     IL_0038: call void [mscorlib]System.Console::WriteLine(int32) 

     IL_003d: ldstr "checked call " 
     IL_0042: call void [mscorlib]System.Console::Write(string) 
     IL_0047: ldloc.0 
     IL_0048: call int32 Checked.Program::Add(int32, int32) 
     IL_004d: call void [mscorlib]System.Console::WriteLine(int32) 

     IL_0052: ret 
    } 
} 

Wie Sie sehen können Die checked und unchecked Blöcke sind lediglich ein Quellcode-Konzept - es gibt keine IL, die beim Hin- und Herschalten zwischen dem (in der Quelle) checked und einem unchecked Kontext emittiert wird. Was sich ändert, sind die Operationscodes, die für direkte arithmetische Operationen (in diesem Fall add und add.ovf) ausgegeben wurden, die in diesen Blöcken textlich eingeschlossen waren. Die Spezifikation umfasst, welche Operationen betroffen sind:

Die folgenden Operationen durch den Überlauf überprüft Zusammenhang mit den geprüften und ungeprüften Operatoren und Anweisungen etabliert betroffen sind:

  • Die vordefinierte ++ und - unäre Operatoren (§7.6.9 und §7.7.5), wenn der Operand ein ganzzahliger Typ ist.
  • Der vordefinierte - unäre Operator (§7.7.2), wenn der Operand ein ganzzahliger Typ ist.
  • Die vordefinierten Operatoren +, -, * und/binary (§7.8), wenn beide Operanden ganzzahlige Typen sind.
  • Explizite numerische Konvertierungen (§6.2.1) von einem Integraltyp in einen anderen Integraltyp oder von Float oder Double in einen Integraltyp.

Und wie Sie sehen können, ist ein Verfahren aus einem Block checked oder unchecked genannt wird seinen Körper behalten und es wird keine Informationen über nicht erhalten, was Kontext aus aufgerufen wurde. Dies wird auch in der Spezifikation erläutert:

Die aktivierten und nicht aktivierten Operatoren wirken sich nur auf den Überlaufprüfkontext für die Operationen aus, die textlich in den Token "(" und ") enthalten sind. Die Operatoren haben keine Auswirkungen auf Funktionsmember, die als Ergebnis der Auswertung des enthaltenen Ausdrucks aufgerufen werden.

Im Beispiel

class Test 
{ 
    static int Multiply(int x, int y) { 
     return x * y; 
    } 
    static int F() { 
     return checked(Multiply(1000000, 1000000)); 
    } 
} 

die Verwendung der überprüften in F wirkt sich nicht auf die Auswertung von x * y in Multiplizieren, so x * y in der Standardüberlaufprüfung Kontext ausgewertet wird.

Wie bereits erwähnt, wurde die obige IL mit aktivierten C# -Compiler-Optimierungen generiert. Die gleichen Schlussfolgerungen können aus der IL gezogen werden, die ohne diese Optimierungen ausgegeben wird.

+5

Aktivieren Sie die Optimierung, Debuggen von MSIL ist aus Performance-Sicht nicht interessant. –

19

Wenn Sie wirklich den Unterschied sehen möchten, überprüfen Sie einige generierte IL. Lassen Sie uns ein sehr einfaches Beispiel:

using System; 

public class Program 
{ 
    public static void Main() 
    { 
     for(int i = 0; i < 10; i++) 
     { 
      var b = int.MaxValue + i; 
     } 
    } 
} 

Und wir bekommen:

.maxstack 2 
.locals init (int32 V_0, 
     int32 V_1, 
     bool V_2) 
IL_0000: nop 
IL_0001: ldc.i4.0 
IL_0002: stloc.0 
IL_0003: br.s  IL_0013 

IL_0005: nop 
IL_0006: ldc.i4  0x7fffffff 
IL_000b: ldloc.0 
IL_000c: add 
IL_000d: stloc.1 
IL_000e: nop 
IL_000f: ldloc.0 
IL_0010: ldc.i4.1 
IL_0011: add 
IL_0012: stloc.0 
IL_0013: ldloc.0 
IL_0014: ldc.i4.s 10 
IL_0016: clt 
IL_0018: stloc.2 
IL_0019: ldloc.2 
IL_001a: brtrue.s IL_0005 

IL_001c: ret 

Jetzt wollen wir sicherstellen, dass wir überprüft:

public class Program 
{ 
    public static void Main() 
    { 
     for(int i = 0; i < 10; i++) 
     { 
      checked 
      { 
       var b = int.MaxValue + i; 
      } 
     } 
    } 
} 

Und jetzt bekommen wir die folgende IL:

.maxstack 2 
.locals init (int32 V_0, 
     int32 V_1, 
     bool V_2) 
IL_0000: nop 
IL_0001: ldc.i4.0 
IL_0002: stloc.0 
IL_0003: br.s  IL_0015 

IL_0005: nop 
IL_0006: nop 
IL_0007: ldc.i4  0x7fffffff 
IL_000c: ldloc.0 
IL_000d: add.ovf 
IL_000e: stloc.1 
IL_000f: nop 
IL_0010: nop 
IL_0011: ldloc.0 
IL_0012: ldc.i4.1 
IL_0013: add 
IL_0014: stloc.0 
IL_0015: ldloc.0 
IL_0016: ldc.i4.s 10 
IL_0018: clt 
IL_001a: stloc.2 
IL_001b: ldloc.2 
IL_001c: brtrue.s IL_0005 

IL_001e: ret 

Wie Sie sehen können, das einzige Unterschied (mit Ausnahme von einigen zusätzlichen nop s) ist, dass unsere hinzufügen Operation add.ovf statt einer einfachen add. Der einzige Overhead, den Sie erhalten, ist der Unterschied zwischen diesen Vorgängen.

Nun, was passiert, wenn wir bewegen den checked Block die gesamte for Schleife umfassen:

public class Program 
{ 
    public static void Main() 
    { 
     checked 
     { 
      for(int i = 0; i < 10; i++) 
      { 
       var b = int.MaxValue + i; 
      } 
     } 
    } 
} 

wir die neuen IL erhalten:

.maxstack 2 
.locals init (int32 V_0, 
     int32 V_1, 
     bool V_2) 
IL_0000: nop 
IL_0001: nop 
IL_0002: ldc.i4.0 
IL_0003: stloc.0 
IL_0004: br.s  IL_0014 

IL_0006: nop 
IL_0007: ldc.i4  0x7fffffff 
IL_000c: ldloc.0 
IL_000d: add.ovf 
IL_000e: stloc.1 
IL_000f: nop 
IL_0010: ldloc.0 
IL_0011: ldc.i4.1 
IL_0012: add.ovf 
IL_0013: stloc.0 
IL_0014: ldloc.0 
IL_0015: ldc.i4.s 10 
IL_0017: clt 
IL_0019: stloc.2 
IL_001a: ldloc.2 
IL_001b: brtrue.s IL_0006 

IL_001d: nop 
IL_001e: ret 

Sie, dass beide der add Operationen sehen wurden in add.ovf anstatt nur die innere Operation umgewandelt, so dass Sie doppelt so viel "Overhead" bekommen. In jedem Fall, ich denke, der "Overhead" wäre für die meisten Anwendungsfälle vernachlässigbar.

+0

Aber wenn Sie die * Schleife * mit 'checked 'umbrechen, dann verwenden Sie' add.ovf' anstelle von 'add' für die Variable loop. –

+0

@MattBurland - Ich war dabei, diesen Fall hinzuzufügen, als Sie kommentiert haben. Siehe das Update. –

+0

Gibt es eine Optimierung, die der Generator beim Generieren des Maschinencodes vornimmt, wenn ein Codeblock nur ".ovf" arithmetische Anweisungen hat? Oder macht die CPU irgendwelche Optimierungen, wenn sie nur "geprüften" Maschinencode ausführt? Dies sind allgemeine Fragen: Es gibt viele Generatoren, Befehlssätze und CPUs, die berücksichtigt werden müssen. Meine Vermutung ist, dass die Antwort auf beide Fragen immer nein ist, aber die Möglichkeit unterstreicht, warum es in der Regel keinen Ersatz für zuverlässige Messungen gibt. –

1

Generell gibt es eine Kosten zwischen aktivierten und deaktivierten Modus zu wechseln?

Nein, nicht in Ihrem Beispiel. Der einzige Overhead ist der ++i.

sowohl in den Fällen C# Compiler add.ovf, sub.ovf, mul.ovf oder conv.ovf erzeugen.

Aber wenn die Schleife innerhalb geprüft Block ist, wird es eine zusätzliche add.ovf für ++i

6

Zusätzlich zu den Antworten werden oben ich klarstellen wollen, wie die Prüfung durchführen tut. Die einzige Methode, die ich kenne, ist die OF und CF Flags zu überprüfen. Das Flag CF wird durch vorzeichenlose arithmetische Anweisungen gesetzt, während OF durch vorzeichenbehaftete arithmetische Anweisungen gesetzt wird.

Diese Flags können mit den seto\setc Anweisungen oder (die am häufigsten verwendete Art und Weise) gelesen werden wir den jo\jc Sprungbefehl nur verwenden können, die an die gewünschte Adresse springen, wenn die OF\CF Flag gesetzt ist.

Aber es gibt ein Problem. jo\jc ist ein "bedingter" Sprung, der für die CPU-Pipeline ein totaler Schmerz ist. Also dachte ich, es gibt vielleicht noch einen anderen Weg das zu tun, wie ein spezielles Register zu setzen, um die Ausführung zu unterbrechen, wenn ein Überlauf entdeckt wird. Also beschloss ich herauszufinden, wie das JIT von Microsoft das macht.

Ich bin sicher, dass die meisten von Ihnen gehört haben, dass Microsoft die Teilmenge von .NET mit dem Namen .NET Core geöffnet hat. Der Quellcode von .NET Core enthält CoreCLR, also habe ich es eingegraben. Der Überlauferkennungscode wird in der CodeGen::genCheckOverflow(GenTreePtr tree)-Methode (Zeile 2484) generiert. Es ist deutlich zu sehen, dass die Anweisung jo für die Überschreibprüfung mit Vorzeichen und die jb (Überraschung!) Für den Überlauf ohne Vorzeichen verwendet wird. Ich habe seit langer Zeit nicht mehr im Assembly programmiert, aber es sieht so aus als ob jb und jc die gleichen Anweisungen sind (beide prüfen nur das Übertrags-Flag). Ich weiß nicht, warum die JIT-Entwickler sich dafür entschieden haben, jb anstelle von jc zu verwenden, denn wenn ich ein CPU-Hersteller wäre, würde ich einen Verzweigungs-Prädiktor annehmen, dass jo\jc Sprünge sehr unwahrscheinlich sind.

Zusammengefasst, es gibt keine zusätzlichen Anweisungen, die aufgerufen werden, um zwischen aktiviertem und deaktiviertem Modus zu wechseln, aber die arithmetischen Operationen in checked Block müssen deutlich langsamer sein, solange die Überprüfung nach jeder arithmetischen Anweisung durchgeführt wird. Ich bin mir aber ziemlich sicher, dass moderne CPUs damit gut umgehen können.

Ich hoffe es hilft.

+2

Sie sprechen gerade über die x86/x64-Architektur, oder? Ich denke, du solltest das explizit machen, .Net läuft auch auf anderen Architekturen. – svick

+2

Die "jb" und "jc" -Mnemoniks werden nach dem gleichen Bitmuster zusammengesetzt. Ich glaube, Intel bezieht sich auf die Anweisung, die springt, wenn carry als "jb" [Ziel ist niedriger als Quelle] und die, die springt, wenn carry nicht als "jae" [Ziel über oder gleich der Quelle] für Konsistenz mit festgelegt wird " ja "[Ziel ist über der Quelle, dh Übertragsmerker ist gelöscht und Z-Merker ist nicht gesetzt] und" jbe "[Zielwert unterhalb oder gleich der Quelle, dh Übertrag ist gesetzt oder Z-Merker ist gesetzt]. – supercat

+0

@supercat Ja, das macht Sinn, danke. –