2015-09-17 15 views
29

consideri un ciclo come questo:C'è un costo per entrare e uscire da un blocco C# controllato?

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

Se io so che io non sarà in overflow, ma voglio un controllo contro trabocco, troncamento, ecc, nella parte "fare qualcosa", sono io meglio con il blocco checked all'interno o all'esterno del ciclo?

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

o

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

Più in generale, c'è un costo per la commutazione tra modo controllato ed incontrollato?

+14

Dubito che 'checked' e' unschecked' lo facciano come blocchi. Probabilmente dicono semplicemente al compilatore di emettere un codice diverso nelle istruzioni aritmetiche che appaiono direttamente in esse. Ma lasciami ... controllare. –

+2

@ TheodorosChatzigiannakis Sono d'accordo, il blocco 'checked' emette semplicemente le istruzioni' .ovf'. –

+1

https://en.wikipedia.org/wiki/List_of_CIL_instructions Heh, @EldarDordzhiev mi ha battuto su di esso - le versioni '.ovf' delle istruzioni aritmetiche sono emesse - aggiungono un po 'di overhead contro usando" plain " istruzioni aritmetiche. – xxbbcc

risposta

9

checked e unchecked non vengono visualizzati a livello IL. Vengono utilizzati solo nel codice sorgente C# per indicare al compilatore se selezionare o meno le istruzioni IL di controllo o non di controllo quando si esegue l'override della preferenza predefinita della configurazione di build (impostata tramite un flag del compilatore).

Naturalmente, in genere ci sarà una differenza di prestazioni dovuta al fatto che sono stati emessi diversi codici opzionali per le operazioni aritmetiche (ma non per entrare o uscire dal blocco). In genere si prevede che l'aritmetica verificata abbia un overhead rispetto alla corrispondente aritmetica non controllata.

È un dato di fatto, considerano questo programma C#:

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; 
    } 
} 

Questa è la generata IL, con le ottimizzazioni attivate e con l'aritmetica incontrollato di default:

.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 
    } 
} 

Come si può vedere , i blocchi checked e unchecked sono semplicemente un concetto di codice sorgente: non vi è alcun IL emesso quando si passa avanti e indietro tra ciò che era (nella sorgente) un checked e un contesto unchecked. Ciò che cambia sono gli opcode emessi per le operazioni aritmetiche dirette (in questo caso, add e add.ovf) che sono stati racchiusi testualmente in quei blocchi. La specifica comprende che sono influenzati:

Le seguenti operazioni sono interessati dal contesto trabocco controllando stabilito dagli operatori controllati e incontrollati e dichiarazioni:

  • il predefinito ++ e - (unari §7.6.9 e §7.7.5), quando l'operando è di tipo integrale.
  • L'operatore predefinito - unario (§7.7.2), quando l'operando è di tipo integrale.
  • Gli operatori predefiniti +, -, * e/binari (§7.8), quando entrambi gli operandi sono di tipo integrale.
  • Conversioni numeriche esplicite (§6.2.1) da un tipo integrale a un altro tipo integrale o da variabile o doppio a un tipo integrale.

E come si può vedere, un metodo chiamato da un blocco checked o unchecked manterrà il suo corpo e non riceverà alcuna informazione su ciò che contesto è stato chiamato da. Questo è anche specificato nelle specifiche:

Gli operatori controllati e non controllati interessano solo il contesto di controllo di overflow per quelle operazioni che sono contenute testualmente nei token "(" e ")". Gli operatori non hanno alcun effetto sui membri delle funzioni invocati come risultato della valutazione dell'espressione contenuta.

Nell'esempio

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

l'uso controllato in F non pregiudica la valutazione di x * y in Moltiplicare, quindi x * y viene valutato nel contesto controllo troppopieno predefinito.

Come indicato, l'IL sopra riportato è stato generato con le ottimizzazioni del compilatore C# attivate. Le stesse conclusioni possono essere tratte dall'IL che viene emesso senza queste ottimizzazioni.

+5

Attivare l'ottimizzazione, eseguire il debug di MSIL non è interessante dal punto di vista delle prestazioni. –

19

Se si vuole veramente vedere la differenza, controllare alcuni IL generati. Prendiamo un esempio molto semplice:

using System; 

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

e otteniamo:

.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 

Ora, facciamo in modo stiamo controllato:

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

E ora otteniamo la seguente 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 

Come potete vedere, l'unico la differenza (ad eccezione di alcuni extra nop s) è che la nostra operazione di aggiunta emette add.ovf anziché un semplice add. L'unico sovraccarico che accumulerai è la differenza in quelle operazioni.

