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

我已经读到一个线程的默认堆栈大小是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。(分配时间) 从技术上讲,堆栈分配并不是真正的分配,运行时只是确保堆栈的一部分(帧?)是为数组保留的。

不过,我强烈建议你在这方面要小心。 我的建议如下:

当你需要频繁地创建从不离开函数的数组时(例如通过传递它的引用),使用堆栈将是一个巨大的改进。 如果您可以回收一个数组,那么就尽可能地这样做!堆是长期对象存储的最佳位置。(污染全局内存不好;堆栈帧可以消失)

(注意:1。仅适用于值类型;引用类型将分配在堆上,收益将减少到0)

回答这个问题本身:我在任何大堆栈测试中都没有遇到过任何问题。 我相信唯一可能的问题是堆栈溢出,如果你不小心你的函数调用和运行的内存时,创建你的线程(s),如果系统运行低。

下面的部分是我的初步答案。这是错误的,测试也不正确。本文件仅供参考。


我的测试表明,在数组中使用堆栈分配的内存和全局内存至少比堆分配的内存慢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.

我在Windows 8.1 Pro(带有更新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.

不过,堆栈似乎变得越来越慢了。

其他回答

有一件事可能会出错,那就是你可能没有得到这样做的许可。除非在完全信任模式下运行,框架将忽略对更大堆栈大小的请求(参见线程构造函数上的MSDN (ParameterizedThreadStart, Int32))

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

高性能的数组可能以与普通c#数组相同的方式访问,但这可能是麻烦的开始:考虑以下代码:

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

您希望出现一个越界异常,这完全有意义,因为您正在尝试访问元素200,但允许的最大值是99。如果你使用stackalloc路由,那么将没有对象包裹在你的数组上进行绑定检查,下面将不会显示任何异常:

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

Above you are allocating enough memory to hold 100 floats and you are setting the sizeof(float) memory location which starts at the location started of this memory + 200*sizeof(float) for holding your float value 10. Unsurprisingly this memory is outside the allocated memory for the floats and nobody would know what could be stored in that address. If you are lucky you might have used some currently unused memory but at the same time it is likely you might overwrite some location that was used for storing other variables. To Summarize: Unpredictable runtime behaviour.

Microbenchmarking languages with JIT and GC such as Java or C# can be a bit complicated, so it's generally a good idea to use an existing framework - Java offers mhf or Caliper which are excellent, sadly to the best of my knowledge C# doesn't offer anything approaching those. Jon Skeet wrote this here which I'll blindly assume takes care of the most important things (Jon knows what he's doing in that area; also yes no worries I did actually check). I tweaked the timing a bit because 30 seconds per test after warmup was too much for my patience (5 seconds ought to do).

首先是结果,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);
}

我在那里有一个预定,我只是不知道如何预测它——权限、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

And

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

它抛出的异常:

System.NullReferenceException
System.IndexOutOfRangeException
System.ArrayTypeMismatchException

如您所见,stenem在数组范围检查和类型检查方面做了更多的工作。由于循环体只做很少的事情(只赋值),检查的开销支配了计算时间。这就是为什么性能相差530%的原因。

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