我目前正在开发一个非常性能关键的程序,我决定探索的一条路径可能有助于减少资源消耗,即增加我的工作线程的堆栈大小,这样我就可以将我将访问的大部分数据(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;
    }
}

当前回答

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

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

我在那里有一个预定,我只是不知道如何预测它——权限、GC(需要扫描堆栈)等等——所有这些都可能受到影响。我非常倾向于使用非托管内存:

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

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);
}