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

标签 c# assembly stack-overflow

我试图理解我在某些 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 方法中,堆栈上分配的空间似乎远远多于所需的空间。以下内容出现在该方法的反汇编窗口中,位于任何实际的 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 的堆栈空间。我的问题是:

  1. 我的理解正确吗?
  2. 如果 1 为"is",那么为什么要分配所有空间?

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

最佳答案

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

我问,因为查看整个反汇编方法,很明显堆栈空间正在用于每次调用 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] 用于第二段。

我已经很长时间没有担心 native 编译器将代码转换成什么了。上次我不得不调试堆栈使​​用问题实际上是二十年前的事了。但是,很明显编译器已经决定,由于某种原因,它需要为每个调用添加新的临时变量,因此需要大量分配。

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

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

当然,正如您已经提到的,这实际上完全不是问题。这不是一个理智的人会如何编写代码的方式,因此疯狂的后果纯粹是学术性的。

关于C# 方法分配不必要的堆栈空间?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/48515442/

相关文章:

编译器使用局部变量而不调整 RSP

C 和汇编测验

c - 遍历数组 MASM 中的元素并对其求平均值

c++ - 构造函数中的 Const 参数导致 stackoverflow

c# - 递归的任何其他原因是否会导致堆栈溢出?

c# - 如何从字符串为 Lambda 表达式动态创建方法

c# - 寻找 Properties.Settings.Default 的快捷方式

c# - 如何创建 Entity Framework 模型第一个关联表?

c# - 将对象图写入 XAML 时出现 StackOverFlow 异常

c# - 让 Entity Framework 与 SQL Server 地理空间索引一起使用