Ora, cosa succede se ci spostiamo il blocco checked per includere l'intero for ciclo:

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

otteniamo la nuova IL:

.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 

Si può vedere che entrambi i add operazioni sono stati convertiti in add.ovf anziché solo nell'operazione interna, in modo da ottenere il doppio del "sovraccarico". In ogni caso, immagino che il "sovraccarico" sarebbe trascurabile per la maggior parte dei casi d'uso.

+0

Ma se si avvolge il * ciclo * con 'checked', si finirà per usare' add.ovf' invece di 'add' per la variabile loop. –

+0

@ MattBurland - Ero in procinto di aggiungere quel caso quando hai commentato. Guarda l'aggiornamento. –

+0

Esiste qualche ottimizzazione che il generatore assume quando genera il codice macchina se un blocco di codice ha solo istruzioni aritmetiche '.ovf'? Oppure la CPU fa qualche ottimizzazione quando esegue solo il codice macchina "controllato"? Queste sono domande ampie: ci sono molti generatori, set di istruzioni e CPU da considerare. La mia ipotesi è che la risposta ad entrambe le domande è sempre no, ma la possibilità evidenzia perché di solito non esiste un sostituto per misurazioni affidabili. –

1

"Più in generale, c'è un costo per il passaggio tra la modalità controllato e incontrollato?"

No, non è nel tuo esempio. L'unico overhead è lo ++i.

In entrambi i casi C# compilatore genererà add.ovf, sub.ovf, mul.ovf o conv.ovf.

Ma quando il ciclo è controllato all'interno del blocco, ci sarà un ulteriore add.ovf per ++i

6

Oltre alle risposte sopra voglio chiarire come funziona il controllo di eseguire. L'unico metodo che conosco è controllare i flag OF e CF. Il flag CF viene impostato con istruzioni aritmetiche senza segno mentre OF viene impostato mediante istruzioni aritmetiche firmate.

Questi flag possono essere letti con le istruzioni seto\setc o (il modo più usato) possiamo semplicemente utilizzare l'istruzione jo\jc salto che saltare l'indirizzo desiderato, se è impostato il flag OF\CF.

Ma, c'è un problema. jo\jc è un salto "condizionale", che è un dolore totale nel *** per la pipeline della CPU. Quindi ho pensato che ci fosse un altro modo per farlo, come impostare un registro speciale per interrompere l'esecuzione quando viene rilevato un overflow, così ho deciso di scoprire come funziona il JIT di Microsoft.

Sono sicuro che la maggior parte di voi ha sentito che Microsoft ha aperto il sottoinsieme di .NET, denominato .NET Core. Il codice sorgente di .NET Core include CoreCLR, quindi l'ho inserito. Il codice di rilevamento dell'overflow viene generato nel metodo CodeGen::genCheckOverflow(GenTreePtr tree) (riga 2484). Si può chiaramente vedere che l'istruzione jo viene utilizzata per il controllo di overflow con segno e lo jb (sorpresa!) Per l'overflow senza segno. Non ho programmato in assembly per un lungo periodo, ma sembra che jb e jc siano le stesse istruzioni (entrambi controllano solo il flag di trasporto). Non so perché gli sviluppatori JIT abbiano deciso di utilizzare jb invece di jc perché se fossi un produttore di CPU, farei in modo che un predittore di ramo presuma i salti jo\jc come molto improbabile.

In sintesi, non ci sono ulteriori istruzioni invocate per passare dalla modalità controllato e incontrollato, ma le operazioni aritmetiche in checked blocco deve essere notevolmente più lento, fino a quando il controllo viene eseguito dopo ogni istruzione aritmetica. Tuttavia, sono abbastanza sicuro che le moderne CPU possano gestirle bene.

Spero che aiuti.

+2

Stai parlando specificamente dell'architettura x86/x64, giusto? Penso che dovresti renderlo esplicito, .Net funziona anche su altre architetture. – svick

+2

I mnemonici "jb" e "jc" si assemblano sullo stesso modello di bit. Credo che Intel si riferisca all'istruzione che salta quando carry è impostata come "jb" [la destinazione è inferiore alla sorgente] e quella che salta se carry non è impostata come "jae" [destinazione sopra o uguale alla sorgente] per coerenza con " ja "[la destinazione è al di sopra della sorgente, ovvero flag di carry è chiaro e la flag Z non è impostata] e" jbe "[destinazione inferiore o uguale alla sorgente, ovvero carry è impostato o è impostato Z flag]. – supercat

+0

@supercat Sì, questo ha senso, grazie. –