我试图理解我在某些 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 为"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/