2016-05-23 15 views
3

Im folgenden C# -Programm, das im Visual Studio 2015 Update 2 x 64 Release-Modus auf einer Broadwell-CPU und Windows 8.1 kompiliert wurde, werden zwei Varianten eines Benchmarks ausgeführt. Beide machen dasselbe - insgesamt fünf Millionen ganze Zahlen in einem Array.Warum ist diese aufsummierende Operation auf dem Stack schneller als der Heap?

Der Unterschied zwischen den beiden Benchmarks ist, dass eine Version die laufende Summe (eine einzelne lange) auf dem Stapel hält und die andere hält sie auf dem Haufen. In beiden Versionen findet keine Zuordnung statt; Beim Scannen entlang eines Arrays wird eine Summe hinzugefügt.

Im Test sehe ich einen konsistenten signifikanten Leistungsunterschied zwischen der Benchmark-Variante mit der Summe auf dem Heap und dem auf dem Stack. Bei einigen Testgrößen ist es dreimal langsamer, wenn sich die Summe auf dem Heap befindet.

Warum gibt es eine solche Leistungsabweichung zwischen den beiden Speicherorten für die Summe?

using System; 
using System.Diagnostics; 

namespace StackHeap 
{ 
    class StackvHeap 
    { 
     static void Main(string[] args) 
     { 
      double stackAvgms, heapAvgms; 

      // Warmup 
      runBenchmark(out stackAvgms, out heapAvgms); 

      // Run 
      runBenchmark(out stackAvgms, out heapAvgms); 

      Console.WriteLine($"Stack avg: {stackAvgms} ms\nHeap avg: {heapAvgms} ms"); 
     } 

     private static void runBenchmark(out double stackAvgms, out double heapAvgms) 
     { 
      Benchmarker b = new Benchmarker(); 
      long stackTotalms = 0; 
      int trials = 100; 
      for (int i = 0; i < trials; ++i) 
      { 
       stackTotalms += b.stackTotaler(); 
      } 
      long heapTotalms = 0; 
      for (int i = 0; i < trials; ++i) 
      { 
       heapTotalms += b.heapTotaler(); 
      } 

      stackAvgms = stackTotalms/(double)trials; 
      heapAvgms = heapTotalms/(double)trials; 
     } 
    } 

    class Benchmarker 
    { 
     long heapTotal; 
     int[] vals = new int[5000000]; 

     public long heapTotaler() 
     { 
      setup(); 
      var stopWatch = new Stopwatch(); 
      stopWatch.Start(); 

      for (int i = 0; i < vals.Length; ++i) 
      { 
       heapTotal += vals[i]; 
      } 
      stopWatch.Stop(); 
      //Console.WriteLine($"{stopWatch.ElapsedMilliseconds} milliseconds with the counter on the heap"); 
      return stopWatch.ElapsedMilliseconds; 
     } 

     public long stackTotaler() 
     { 
      setup(); 
      var stopWatch = new Stopwatch(); 
      stopWatch.Start(); 

      long stackTotal = 0; 
      for (int i = 0; i < vals.Length; ++i) 
      { 
       stackTotal += vals[i]; 
      } 
      stopWatch.Stop(); 
      //Console.WriteLine($"{stopWatch.ElapsedMilliseconds} milliseconds with the counter on the stack"); 
      return stopWatch.ElapsedMilliseconds; 
     } 

     private void setup() 
     { 
      heapTotal = 0; 
      for (int i = 0; i < vals.Length; ++i) 
      { 
       vals[i] = i; 
      } 
     } 
    } 
} 
+0

Der "Stack" -Etotalierer verwendet fast sicher ein Register. Haben Sie sich die Demontage angesehen? – Blorgbeard

+0

Ich weiß nur, die Antwort wird mit Cache-Treffern und Fehlschlägen zu tun sein – pm100

+1

