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;
}
}
}
}
Der "Stack" -Etotalierer verwendet fast sicher ein Register. Haben Sie sich die Demontage angesehen? – Blorgbeard
Ich weiß nur, die Antwort wird mit Cache-Treffern und Fehlschlägen zu tun sein – pm100
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