C# 方法分配不必要的堆栈 space?

C# method allocating unnecessary stack space?

我试图理解我在某些 C# 代码中看到的某些行为,而不考虑这是否是应用程序 应该 的编写方式。基本上,考虑以下代码:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace StackTest
{
    class MyClass
    {
        private int x;

        public MyClass(int x)
        {
            this.x = x;
        }
    }

    class DictClass
    {
        private Dictionary<Guid, MyClass> m_dict;
        private Dictionary<int, MyClass> m_intDict;

        public DictClass()
        {
            m_dict = new Dictionary<Guid, MyClass>();
            m_intDict = new Dictionary<int, MyClass>();
            Init(m_dict, m_intDict);
        }

        public void Init(
            Dictionary<Guid, MyClass> dict,
            Dictionary<int, MyClass> intDict)
        {
            int index = 0;
            MyClass obj;

            // BEGIN REPEATED_FRAGMENT
            ++index;
            obj = new MyClass(index);
            dict.Add(Guid.NewGuid(), obj);
            intDict.Add(index, obj);
            // END REPEATED_FRAGMENT

            // Repeat REPEATED_FRAGMENT about 1400 times
        }

        public override string ToString()
        {
            return m_dict.Values.First().ToString();
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            var dc = new DictClass();
            Console.WriteLine(dc);
        }
    }
}

在方法 Init 中,似乎在堆栈上分配的 space 远远多于应有的分配。以下内容出现在方法的反汇编 window 中,在任何实际的 C# 语句之前:

03720568  push        ebp  
03720569  mov         ebp,esp  
0372056B  push        edi  
0372056C  push        esi  
0372056D  push        ebx  
0372056E  test        dword ptr [esp-1000h],eax  
03720575  test        dword ptr [esp-2000h],eax  
0372057C  sub         esp,2C7Ch  
03720582  mov         esi,ecx  
...and so on...

如果我没看错的话,它正在为具有 2 个参数和 2 个局部变量以及少量临时变量的方法分配大约 11 KB 的堆栈 space。我的问题是:

  1. 我没看错吗?
  2. 如果 1 是,那么为什么要分配所有 space?

同样,现在并不真正关心您是否真的应该以这种方式编写代码。只是好奇发生了什么。

你是如何检查反汇编的?使用 Visual Studio?或者像 Windbg 这样的低级调试器?

我问,因为查看整个反汇编方法,很明显堆栈 space 被用于每次调用 new MyClass(index)dict.Add(...) 的临时存储。例如,这是我在第一部分看到的内容(注意粗体参数):

    39:             ++index;
07980082  inc         dword ptr [ebp-0Ch]  
    40:             obj = new MyClass(index);
07980085  mov         ecx,2EA4E30h  
0798008A  call        02E930F4  
0798008F  mov         dword ptr [ebp-10h],eax  
07980092  mov         ecx,dword ptr [ebp-10h]
07980095  mov         edx,dword ptr [ebp-0Ch]  
07980098  call        dword ptr ds:[2EA4E2Ch]  
0798009E  mov         eax,dword ptr [ebp-10h]  
079800A1  mov         dword ptr [ebp-4F38h],eax  
    41:             dict.Add(Guid.NewGuid(), obj);
079800A7  lea         ecx,[ebp-20h]  
079800AA  call        72D527F0  
079800AF  lea         eax,[ebp-20h]  
079800B2  sub         esp,10h  
079800B5  movq        xmm0,mmword ptr [eax]  
079800B9  movq        mmword ptr [esp],xmm0  
079800BE  movq        xmm0,mmword ptr [eax+8]  
079800C3  movq        mmword ptr [esp+8],xmm0  
079800C9  mov         ecx,dword ptr [ebp-4F34h]  
079800CF  mov         edx,dword ptr [ebp-4F38h]  
079800D5  cmp         dword ptr [ecx],ecx  
079800D7  call        72D2DD70  
    42:             intDict.Add(index, obj);
079800DC  push        dword ptr [ebp-4F38h]  
079800E2  mov         ecx,dword ptr [ebp+8]  
079800E5  mov         edx,dword ptr [ebp-0Ch]  
079800E8  cmp         dword ptr [ecx],ecx  
079800EA  call        72CFF2F0  

这是我看到的第二部分:

    45:             ++index;
079800EF  inc         dword ptr [ebp-0Ch]  
    46:             obj = new MyClass(index);
079800F2  mov         ecx,2EA4E30h  
079800F7  call        02E930F4  
079800FC  mov         dword ptr [ebp-24h],eax  
079800FF  mov         ecx,dword ptr [ebp-24h]  
07980102  mov         edx,dword ptr [ebp-0Ch]  
07980105  call        dword ptr ds:[2EA4E2Ch]  
0798010B  mov         eax,dword ptr [ebp-24h]  
0798010E  mov         dword ptr [ebp-4F38h],eax  
    47:             dict.Add(Guid.NewGuid(), obj);
07980114  lea         ecx,[ebp-34h]  
07980117  call        72D527F0  
0798011C  lea         eax,[ebp-34h]  
0798011F  sub         esp,10h  
07980122  movq        xmm0,mmword ptr [eax]  
07980126  movq        mmword ptr [esp],xmm0  
0798012B  movq        xmm0,mmword ptr [eax+8]  
07980130  movq        mmword ptr [esp+8],xmm0  
07980136  mov         ecx,dword ptr [ebp-4F34h]  
0798013C  mov         edx,dword ptr [ebp-4F38h]  
07980142  cmp         dword ptr [ecx],ecx  
07980144  call        72D2DD70  
    48:             intDict.Add(index, obj);
07980149  push        dword ptr [ebp-4F38h]  
0798014F  mov         ecx,dword ptr [ebp+8]  
07980152  mov         edx,dword ptr [ebp-0Ch]  
07980155  cmp         dword ptr [ecx],ecx  
07980157  call        72CFF2F0  

换句话说,堆栈槽 [ebp-10h][ebp-20h] 在第一段中使用,而槽 [ebp-24h][ebp-34h] 在第二段中使用。

我已经很久没有担心本机编译器将代码转换成什么了。上一次我不得不调试堆栈使​​用问题几乎是在二十年前。但是,很明显编译器已经决定出于某种原因,它需要为这些调用中的每一个调用新的临时变量,因此需要大量分配。

可能在完全优化的构建中,即在 Visual Studio 的调试器下 运行(当附加到进程时,它本身可以抑制甚至对于发布版本的优化),编译器能够优化这些堆栈槽,将它们组合成单个变量,以供每次调用重用。因此我的问题是你是如何观察代码的。

如果即使在没有附加 Visual Studio 调试器的情况下编译代码时,您也看到了 JIT 编译器的输出,那么我没有很好的解释为什么编译器不共享堆栈每次通话的插槽。不过,这么大的方法可能会导致优化器直接放弃,这就足够了。 :)

当然,正如您已经提到的,这完全不是问题。正常人不会这样写代码,所以精神错乱的后果纯粹是学术上的。