我目前正在开发一个非常性能关键的程序,我决定探索的一条路径可能有助于减少资源消耗,即增加我的工作线程的堆栈大小,这样我就可以将我将访问的大部分数据(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;
}
}
由于性能差异太大,问题几乎与配置无关。这很可能是由数组访问引起的。
我分解了函数的循环体:
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)。
编辑:(代码和测量中的微小变化会导致结果的巨大变化)
首先,我在调试器(F5)中运行优化的代码,但这是错误的。它应该在没有调试器的情况下运行(Ctrl+F5)。其次,代码可能被彻底优化了,所以我们必须把它复杂化,这样优化器就不会干扰我们的测量。我让所有方法都返回数组中的最后一项,数组的填充方式不同。在OP的TestMethod2中还有一个额外的0,它总是使它慢十倍。
除了你提供的两种方法外,我还尝试了其他一些方法。方法3的代码与方法2相同,但该函数被声明为不安全的。方法4是使用指针访问定期创建的数组。方法5是使用指针访问非托管内存,如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);
}
}
在将测试代码与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.
不过,堆栈似乎变得越来越慢了。
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);
}