c# - 栈空间不足 - 创建默认堆栈大小为50x的线程时有什么危险?




vs堆栈大小 (6)

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

这是迄今为止我所说的最大危险。 你的基准测试存在严重问题,这些不可预知的行为通常会在某处隐藏一个令人讨厌的bug。

在.NET程序中消耗大量堆栈空间是非常非常困难的,而不是过度递归。 托管方法的堆栈框架的大小被设置为石头。 只是方法的参数和方法中局部变量的总和。 减去可以存储在CPU寄存器中的那些数据,可以忽略它,因为它们的数量很少。

增加堆栈大小并不能完成任何事情,只会预留一堆永远不会使用的地址空间。 当然没有任何机制可以解释由于不使用记忆而导致的性能增加。

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

您的程序的主线程(由操作系统启动的程序)默认为1 MB堆栈,当您编译以x64为目标的程序时,该程序为4 MB。 越来越多,需要在后期构建事件中使用/ STACK选项运行Editbin.exe。 在32位模式下运行程序时,通常可能需要最多500 MB的内存。 线程也可以,当然更容易,对于32位程序,危险区通常会徘徊在90 MB左右。 当您的程序长时间运行并且地址空间与以前的分配情况分离时触发。 总地址空间使用率必须已经很高,才能获得此失败模式。

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

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

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

我知道这通常被认为是“不安全的”,不推荐使用,但是通过使用此方法对当前代码进行基准测试后,我发现处理速度提高了530% ! 所以我不能简单地通过这个选项没有进一步调查,这导致我的问题; 将堆叠增加到如此大的尺寸(可能出现什么问题)有哪些危险?我应该采取哪些预防措施来尽量减少这种危险?

我的测试代码,

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进行比较后,我确定我们都是对的!
但是,关于不同的事情:

  • 访问内存(读取和写入)无论在哪里都是一样快 - 堆栈,全局或堆。
  • 然而, 分配它的速度是堆栈中最快的,堆中速度最慢。

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

尽管如此,我强烈建议小心谨慎。
我建议如下:

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

注意 :1.仅适用于值类型;引用类型将分配在堆上,并且优点将降低到0)

回答这个问题本身:我从来没有遇到任何大堆测试的问题。
我相信唯一可能的问题是堆栈溢出,如果在系统运行低时创建线程时不小心使用函数调用并且内存不足。

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

我的测试表明堆栈分配的内存和全局内存至少比在堆阵列中使用的堆内存分配速度慢15%(占用时间的120%)!

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

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)上进行了测试
我测试了x86和x64,结果是相同的。

编辑 :我增加了所有线程的堆栈大小201 MB,样本大小为5000万,迭代次数减少到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.

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


性能高的数组可能与正常的C#相同,但这可能是麻烦的开始:请考虑以下代码:

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

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

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

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


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

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

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

我分解了函数的循环体:

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规范中抛出异常:

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

它抛出的异常:

System.NullReferenceException

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

它抛出异常:

System.NullReferenceException
System.IndexOutOfRangeException
System.ArrayTypeMismatchException

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

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


编辑:(代码和测量的小变化会导致结果的巨大变化)

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

除了您提供的两个方法外,我还尝试了其他一些方法。 方法3具有与方法2相同的代码,但该函数被声明为unsafe 。 方法4使用指针访问来定期创建数组。 如Marc Gravell所述,方法5使用指向非托管内存的指针访问。 所有五种方法运行时间非常相似。 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);
        }
    }






stack-memory