创建堆栈大小为默认值50倍的线程有什么危险?

Sam*_*Sam 228 .net c# memory stack-memory

我目前正在开发一个性能非常关键的程序,我决定探索的一条路径可能有助于减少资源消耗,增加了我的工作线程的堆栈大小,因此我可以移动float[]我将要访问的大部分数据堆栈(使用stackalloc).

我已经读过一个线程的默认堆栈大小是1 MB,所以为了移动我所有的float[]s,我必须将堆栈扩展大约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;
    }
}
Run Code Online (Sandbox Code Playgroud)

Ver*_*cas 45

在将测试代码与Sam进行比较后,我确定我们都是对的!
但是,关于不同的事情:

  • 无论在何处,访问内存(读取和写入)都是一样快 - 堆栈,全局或堆栈.
  • 但是,分配它在堆栈上最快,在堆上最慢.

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

不过,我强烈建议小心这一点.
我推荐以下内容:

  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.
Run Code Online (Sandbox Code Playgroud)

我在Windows 8.1 Pro(使用Update 1)上测试,使用i7 4700 MQ,在.NET 4.5.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.
Run Code Online (Sandbox Code Playgroud)

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

  • @Voo测试我的基准测试和他在这个答案评论中添加的要点.将代码组合在一起并运行几百个测试.然后回来报告你的结论.我已经非常彻底地完成了我的测试,而且当我说.NET没有像Java那样解释任何字节码时,我非常清楚我在说什么,它会立即对它进行JIT. (2认同)

Han*_*ant 28

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

到目前为止,这是我所说的最大危险.您的基准测试存在严重问题,代码行为不可预测通常会隐藏某处隐藏的错误.

除了过多的递归之外,在.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#中使用指针始终具有创建更快代码的诀窍,它不受数组边界检查的影响.

  • 报告的5倍加速是从`float []`移动到`float*`.大堆栈就是如何实现的.在某些情况下,x5加速对于这种变化是完全合理的. (21认同)
  • 好的,当我开始回答这个问题时,我还没有代码片段.仍然足够接近. (3认同)

Mar*_*ell 22

我会在那里预订,我根本就不知道如何预测它 - 权限,GC(需要扫描堆栈)等等 - 所有这些都可能受到影响.我很想尝试使用非托管内存:

var ptr = Marshal.AllocHGlobal(sizeBytes);
try
{
    float* x = (float*)ptr;
    DoWork(x);
}
finally
{
    Marshal.FreeHGlobal(ptr);
}
Run Code Online (Sandbox Code Playgroud)

  • @dcastro它需要扫描堆栈以检查只存在于堆栈上的引用.我只是不知道当它到达如此巨大的`stackalloc`时会发生什么 - 它有点需要跳过它,你希望它能毫不费力地这样做 - 但我想说的是它引入了*不必要的*并发症/担忧.IMO,`stackalloc`非常适合作为临时缓冲区,但对于专用工作区,更期望在某处分配一个块内存,而不是滥用/混淆堆栈, (6认同)

PMF*_*PMF 8

可能出错的一件事是您可能无法获得许可.除非以完全信任模式运行,否则框架将忽略更大堆栈大小的请求(请参阅MSDN Thread Constructor (ParameterizedThreadStart, Int32))

我建议重写代码,使其在堆上使用迭代和手动堆栈实现,而不是将系统堆栈大小增加到如此庞大的数字.


MHO*_*OOS 6

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

float[] someArray = new float[100]
someArray[200] = 10.0;
Run Code Online (Sandbox Code Playgroud)

你期望一个超出范围的异常,这完全有意义,因为你试图访问元素200,但最大允许值是99.如果你去stackalloc路由然后没有对象缠绕你的数组绑定检查和以下内容不会显示任何异常:

Float* pFloat =  stackalloc float[100];
fFloat[200]= 10.0;
Run Code Online (Sandbox Code Playgroud)

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

  • @TomTom呃,没有; 答案是有道理的; 问题谈到`stackalloc`,在这种情况下我们谈论的是`float*`等 - 它没有相同的检查.它被称为"不安全"是有充分理由的.我个人非常乐意在有充分理由的情况下使用"不安全",但苏格拉底提出了一些合理的观点. (9认同)

Voo*_*Voo 6

使用JIT和GC的微型标记语言(如Java或C#)可能有点复杂,因此使用现有框架通常是一个好主意 - Java提供了非常好的mhf或Caliper,遗憾的是据我所知C#不提供任何接近这些的东西.Jon Skeet在这里写了这个,我将盲目地假设照顾最重要的事情(Jon知道他在那个领域做了什么;也不用担心我确实做了检查).我稍微调整了时间,因为热身后每次测试30秒对我的耐心来说太多了(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)
Run Code Online (Sandbox Code Playgroud)

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)
Run Code Online (Sandbox Code Playgroud)

这提供了更合理的加速,最多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);
}
Run Code Online (Sandbox Code Playgroud)


HKT*_*Lee 5

由于性能差异太大,问题几乎与分配无关.它可能是由阵列访问引起的.

我反汇编了函数的循环体:

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
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

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

stind.r4: Store value of type float32 into memory at address
Run Code Online (Sandbox Code Playgroud)

抛出的例外情况:

System.NullReferenceException
Run Code Online (Sandbox Code Playgroud)

stelem.r4: Replace array element at index with the float32 value on the stack.
Run Code Online (Sandbox Code Playgroud)

抛出异常:

System.NullReferenceException
System.IndexOutOfRangeException
System.ArrayTypeMismatchException
Run Code Online (Sandbox Code Playgroud)

如您所见,stelem在阵列范围检查和类型检查方面做了更多工作.由于循环体做的很少(仅赋值),因此检查的开销占据了计算时间.这就是性能相差530%的原因.

这也回答了你的问题:危险是没有阵列范围和类型检查.这是不安全的(如函数声明中所述; D).