创build一个默认的堆栈大小为50x的线程有什么危险?

我目前正在研究一个性能非常关键的程序,我决定探索一条可能有助于减less资源消耗的path,从而增加我的工作线程的堆栈大小,这样我就可以移动大部分数据( float[] )访问堆栈(使用stackalloc )。

我读过一个线程的默认堆栈大小为1 MB,所以为了移动我所有的float[]我必须将堆栈扩展大约50倍(到50 MB〜)。

我知道这通常被认为是“不安全的”,不build议使用,但是通过对这个方法进行基准testing,发现处理速度提高了530% ! 所以我不能不经过进一步的调查就通过这个选项,这就引出我的疑问。 将堆栈增加到如此之大(可能出错的地方)有哪些危险?我应该采取哪些预防措施来尽量减less这种危险?

我的testing代码,

 public static unsafe void TestMethod1() { float* samples = stackalloc float[12500000]; for (var ii = 0; ii < 12500000; ii++) { samples[ii] = 32768; } } public static void TestMethod2() { var samples = new float[12500000]; for (var i = 0; i < 12500000; i++) { samples[i] = 32768; } } 

通过与Sam的testing代码进行比较,我确定我们都是对的!
但是,关于不同的事情:

  • 访问内存(读取和写入)在任何地方都是一样快 – 堆栈,全局或堆。
  • 然而, 分配它是堆栈中速度最快的,堆速度最慢。

它是这样的: stack < global < heap 。 (分配时间)
从技术上讲,堆栈分配并不是一个真正的分配,运行时只是确保为数组保留一部分堆栈(frame?)。

不过,我强烈build议小心谨慎。
我build议如下:

  1. 当你需要频繁地创build永远不会离开函数的数组(例如通过传递它的引用)时,使用这个栈将是一个巨大的改进。
  2. 如果你可以回收一个数组,只要你可以! 堆是长期对象存储的最佳场所。 (污染全局内存不好,堆栈帧可能会消失)

注意 :1.仅适用于值types;引用types将在堆上分配,优点将降为0)

为了回答这个问题:我从来没有遇到任何大堆testing的问题。
我相信唯一可能的问题是堆栈溢出,如果在系统运行低时创build线程的时候不小心你的函数调用和内存不足的话。

以下部分是我的初步答案。 这是错误的,testing是不正确的。 它仅供参考。


我的testing表明堆栈分配的内存和全局内存至less比堆内存分配内存的速度慢15%(占用时间的120%)!

这是我的testing代码 ,这是一个示例输出:

 Stack-allocated array time: 00:00:00.2224429 Globally-allocated array time: 00:00:00.2206767 Heap-allocated array time: 00:00:00.1842670 ------------------------------------------ Fastest: Heap. | S | G | H | --+---------+---------+---------+ S | - | 100.80 %| 120.72 %| --+---------+---------+---------+ G | 99.21 %| - | 119.76 %| --+---------+---------+---------+ H | 82.84 %| 83.50 %| - | --+---------+---------+---------+ Rates are calculated by dividing the row's value to the column's. 

我使用.NET 4.5.1下的i7 4700 MQ在Windows 8.1 Pro(使用Update 1)上进行了testing
我testing了x86和x64,结果是一样的。

编辑 :我增加了所有线程的大小为201 MB,样本大小为5000万,迭代减less到5。
结果与上面相同

 Stack-allocated array time: 00:00:00.4504903 Globally-allocated array time: 00:00:00.4020328 Heap-allocated array time: 00:00:00.3439016 ------------------------------------------ Fastest: Heap. | S | G | H | --+---------+---------+---------+ S | - | 112.05 %| 130.99 %| --+---------+---------+---------+ G | 89.24 %| - | 116.90 %| --+---------+---------+---------+ H | 76.34 %| 85.54 %| - | --+---------+---------+---------+ Rates are calculated by dividing the row's value to the column's. 

虽然,看起来堆栈实际上变慢了

我发现处理速度提高了530%!

这是我所说的最大的危险。 你的基准testing有一些严重的错误,那些不可预测的行为通常有一个隐藏在某处的令人讨厌的bug。

在.NET程序中消耗大量堆栈空间是非常非常困难的,而不是过度recursion。 托pipe方法的堆栈框架的大小是设置的。 只是方法的参数和方法中局部variables的总和。 减去可以存储在CPU寄存器中的那些数据,可以忽略它,因为它们的数目太less了。

增加堆栈大小并不能完成任何事情,只需要保留一堆永远不会使用的地址空间。 当然,没有任何机制可以解释从不使用内存的性能提高。

