为什么堆栈上的总计操作比堆快?

时间:2016-05-23 21:54:05

标签: c# performance stack heap

在Broadwell CPU和Windows 8.1上以Visual Studio 2015 Update 2 x64 Release模式编译的下面的C#程序中,运行了两个基准测试变体。它们都做同样的事情 - 数组中总共有500万个整数。

两个基准测试之间的区别在于,一个版本保持堆栈上的运行总计(单个长整数),另一个版本将其保留在堆上。两个版本都没有分配;在沿阵列扫描时添加总数。

在测试中,我发现基准测试版本与堆上的总数和堆栈上的总数之间存在一致的显着性能差异。对于一些测试大小,当总数在堆上时,它的速度要慢三倍。

为什么两个内存位置之间存在性能差异?

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

2 个答案:

答案 0 :(得分:4)

  

对于某些测试尺寸,它的速度要慢三倍

这是解决潜在问题的唯一线索。如果您关心 long 变量的perf,请不要使用x86抖动。对齐非常重要,您无法在32位模式下获得足够好的对齐保证。

然后,CLR只能与4对齐,从而给出这样的测试3个不同的结果。变量可以与快速版本8对齐。并且在缓存行中未对齐到4,大约慢2倍。未对齐到4并跨越L1缓存线边界,大约慢3倍。与 double btw相同的问题。

使用项目&gt;属性&gt;构建标签&gt;取消&#34;首选32位模式&#34;复选框。以防万一,使用工具&gt;选项&gt;调试&gt;一般&gt;取消&#34;抑制JIT优化&#34;。调整基准代码,在代码周围放置一个for循环,我总是运行它至少10次。选择Release模式配置并再次运行测试。

你现在有一个完全不同的问题,可能更符合你的期望。是的,默认情况下,局部变量不是 volatile ,字段是。必须在循环中更新 heapTotal 是你看到的开销。

答案 1 :(得分:2)

这来自heapTotaller反汇编:

            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  

您可以看到[rsi+10h]变量使用heapTotal

这来自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  

您可以看到JIT优化了代码:它使用RCX的{​​{1}}寄存器。

寄存器比内存访问更快,因此提高了速度。