2014-11-24 12 views
15

Sapevo già che l'impostazione di un campo è molto più lenta dell'impostazione di una variabile locale, ma sembra anche che l'impostazione di un campo con una variabile locale sia molto più lenta rispetto all'impostazione di una variabile locale con un campo. Perchè è questo? In entrambi i casi viene utilizzato l'indirizzo del campo.Perché impostare un campo molto più lentamente di ottenere un campo?

public class Test 
{ 
    public int A = 0; 
    public int B = 4; 

    public void Method1() // Set local with field 
    { 
     int a = A; 

     for (int i = 0; i < 100; i++) 
     { 
      a += B; 
     } 

     A = a; 
    } 

    public void Method2() // Set field with local 
    { 
     int b = B; 

     for (int i = 0; i < 100; i++) 
     { 
      A += b; 
     } 
    } 
} 

I risultati benchmark con 10e + 6 iterazioni sono:

 
Method1: 28.1321 ms 
Method2: 162.4528 ms 
+2

Dipende da un sacco di cose, ma la spiegazione più ovvia è che ottenere non deve accedere a DRAM (il valore nella cache della CPU), mentre l'impostazione fa (cache write-through ... cioè il valore viene scritto sia nella cache che nella memoria di sistema). Si noti che l'impostazione di una variabile locale potrebbe non consentire alcun accesso alla memoria, poiché il compilatore potrebbe aver ottimizzato la variabile locale in un registro. –

+0

@PeterDuniho - Pensavo che solo i locali fossero idonei per il caching della CPU? – toplel32

+0

Come ho accennato nel mio commento, i locali spesso non sono nemmeno memorizzati nella RAM di sistema. Tuttavia, l'accesso alla memoria, indipendentemente dal tipo di variabile, è idoneo per la memorizzazione nella cache. La cache non si cura (o nemmeno sa) del perché si stia utilizzando uno specifico indirizzo di memoria; memorizza tutti i dati che è in grado di memorizzare quando è coinvolta la memoria di sistema. –

risposta

15

L'esecuzione di questo sulla mia macchina, ottengo le differenze di tempo simili, però guardando il codice JITted per 10 milioni di iterazioni, è chiaro a vedere perché questo è il caso:

Metodo A:

mov  r8,rcx 
; "A" is loaded into eax 
mov  eax,dword ptr [r8+8] 
xor  edx,edx 
; "B" is loaded into ecx 
mov  ecx,dword ptr [r8+0Ch] 
nop  dword ptr [rax] 
loop_start: 
; Partially unrolled loop, all additions done in registers 
add  eax,ecx 
add  eax,ecx 
add  eax,ecx 
add  eax,ecx 
add  edx,4 
cmp  edx,989680h 
jl  loop_start 
; Store the sum in eax back to "A" 
mov  dword ptr [r8+8],eax 
ret 

e Metodo B:

; "B" is loaded into edx 
mov  edx,dword ptr [rcx+0Ch] 
xor  r8d,r8d 
nop word ptr [rax+rax] 
loop_start: 
; Partially unrolled loop, but each iteration requires reading "A" from memory 
; adding "B" to it, and then writing the new "A" back to memory. 
mov  eax,dword ptr [rcx+8] 
add  eax,edx 
mov  dword ptr [rcx+8],eax 
mov  eax,dword ptr [rcx+8] 
add  eax,edx 
mov  dword ptr [rcx+8],eax 
mov  eax,dword ptr [rcx+8] 
add  eax,edx 
mov  dword ptr [rcx+8],eax 
mov  eax,dword ptr [rcx+8] 
add  eax,edx 
mov  dword ptr [rcx+8],eax 
add  r8d,4 
cmp  r8d,989680h 
jl  loop_start 
rep ret 

Come si può vedere dal gruppo, Metodo a sta per essere significativamente più veloce in quanto i valori di a e B sono entrambi messi in registri, e tutto il aggiunte si verificano lì senza scritture intermedie alla memoria. Il metodo B invece utilizza un carico e memorizza su "A" in memoria per ogni singola iterazione.

2

Nel caso 1 a è chiaramente memorizzato in un registro. Qualsiasi altra cosa sarebbe un risultato di compilazione orribile.

Probabilmente, il JIT .NET non è disposto/in grado di convertire i negozi per A registrare negozi in caso 2.

Dubito che questo è costretto dal modello di memoria .NET perché altri thread possono mai dire la differenza tra i due metodi se osservano solo A come 0 o la somma. Non possono smentire la teoria secondo cui l'ottimizzazione non è mai avvenuta. Ciò lo rende consentito dalla semantica della macchina astratta .NET.

Non è sorprendente vedere .NET JIT eseguire piccole ottimizzazioni. Questo è ben noto ai follower del tag performance su Stack Overflow.

So per esperienza che il JIT ha una maggiore probabilità di memorizzare nella cache i carichi di memoria nei registri. Ecco perché il caso 1 (apparentemente) non accede a B ad ogni iterazione.

I calcoli registrati sono meno costosi dell'accesso alla memoria. Questo è vero anche se la memoria in questione si trova nella cache della CPU L1 (come nel caso qui).

Pensavo che solo i locali fossero idonei per il caching della CPU?

Questo non può essere così perché la CPU non sa nemmeno cosa sia un locale. Tutti gli indirizzi hanno lo stesso aspetto.

+0

La parte finale mi ha fatto meravigliare; esiste qualcosa come l'accesso al campo dopo la compilazione JIT? In caso contrario, l'accesso al campo A.B.C.D sarebbe veloce quanto l'accesso a A destra? – toplel32

+0

L'indirizzo di D può essere calcolato solo dopo aver navigato in A.B, B.C e B.D se questi sono tipi di riferimento. È costoso perché blocca la pipeline. – usr

+0

Se tutti questi tipi sono tipi di valore, l'offset di D in A è noto staticamente e l'accesso a D è veloce come qualsiasi altro campo. – usr

-2

method2: campo viene letto ~ 100x e impostare ~ 100x troppo = 200x larg_0 (questo) + 100x ldfld (campo di carico) + 100x stfld (campo set) + 100x ldloc (locale)

Method1: campo è leggi 100x ma non impostato equivale a method1 meno 100x ldarg_0 (this)

Problemi correlati