2013-03-04 3 views
15

qui qualche programma di test in C#:Perché compilatore C# in alcuni casi emette newobj/stobj anziche 'chiamare esempio .ctor' per struct di inizializzazione

using System; 


struct Foo { 
    int x; 
    public Foo(int x) { 
     this.x = x; 
    } 
    public override string ToString() { 
     return x.ToString(); 
    } 
} 

class Program { 
    static void PrintFoo(ref Foo foo) { 
     Console.WriteLine(foo); 
    } 

    static void Main(string[] args) { 
     Foo foo1 = new Foo(10); 
     Foo foo2 = new Foo(20); 

     Console.WriteLine(foo1); 
     PrintFoo(ref foo2); 
    } 
} 

e qui smontata versione compilata di metodo Main:

.method private hidebysig static void Main (string[] args) cil managed { 
    // Method begins at RVA 0x2078 
    // Code size 42 (0x2a) 
    .maxstack 2 
    .entrypoint 
    .locals init (
     [0] valuetype Foo foo1, 
     [1] valuetype Foo foo2 
    ) 

    IL_0000: ldloca.s foo1 
    IL_0002: ldc.i4.s 10 
    IL_0004: call instance void Foo::.ctor(int32) 
    IL_0009: ldloca.s foo2 
    IL_000b: ldc.i4.s 20 
    IL_000d: newobj instance void Foo::.ctor(int32) 
    IL_0012: stobj Foo 
    IL_0017: ldloc.0 
    IL_0018: box Foo 
    IL_001d: call void [mscorlib]System.Console::WriteLine(object) 
    IL_0022: ldloca.s foo2 
    IL_0024: call void Program::PrintFoo(valuetype Foo&) 
    IL_0029: ret 
} // end of method Program::Main 

Non capisco perché newobj/stobj è stato emesso invece di semplice chiamata .ctor? Per renderlo più misterioso, newobj + stobj ottimizzato da JIT-compilatore in modalità a 32 bit per una chiamata ctor, ma non lo fa in modalità a 64 bit ...

UPDATE:

Per chiarire la mia confusione, di seguito sono le mie aspettative.

value-type espressione dichiarazione come

Foo foo = new Foo(10)

è elaborato tramite

call instance void Foo::.ctor(int32)

value-type espressione dichiarazione come

Foo foo = default(Foo)

dovrebbe essere compilato tramite

initobj Foo

a mio variabile TEMP parere in caso di espressione di costruzione, o l'istanza di espressione di default dovrebbe essere considerato come variabile di destinazione, come questo non poteva seguire per ogni comportamento pericoloso

try{ 
    //foo invisible here 
    ... 
    Foo foo = new Foo(10); 
    //we never get here, if something goes wrong 
}catch(...){ 
    //foo invisible here 
}finally{ 
    //foo invisible here 
} 

assegnazione espressione come

foo = new Foo(10); // foo declared somewhere before

è elaborato per qualcosa di simile:

.locals init (
    ... 
    valuetype Foo __temp, 
    ... 
) 

... 
ldloca __temp 
ldc.i4 10 
call instance void Foo::.ctor(int32) 
ldloc __temp 
stloc foo 
... 

questo il modo in cui ho capito che cosa dice C# specifica:

7.6.10.1 creazione oggetto espressioni

...

L'elaborazione in fase di esecuzione di un'espressione di creazione dell'oggetto della forma new T (A), in cui T è di tipo classe o struct-type e A è un argomento-elenco facoltativo, consiste dei seguenti passaggi:

...

Se T è una struttura di tipo:

  • un'istanza di tipo T viene creato allocando un temporaneo variabile locale.Dal momento che un costruttore di una struct tipo istanza è necessario assegnare sicuramente un valore per ciascun campo dell'istanza create, nessuna inizializzazione della variabile temporanea è necessaria.

  • Il costruttore di istanza è invocato secondo le regole del membro invocazione funzione (§7.5.4). Un riferimento all'istanza appena assegnata viene automaticamente passato al costruttore dell'istanza e l'istanza può essere richiamata all'interno di tale costruttore come questo.

Voglio fare l'accento su "l'assegnazione di una variabile temporanea locale". e nella mia comprensione l'istruzione newobj presuppone la creazione dell'oggetto su heap ...

La dipendenza dalla creazione dell'oggetto da come è stata usata mi ha reso in questo caso, come foo1 e foo2 mi sembrano identici.

+0