这与本地程序不同,特别是用C语言编写的程序,它也可以为堆栈框架上的数组预留空间。 堆栈缓冲区后面的基本恶意软件攻击向量溢出。 在C#中也是可以的,你必须使用stackalloc关键字。 如果你这样做,那么显而易见的危险是不得不编写受到这种攻击的不安全的代码,以及随机的栈帧损坏。 很难诊断错误。 在稍后的抖动中有一个对策,我认为从.NET 4.0开始,抖动产生的代码将一个“cookie”放在堆栈帧上,并在方法返回时检查它是否完好无损。 即时崩溃的桌面,没有任何方式拦截或报告,如果发生这样的事故。 这对用户的精神状态是危险的。

您的程序的主线程(由操作系统启动的程序)默认为1 MB,当您编译以x64为目标的程序时,该程序为4 MB。 越来越多,需要在后期构build事件中使用/ STACK选项运行Editbin.exe。 在32位模式下运行之前,您通常可能需要最多500 MB的内存才能启动。 线程也可以,当然更容易,危险区通常为一个32位程序徘徊在90 MB左右。 当你的程序运行了很长时间,并且地址空间从之前的分配中被分割出来的时候触发。 总地址空间使用率必须已经很高,超过一个演出,以获得这种失败模式。

三重检查你的代码,有一些非常错误的东西。 除非您明确写入代码以利用它,否则无法获得具有更大堆栈的x5加速。 这总是需要不安全的代码。 在C#中使用指针总是有创build更快的代码的诀窍,它不受数组边界检查。

我会在那里预订,我根本不知道如何预测它 – 权限,GC(需要扫描堆栈)等 – 都可能受到影响。 我会非常想使用非托pipe内存,而不是:

 var ptr = Marshal.AllocHGlobal(sizeBytes); try { float* x = (float*)ptr; DoWork(x); } finally { Marshal.FreeHGlobal(ptr); } 

有一件事可能会出错,那就是你可能没有得到许可。 除非以完全信任模式运行,否则框架将忽略更大堆栈大小的请求(请参阅Thread Constructor (ParameterizedThreadStart, Int32)上的MSDN)

与其将系统堆栈大小增加到如此庞大的数字,我build议重写代码,以便在堆上使用迭代和手动堆栈实现。

性能高的数组可能可以像正常的C#一样访问,但这可能是麻烦的开始:考虑下面的代码:

 float[] someArray = new float[100] someArray[200] = 10.0; 

你期望一个出界的exception,这是完全有道理的,因为你试图访问元素200,但最大允许的值是99.如果你去的stackalloc路由,那么将没有对象包裹你的数组绑定检查和以下将不会显示任何exception情况:

 Float* pFloat = stackalloc float[100]; fFloat[200]= 10.0; 

在上面你正在分配足够的内存来存放100个浮点数,并且你正在设置sizeof(float)的内存位置,从这个内存的位置开始+ 200 * sizeof(float)来保存你的浮点值10.不出所料,这个内存不在为浮动分配内存,没有人知道可以在那个地址存储什么。 如果幸运的话,你可能已经使用了一些当前未使用的内存,但是同时你可能会覆盖一些用于存储其他variables的位置。 总结:不可预知的运行时行为。

