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 是,那么为什么要分配所有 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 编译器的输出,那么我没有很好的解释为什么编译器不共享堆栈每次通话的插槽。不过,这么大的方法可能会导致优化器直接放弃,这就足够了。 :)
当然,正如您已经提到的,这完全不是问题。正常人不会这样写代码,所以精神错乱的后果纯粹是学术上的。
我试图理解我在某些 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 是,那么为什么要分配所有 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 编译器的输出,那么我没有很好的解释为什么编译器不共享堆栈每次通话的插槽。不过,这么大的方法可能会导致优化器直接放弃,这就足够了。 :)
当然,正如您已经提到的,这完全不是问题。正常人不会这样写代码,所以精神错乱的后果纯粹是学术上的。