Come provare che .NET CLR JIT compila ogni metodo solo una volta per esecuzione?

C’è una vecchia domanda che chiede se C # viene JIT compilato ogni volta e la risposta del famoso Jon Skeet è: “no, è compilata solo una volta per applicazione” finché parliamo di applicazioni desktop che non sono NGENed.

Voglio sapere se queste informazioni del 2009 sono ancora vere e voglio scoprirlo sperimentando e facendo il debug, potenzialmente mettendo un breakpoint su JITter e usando i comandi di WinDbg per ispezionare oggetti e metodi.

La mia ricerca finora

So che il layout della memoria .NET considera un’intestazione (all’indirizzo A-4) e una tabella dei metodi (all’indirizzo A + 0) per object prima dell’avvio effettivo dei dati (all’indirizzo A + 4). Quindi sarebbe ansible che ogni object avesse una tabella di metodi diversa e quindi potesse avere metodi JITted diversi.

Perché ho dei dubbi sulla correttezza della dichiarazione?

Avevamo un workshop per la programmazione parallela e un claim del trainer era che i metodi sono JITted per ogni object per thread. Questo chiaramente non aveva senso per me e sono stato in grado di scrivere un’applicazione di esempio di contatore.

Sfortunatamente, sono venuti fuori i seguenti altri argomenti, per i quali voglio anche scrivere una dimostrazione:

  • nuovi framework .NET
  • domini applicativi
  • sicurezza di accesso al codice

La risposta collegata è stata scritta quando .NET 3.5 è stato rilasciato. Da allora non è cambiato sostanzialmente, ovvero non ha ricevuto aggiornamenti per .NET 4.0, 4.6 e 4.6.

Per quanto riguarda i domini delle applicazioni, la mia opinione personale è che potrei scaricare un dominio dell’applicazione, che scarica gli assembly. Se un assembly viene scaricato, non funziona e il codice IL lo accompagna. Non vedo molti benefici nel mantenere il codice nativo per il codice IL che è stato distrutto. Pertanto, potrei immaginare che la creazione di un dominio dell’applicazione e il caricamento di nuovo l’assembly possa comportare di nuovo il metodo JIT.

Per quanto riguarda la sicurezza di accesso al codice, non sono sicuro se sia considerato dal compilatore JIT in base alle autorizzazioni correnti o se sia eseguito da reflection in fase di esecuzione. Se è fatto dal compilatore JIT, il codice compilato sarà diverso, a seconda del set di autorizzazioni.

Il “principio di progettazione” del JIT è quello di compilare un metodo (una istanziazione del metodo per i metodi generici) una volta e riutilizzare lo stesso codice nativo. Naturalmente, l’implementazione è estremamente complicata e cercherò di semplificare la risposta senza compromettere l’accuratezza. La risposta è la stessa per tutte le versioni del runtime da .NET 2.0 e fino all’ultimo .NET 4.6 (non so su .NET 1.x, probabilmente lo stesso).

I callback del profiler di runtime e gli eventi ETW sono scarsamente documentati. Entrambi si verificano quando viene tentata la compilazione JIT, ma non necessariamente riuscita. Ci sono tre casi in cui ciò può accadere: 1- il metodo non riesce a soddisfare determinati requisiti di sicurezza, 2- la verifica del metodo non è riuscita e 3- memoria non può essere allocata per contenere il codice nativo da emettere. Pertanto, il JIT avvia la callback e l’evento può sovrastimare il numero di volte in cui un metodo è stato compilato. Allo stesso modo, la callback e l’evento di completamento del JIT sono imprecisi. Ci sono alcuni casi raramente in cui si potrebbe sottostimare il numero di volte in cui lo stesso metodo è stato compilato con successo. Vale la pena ricordare a questo punto che il contatore delle prestazioni JITted # dei metodi riporta esattamente il numero di volte in cui tutti i metodi IL sono stati compilati collettivamente in tutte le appdomain di un processo.

Per gli assembly specifici dell’app, i metodi vengono compilati separatamente in ciascun appdomain. Non c’è condivisione (anche se a volte può essere tecnicamente ansible). Per gli assembly neutrali appdomain, il runtime tenta di compilare ciascun metodo una volta e condividere il codice nativo con tutte le appdomain.

Come provare che .NET CLR JIT compila ogni metodo solo una volta per esecuzione?

Bene, in alcuni casi (come quando si usa il JIT in background e altri casi estremamente sottili), un metodo può essere compilato senza mai essere eseguito. Quindi dire che ogni metodo è compilato una volta per corsa è impreciso.

È ansible fare riferimento al codice sorgente JIT CoreCLR per ulteriori informazioni (il JIT è lo stesso utilizzato nel framework .NET 4.5+ ma questa risposta si applica alle versioni precedenti poiché il meccanismo di triggerszione JIT è per lo più lo stesso). Il codice sorgente è la prova.

i metodi sono JITted per ogni object per thread

Sì, non ha alcun senso. L’ambito della compilazione è appdomains.