Die MSIL Opcodes beteiligt für lokale Variablen ['ldloc'] (https://msdn.microsoft.com/en-us/library/system.reflection .emit.opcodes.ldloc% 28v = vs.110% 29.aspx) und ['stloc'] (https://msdn.microsoft.com/en-us/library/system.reflection.emit.opcodes.stloc% 28v = vs.110% 29.aspx) haben weniger zu tun als ['ldfld'] (https://msdn.microsoft.com/en-us/library/system.reflection.emit.opcodes.ldfld%28v=vs .110% 29.aspx) und ['stfld'] (https://msdn.microsoft.com/en-us/library/system.reflection.emit.opcodes.stfld%28v=vs.110%29.aspx) (um auf Felder zuzugreifen und zu schreiben). Schau dir an, was mit dem Stack für jeden von diesen passiert. – spender

Antwort

4

Mit einigen Testgrößen ist es dreimal langsamer

, dass die einzige Cue auf das zugrunde liegende Problem. Wenn Sie sich für long Variablen interessieren, dann verwenden Sie nicht den x86-Jitter. Die Ausrichtung ist entscheidend und im 32-Bit-Modus kann keine ausreichende Ausrichtungsgarantie erzielt werden.

Die CLR kann dann nur auf 4 ausrichten, das gibt einem solchen Test 3 eindeutige Ergebnisse. Die Variable kann auf 8, die schnelle Version, ausgerichtet werden. Und zu 4 innerhalb einer Cache-Zeile falsch ausgerichtet, etwa 2 mal langsamer. Und zu 4 falsch ausgerichtet und die L1-Cache-Zeilengrenze überbrückend, etwa 3 mal langsamer. Gleiche Art von Problem mit Doppel BTW.

Verwenden Sie Projekt> Eigenschaften> Registerkarte Erstellen> deaktivieren Sie das Kontrollkästchen "32-Bit-Modus bevorzugen". Nur für den Fall, verwenden Sie Extras> Optionen> Debugging> Allgemein> deaktivieren Sie die "JIT Optimierung unterdrücken". Tweak den Benchmark-Code, legen Sie eine for-Schleife um den Code, ich es immer mindestens 10 mal ausführen. Wählen Sie die Konfiguration des Freigabemodus und führen Sie die Tests erneut aus.

Sie haben jetzt eine ganz andere Frage, wahrscheinlich mehr nach Ihren Erwartungen. Ja, lokale Variablen sind nicht flüchtige standardmäßig Felder sind. Aktualisieren müssen heapTotal innerhalb der Schleife ist der Overhead, den Sie sehen.

+0

Zwei _separate_ Antworten in einem - beide neu für mich. Ich wusste nicht, dass die 32-Bit-CLR nur 4-Byte-Ausrichtung hat und dass Felder standardmäßig nicht flüchtig sind. Vielen Dank! ([Dieser Artikel] (http://joeduffyblog.com/2007/01/22/guaranteeing-clr-data-alignment-at-n-byte-boundaries/) bestätigt 4-Byte-Ausrichtung auf 32-Bit-CLR.) (Aber Ich kann keine ähnliche Referenz für Felder angeben, die standardmäßig flüchtig sind - können Sie eine angeben?) – davidbak

+0

(In der Tat, [diese Antwort von Ihnen] (http://stackoverflow.com/a/20350848/751579) impliziert sie " Kannst du das nicht klarstellen? Habe ich diese Antwort falsch gelesen?) – davidbak

+0

Seufz, das Elend der Flüchtigen. Joe Duffy gab es auf, nicht sicher, warum ich seinen Rücken decken muss. Es ist jedoch ein sehr einfacher Fall hier, das Feld ist an anderer Stelle sichtbar, so dass es aktualisiert werden muss. –

2

Dies ist aus der heapTotaller Demontage:

  heapTotal = 0; 
000007FE99F34966 xor   ecx,ecx 
000007FE99F34968 mov   qword ptr [rsi+10h],rcx 
      for (int i = 0; i < vals.Length; ++i) 
000007FE99F3496C mov   rax,qword ptr [rsi+8] 
000007FE99F34970 mov   edx,dword ptr [rax+8] 
000007FE99F34973 test  edx,edx 
000007FE99F34975 jle   000007FE99F34993 
      { 
       heapTotal += vals[i]; 
000007FE99F34977 mov   r8,rax 
000007FE99F3497A cmp   ecx,edx 
000007FE99F3497C jae   000007FE99F349C8 
000007FE99F3497E movsxd  r9,ecx 
000007FE99F34981 mov   r8d,dword ptr [r8+r9*4+10h] 
000007FE99F34986 movsxd  r8,r8d 
000007FE99F34989 add   qword ptr [rsi+10h],r8 

Sie sehen es [rsi+10h] für die heapTotal Variable verwendet wird.

Und das ist aus der stackTotaller:

  long stackTotal = 0; 
000007FE99F3427A xor   ecx,ecx 
      for (int i = 0; i < vals.Length; ++i) 
000007FE99F3427C xor   eax,eax 
000007FE99F3427E mov   rdx,qword ptr [rsi+8] 
000007FE99F34282 mov   r8d,dword ptr [rdx+8] 
000007FE99F34286 test  r8d,r8d 
000007FE99F34289 jle   000007FE99F342A8 
      { 
       stackTotal += vals[i]; 
000007FE99F3428B mov   r9,rdx 
000007FE99F3428E cmp   eax,r8d 
000007FE99F34291 jae   000007FE99F342DD 
000007FE99F34293 movsxd  r10,eax 
000007FE99F34296 mov   r9d,dword ptr [r9+r10*4+10h] 
000007FE99F3429B movsxd  r9,r9d 
000007FE99F3429E add   rcx,r9 

Sie können sehen, wie der JIT den Code optimiert: es ist mit dem RCX Register für heapTotal.

Register sind schneller als Speicherzugriff, daher die Geschwindigkeitsverbesserung.