2014-11-24 17 views
15

Ich wusste bereits, dass das Setzen eines Feldes viel langsamer ist als das Setzen einer lokalen Variablen, aber es scheint auch, dass das Setzen eines Feldes mit eine lokale Variable ist viel langsamer als das Setzen einer lokalen Variablen mit einem Feld. Warum ist das? In jedem Fall wird die Adresse des Feldes verwendet.Warum wird ein Feld viel langsamer eingestellt als ein Feld?

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

Die Benchmark-Ergebnisse mit 10e + 6 Iterationen sind:

 
Method1: 28.1321 ms 
Method2: 162.4528 ms 
+2

Es hängt von vielen Dingen ab, aber die offensichtlichste Erklärung ist, dass man nicht auf DRAM zugreifen muss (der Wert ist im CPU-Cache)), während setting (cache write-through ... dh der Wert wird sowohl in den Cache als auch in den Systemspeicher geschrieben). Beachten Sie, dass das Setzen einer lokalen Variablen möglicherweise zu keinem Speicherzugriff führt, da der Compiler die lokale Variable möglicherweise in einem Register optimiert hat. –

+0

@PeterDuniho - Ich dachte, nur Einheimische könnten CPU-Caching durchführen? – toplel32

+0

Wie ich in meinem Kommentar erwähne, sind Einheimische oft nicht einmal im System-RAM gespeichert. Aber _all_ Speicherzugriff, unabhängig vom Typ der Variablen, ist für Caching geeignet. Der Cache interessiert nicht (oder weiß nicht), warum Sie eine bestimmte Speicheradresse verwenden. Es speichert alle möglichen Daten im Systemspeicher. –

Antwort

15

Ausführen dieses auf meinem Rechner habe ich ähnliche Zeitdifferenzen erhalten, aber am JITted Code für 10M Iterationen, es ist klar zu sehen, warum dies der Fall ist:

Methode 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 

und Methode 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 

Wie Sie aus der Montage sehen können, Methode A wird deutlich schneller sein, da die Werte von A und B beide in den Registern gesetzt werden, und alle der Hinzufügungen treten dort ohne Zwischenschreibvorgänge in den Speicher auf. Methode B hingegen belastet und speichert für jede einzelne Iteration im Speicher.

2

Im Fall 1 a in einem Register gespeichert ist, eindeutig. Alles andere wäre ein schreckliches Ergebnis der Kompilation.

Wahrscheinlich ist das .NET JIT nicht bereit/Lage, die Geschäfte zu A konvertieren speichert im Fall registriert 2.

Ich bezweifle, dass dies von .NET-Speicher-Modell gezwungen ist, weil andere Threads das nie sagen können, Unterschied zwischen Ihren beiden Methoden, wenn sie nur A zu 0 oder die Summe zu beobachten. Sie können die Theorie, dass die Optimierung nie stattgefunden hat, nicht widerlegen. Das erlaubt es unter der Semantik der abstrakten .NET-Maschine.

Es ist nicht überraschend, dass der .NET JIT wenig Optimierungen durchführt. Dies ist den Followern des Tags performance auf Stack Overflow gut bekannt.

Ich weiß aus Erfahrung, dass das JIT viel häufiger Speicherlasten in Registern zwischenspeichert. Deshalb greift Fall 1 (anscheinend) nicht bei jeder Iteration auf B zu.

Registerberechnungen sind billiger als Speicherzugriffe. Dies gilt sogar dann, wenn sich der betreffende Speicher im CPU-L1-Cache befindet (wie es hier der Fall ist).

Ich dachte nur Einheimische waren für CPU-Caching geeignet?

Das kann nicht so sein, weil die CPU nicht einmal weiß, was ein lokaler ist. Alle Adressen sehen gleich aus.

+0

Der letzte Teil ließ mich wundern; Gibt es nach der JIT-Compilation überhaupt einen Feldzugriff? Wenn nicht, dann wäre der Zugriff auf das Feld A.B.C.D so schnell wie nur auf A zuzugreifen? – toplel32

+0

Die Adresse von D kann nur nach Navigieren durch A.B, B.C und B.D berechnet werden, wenn es sich um Referenztypen handelt. Das ist teuer, weil es die Pipeline blockiert. – usr

+0

Wenn all diese Typen Werttypen sind, ist der Offset von D in A statisch bekannt und der Zugriff auf D ist so schnell wie jedes andere Feld. – usr

-2

method2: Feld gelesen ~ 100x und setzen ~ 100x zu = 200x larg_0 (this) + 100x ldfld (Lastfeld) + 100x stfld (set Feld) + 100x ldloc (local)

method1: Feld 100x lesen aber nicht setzen es ist äquivalent zu methode1 minus 100x ldarg_0 (das)

Verwandte Themen