使用JIT和GC(如Java或C#)的微语言标记语言可能有点复杂,所以使用现有的框架通常是一个好主意 – Java提供的mhf或Caliper非常好,可惜我的知识是C#不提供的任何接近这些的东西。 Jon Skeet在这里写下了这个 ,我会盲目的承担起最重要的事情(Jon知道他在这个领域做了些什么,也没有担心我真的检查过了)。 我调整了一点时间,因为热身之后每次testing30秒对我的耐心(5秒钟应该做的)太多了。

所以首先是Windows 7 x64下的.NET 4.5.1 – 数字表示它可以在5秒内运行的迭代,所以越高越好。

x64 JIT:

 Standard 10,589.00 (1.00) UnsafeStandard 10,612.00 (1.00) Stackalloc 12,088.00 (1.14) FixedStandard 10,715.00 (1.01) GlobalAlloc 12,547.00 (1.18) 

x86 JIT(是的,这仍然有点难过):

 Standard 14,787.00 (1.02) UnsafeStandard 14,549.00 (1.00) Stackalloc 15,830.00 (1.09) FixedStandard 14,824.00 (1.02) GlobalAlloc 18,744.00 (1.29) 

这给出了最多14%的更合理的加速(并且大部分的开销是由于GC不得不运行,认为它是现实中最糟糕的情况)。 x86的结果虽然很有趣,但并不完全清楚发生了什么。

这是代码:

 public static float Standard(int size) { float[] samples = new float[size]; for (var ii = 0; ii < size; ii++) { samples[ii] = 32768 + (ii != 0 ? samples[ii - 1] : 0); } return samples[size - 1]; } public static unsafe float UnsafeStandard(int size) { float[] samples = new float[size]; for (var ii = 0; ii < size; ii++) { samples[ii] = 32768 + (ii != 0 ? samples[ii - 1] : 0); } return samples[size - 1]; } public static unsafe float Stackalloc(int size) { float* samples = stackalloc float[size]; for (var ii = 0; ii < size; ii++) { samples[ii] = 32768 + (ii != 0 ? samples[ii - 1] : 0); } return samples[size - 1]; } public static unsafe float FixedStandard(int size) { float[] prev = new float[size]; fixed (float* samples = &prev[0]) { for (var ii = 0; ii < size; ii++) { samples[ii] = 32768 + (ii != 0 ? samples[ii - 1] : 0); } return samples[size - 1]; } } public static unsafe float GlobalAlloc(int size) { var ptr = Marshal.AllocHGlobal(size * sizeof(float)); try { float* samples = (float*)ptr; for (var ii = 0; ii < size; ii++) { samples[ii] = 32768 + (ii != 0 ? samples[ii - 1] : 0); } return samples[size - 1]; } finally { Marshal.FreeHGlobal(ptr); } } static void Main(string[] args) { int inputSize = 100000; var results = TestSuite.Create("Tests", inputSize, Standard(inputSize)). Add(Standard). Add(UnsafeStandard). Add(Stackalloc). Add(FixedStandard). Add(GlobalAlloc). RunTests(); results.Display(ResultColumns.NameAndIterations); } 

由于性能差异太大,这个问题几乎与分配无关。 这可能是由数组访问造成的。

我分解了函数的循环体:

TestMethod1:

 IL_0011: ldloc.0 IL_0012: ldloc.1 IL_0013: ldc.i4.4 IL_0014: mul IL_0015: add IL_0016: ldc.r4 32768. IL_001b: stind.r4 // <----------- This one IL_001c: ldloc.1 IL_001d: ldc.i4.1 IL_001e: add IL_001f: stloc.1 IL_0020: ldloc.1 IL_0021: ldc.i4 12500000 IL_0026: blt IL_0011 

TestMethod2:

 IL_0012: ldloc.0 IL_0013: ldloc.1 IL_0014: ldc.r4 32768. IL_0019: stelem.r4 // <----------- This one IL_001a: ldloc.1 IL_001b: ldc.i4.1 IL_001c: add IL_001d: stloc.1 IL_001e: ldloc.1 IL_001f: ldc.i4 12500000 IL_0024: blt IL_0012 

我们可以检查指令的用法,更重要的是,他们在ECMA规范中抛出的exception:

 stind.r4: Store value of type float32 into memory at address 

它抛出的exception:

 System.NullReferenceException 

 stelem.r4: Replace array element at index with the float32 value on the stack. 

抛出exception:

 System.NullReferenceException System.IndexOutOfRangeException System.ArrayTypeMismatchException 

正如你所看到的, stelem在数组范围检查和types检查stelem做了更多的工作。 由于循环体做的事情很less(只分配值),检查的开销占主导地位的计算时间。 所以这就是性能相差达530%的原因。

这也回答你的问题:危险是没有arrays范围和types检查。 这是不安全的(正如函数声明中提到的; D)。

编辑:(代码和测量的小变化产生了很大的变化)

首先,我在debugging器(F5)中运行优化的代码,但这是错误的。 应该在没有debugging器的情况下运行(Ctrl + F5)。 其次,代码可能会被彻底优化,所以我们必须把它复杂化,以便优化器不会混淆我们的测量。 我让所有的方法返回数组中的最后一个项目,并且数组填充不同。 在OP的TestMethod2中还有一个额外的零,总是让它慢十倍。

除了你提供的两个方法之外,我还尝试了其他一些方法。 方法3具有与方法2相同的代码,但是该函数被声明为unsafe 。 方法4使用指针访问定期创build的数组。 方法5使用指针访问非托pipe内存,如Marc Gravell所述。 所有五种方法运行的时间非常相似。 M5是最快的(而M1则接近第二)。 最快和最慢的差别是5%左右,这不是我所关心的。

  public static unsafe float TestMethod3() { float[] samples = new float[5000000]; for (var ii = 0; ii < 5000000; ii++) { samples[ii] = 32768 + (ii != 0 ? samples[ii - 1] : 0); } return samples[5000000 - 1]; } public static unsafe float TestMethod4() { float[] prev = new float[5000000]; fixed (float* samples = &prev[0]) { for (var ii = 0; ii < 5000000; ii++) { samples[ii] = 32768 + (ii != 0 ? samples[ii - 1] : 0); } return samples[5000000 - 1]; } } public static unsafe float TestMethod5() { var ptr = Marshal.AllocHGlobal(5000000 * sizeof(float)); try { float* samples = (float*)ptr; for (var ii = 0; ii < 5000000; ii++) { samples[ii] = 32768 + (ii != 0 ? samples[ii - 1] : 0); } return samples[5000000 - 1]; } finally { Marshal.FreeHGlobal(ptr); } }