possibile duplicato del [Differenza tra istanza chiamata vs esempio newobj in IL] (http://stackoverflow.com/questions/11966930/difference-between-call-instance-vs-newobj-instance-in -il) –

+2

C l'uriosità è okay, ma questo richiede un significativo scavo. Scarica SSCLI20 e guardare il codice sorgente del compilatore C#, csharp/sscomp/ilgen.cpp, metodo ILGENREC :: genCall(). Qualcosa a che fare con il possibile aliasing, penso. –

+1

@HansPassant: sei corretto; l'ottimizzatore C# sta saltando la copia elision perché è preoccupato del possibile aliasing. In questo particolare codice che è un'ipotesi eccessivamente prudente; in realtà non c'è alcun problema di aliasing qui. Ma piuttosto che fare che l'analisi, il compilatore C# rileva l'arbitro e si arrende sull'ottimizzazione elision presto. –

risposta

22

Prima di tutto, si dovrebbe leggere il mio articolo su questo argomento. Non affronta lo scenario specifica, ma ha alcune buone informazioni di base:

http://blogs.msdn.com/b/ericlippert/archive/2010/10/11/debunking-another-myth-about-value-types.aspx

OK, ora che avete letto che si sa che la specifica C# afferma che la costruzione di un'istanza di una struct ha queste semantiche:

  • Creare una variabile temporanea per memorizzare il valore della struct, inizializzato sul valore predefinito della struct.
  • passare un riferimento a quella variabile temporanea come il "questo" del costruttore

Quindi, quando si dice:

Foo foo = new Foo(123); 

Questo è equivalente a:

Foo foo; 
Foo temp = default(Foo); 
Foo.ctor(ref temp, 123); // "this" is a ref to a variable in a struct. 
foo1 = temp; 

Ora, si potrebbe chiedere perché passare attraverso tutti i problemi di assegnazione di una temporanea quando abbiamo già abbiamo una variabile foo proprio lì che potrebbe essere this:

Foo foo = default(Foo); 
Foo.ctor(ref foo, 123); 

che l'ottimizzazione è chiamato copia elision. Il compilatore C# e/o il jitter sono autorizzati ad eseguire una copia elisione quando determinano usando i loro euristiche che così facendo è sempre invisibile. Vi sono rare circostanze in cui una copia dell'elisione può causare una modifica osservabile nel programma, e in questi casi l'ottimizzazione non deve essere utilizzata. Ad esempio, supponiamo di avere un paio-of-int struct:

Pair p = default(Pair); 
try { p = new Pair(10, 20); } catch {} 
Console.WriteLine(p.First); 
Console.WriteLine(p.Second); 

Ci aspettiamo che p qui è o (0, 0) o (10, 20), mai (10, 0) o (0, 20), anche se il ctor getta a metà. Cioè, l'assegnazione a p era del valore completamente costruito, o non è stata apportata alcuna modifica a p. La copia elisione non può essere eseguita qui; dobbiamo fare una temporanea, passare il temporaneo al ctor, e quindi copiare il temporaneo p.

Allo stesso modo, supponiamo che abbiamo avuto questa follia:

Pair p = default(Pair); 
p = new Pair(10, 20, ref p); 
Console.WriteLine(p.First); 
Console.WriteLine(p.Second); 

Se il compilatore C# esegue l'elisione copia poi this e ref p sono entrambi gli alias a p, che è palesemente diverso se this è un alias a una temporanea ! Il Ctor potrebbe osservare che le modifiche a this causano modifiche a ref p se alias la stessa variabile, ma non osserverebbero se aliasere variabili diverse.

L'euristica del compilatore C# sta decidendo di eseguire la copia elisione su foo1 ma non su foo2 nel programma. Sta vedendo che c'è un ref foo2 nel tuo metodo e decidere proprio lì di mollare. Potrebbe fare un'analisi più sofisticata per determinare che è non in una di queste pazze situazioni di aliasing, ma non è così. La cosa semplice ed economica è semplicemente saltare l'ottimizzazione se c'è qualche possibilità, per quanto remota, che ci possa essere una situazione di aliasing che renda visibile l'elision. Genera il codice newobj e lascia che il jitter decida se vuole eseguire l'elision.

Come per il jitter: i jitter a 64 bit e 32 bit hanno ottimizzatori completamente diversi. Apparentemente uno di loro sta decidendo che può introdurre la copia elision che il compilatore C# non ha, e l'altra no.

+0

grazie, ho trovato il tuo articolo molto cognitivo, ma penso che ci dovrebbe essere distinzione tra "espressione di assegnazione" e "espressione di dichiarazione" e in ultimo caso non vedo alcun caso pericoloso, scrivo aggiornamento alla mia domanda come mi sembra –

+0

Non ci sono casi pericolosi. Il compilatore semplicemente non sta rilevando che c'è un ottimizzazione che potrebbe eseguire. Il compilatore non è tenuto a generare codice ottimale, è solo richiesto per generare il codice corretto. –

0

Questo perché le variabili foo1 e foo2 sono diversi.

La variabile foo1 è solo un valore, ma la variabile foo2 è sia un valore che un puntatore poiché viene utilizzata in una chiamata con la parola chiave ref.

Quando la variabile foo2 viene inizializzata, il puntatore viene impostato per puntare al valore e il costruttore viene chiamato con il valore del puntatore anziché l'indirizzo del valore.

Se si imposta due PrintFoo metodi con l'unica differenza che si ha la parola chiave ref, e li chiami con una variabile ogni:

Foo a = new Foo(10); 
Foo b = new Foo(20); 
PrintFoo(ref a); 
PrintFoo(b); 

Se decompilare il codice generato, la differenza tra le variabili è visibile:

&Foo a = new Foo(10); 
Foo b = new Foo(20); 
Program.PrintFoo(ref a); 
Program.PrintFoo(b); 
+0

Non sono sicuro che seguirò il tuo ragionamento qui. Nell'IL generato nella domanda originale i tipi di slot locali zero e uno sono entrambi Foo; nessuno dei due è gestito-pointer-to-Foo. Il puntatore gestito viene creato dall'opcode load-local-address. –

+0

@EricLippert: Sì, ma appare chiaramente come un diverso tipo di dati nel codice decompilato, e se si guarda il codice generato ci sono due slot allocati nello stack per 'foo2' e uno solo per' foo1'. – Guffa

+1

Immagino che quello che sto ottenendo qui sia: la domanda riguarda perché il compilatore C# produce una particolare sequenza di IL. Appellandosi a ciò che produce un decompilatore di terze parti quando viene dato che IL non spiega perché il compilatore C# sta scegliendo di generare quell'IL in primo luogo. Il punto della domanda è che non è necessario inizializzare foo1 e foo2 in modo diverso. –

Problemi correlati