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

时间:2018-01-30 06:45:10

标签: c# assembly stack-overflow

我正在尝试理解我在某些C#代码中看到的一些行为,而不考虑这是否应该应该如何编写。基本上,请考虑以下代码:

<div class="row">
        <div class="method-test">
            <span class="glyphicon glyphicon-play"></span><h4>recognize() Method</h4>
            <form id="recognizeForm">
                Image (public URL or base64 data): <input type="text" class="image" name="Image">
              Image (local file): <input type="file" class="image-upload" name="Image Upload">
                Gallery Name: <input type="text" class="gallery_name" name="Gallery Name">
                <input type="button" id="testRecognize" value="Test" />
            </form>
        </div>
    </div>

在方法Init中,似乎在堆栈上分配的空间远远超过必要的空间。在任何实际的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);
        }
    }
}

如果我正确读取它,它会为具有2个参数和2个局部变量的方法分配大约11 KB的堆栈空间,以及一些临时值。我的问题是:

  1. 我读得对吗?
  2. 如果是1,那么为什么要分配所有空间?
  3. 同样,现在并不关心你是否应该以这种方式实际编写代码。只是好奇发生了什么。

1 个答案:

答案 0 :(得分:3)

你是如何检查拆卸的?使用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]

自从我不得不担心本机编译器将代码转化为什么以来,已经有很长一段时间了。上次我不得不调试堆栈使​​用问题几乎是在二十年前。但是,似乎很清楚,编译器已经决定由于某种原因,它需要每个调用的新临时变量,因此需要大量的分配。

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

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

当然,正如你已经提到的那样,这实际上是一个完全没有问题的。这不是一个理智的人如何编写代码,所以疯狂的后果纯粹是学术性的。