c# - 初始化大型锯齿状数组会占用超过 1 GB 的 RAM 并因 StackOverflowException 而崩溃

标签 c# arrays

当我编译这段C#代码时(full text)并运行 ArrayTest.exe ,进程挂起几秒钟,消耗 1 GB RAM,并因 StackOverflowException 而崩溃。为什么?

public struct Point { }

public class ArrayTest {
    public static void Main(string[] args) {
        Point[][] array = {
            new Point[]{new Point(), new Point(), /* ... 296 omitted ... */, new Point(), new Point()},
            new Point[]{new Point(), new Point(), /* ... 296 omitted ... */, new Point(), new Point()},
            /* ... 296 omitted ... */
            new Point[]{new Point(), new Point(), /* ... 296 omitted ... */, new Point(), new Point()},
            new Point[]{new Point(), new Point(), /* ... 296 omitted ... */, new Point(), new Point()},
        };
        /* Do nothing and return */
    }
}

我正在为 Microsoft (R) .NET Framework 4.5 使用 Microsoft (R) Visual C# 编译器版本 4.0.30319.33440。我只是在命令行上调用 csc.exe 并执行编译后的 EXE。当我添加 csc /optimize 标志时,问题就消失了。上面的代码片段确实是我正在测试的完整代码 - 在初始化数组后,Main() 中没有执行任何有用的工作。


问题背景:我试图将一组数字测试用例硬编码到一个程序中。在 Java、JavaScript 或 Python 中,代码看起来像这样并且可以正常工作:

class Point { int x; int y; }

Point[][] data = {  // About 1000 entries
    {new Point(1, 2)},
    {new Point(5, 3), new Point(0, 6), new Point(1, 8)},  // Different length
    ... et cetera ...
};
for (Point[] thing : data):
    test(thing);

但是当我尝试在 C# 中编译这样的代码时,数组初始化花费了很长的时间(~5 秒),甚至在使用 test() 的 for 循环开始之前执行。

我的实际代码已经缩减为上面的 MVCE,其中 struct Point 不包含任何字段,而 Main() 仅包含数组初始化,没有任何有用的工作。

最佳答案

好的,我开始编译你的类文件的调试/发布版本。使用 14.0 版工具中的 VS 2015 编译器,IL 的输出是相同的。这涵盖了人们没有注意到问题的原因。

在 VS 2013 中使用的先前编译器中的调试与发布非常直接。 Debug模式下的可执行文件输出为 2,091 kb。发行版中的 IL 表明它只是忽略实际对象,因为它从未被使用过。好的。我会将 VS 2015 Debug IL 与 VS 2013 Debug IL 进行比较。

为简洁起见,我已将数组大小更改为 3x3。

这是 2015 IL 的输出:

  .method public hidebysig static void  Main() cil managed
  {
    .entrypoint
    // Code size       45 (0x2d)
    .maxstack  4
    .locals init (valuetype Point[][] V_0)
    IL_0000:  nop
    IL_0001:  ldc.i4.4
    IL_0002:  newarr     valuetype Point[]
    IL_0007:  dup
    IL_0008:  ldc.i4.0
    IL_0009:  ldc.i4.3
    IL_000a:  newarr     Point
    IL_000f:  stelem.ref
    IL_0010:  dup
    IL_0011:  ldc.i4.1
    IL_0012:  ldc.i4.3
    IL_0013:  newarr     Point
    IL_0018:  stelem.ref
    IL_0019:  dup
    IL_001a:  ldc.i4.2
    IL_001b:  ldc.i4.3
    IL_001c:  newarr     Point
    IL_0021:  stelem.ref
    IL_0022:  dup
    IL_0023:  ldc.i4.3
    IL_0024:  ldc.i4.3
    IL_0025:  newarr     Point
    IL_002a:  stelem.ref
    IL_002b:  stloc.0
    IL_002c:  ret
  } // end of method ArrayTest::Main

此代码与 Release模式代码之间的主要区别是额外的 nop 指令。

这是 2012/2013 版编译器的输出:

  .method public hidebysig static void  Main() cil managed
  {
    .entrypoint
    // Code size       307 (0x133)
    .maxstack  4
    .locals init (valuetype Point[][] V_0,
             valuetype Point[][] V_1,
             valuetype Point[] V_2,
             valuetype Point V_3)
    IL_0000:  nop
    IL_0001:  ldc.i4.4
    IL_0002:  newarr     valuetype Point[]
    IL_0007:  stloc.1
    IL_0008:  ldloc.1
    IL_0009:  ldc.i4.0
    IL_000a:  ldc.i4.3
    IL_000b:  newarr     Point
    IL_0010:  stloc.2
    IL_0011:  ldloc.2
    IL_0012:  ldc.i4.0
    IL_0013:  ldelema    Point
    IL_0018:  ldloca.s   V_3
    IL_001a:  initobj    Point
    IL_0020:  ldloc.3
    IL_0021:  stobj      Point
    IL_0026:  ldloc.2
    IL_0027:  ldc.i4.1
    IL_0028:  ldelema    Point
    IL_002d:  ldloca.s   V_3
    IL_002f:  initobj    Point
    IL_0035:  ldloc.3
    IL_0036:  stobj      Point
    IL_003b:  ldloc.2
    IL_003c:  ldc.i4.2
    IL_003d:  ldelema    Point
    IL_0042:  ldloca.s   V_3
    IL_0044:  initobj    Point
    IL_004a:  ldloc.3
    IL_004b:  stobj      Point
    IL_0050:  ldloc.2
    IL_0051:  stelem.ref
    IL_0052:  ldloc.1
    IL_0053:  ldc.i4.1
    IL_0054:  ldc.i4.3
    IL_0055:  newarr     Point
    IL_005a:  stloc.2
    IL_005b:  ldloc.2
    IL_005c:  ldc.i4.0
    IL_005d:  ldelema    Point
    IL_0062:  ldloca.s   V_3
    IL_0064:  initobj    Point
    IL_006a:  ldloc.3
    IL_006b:  stobj      Point
    IL_0070:  ldloc.2
    IL_0071:  ldc.i4.1
    IL_0072:  ldelema    Point
    IL_0077:  ldloca.s   V_3
    IL_0079:  initobj    Point
    IL_007f:  ldloc.3
    IL_0080:  stobj      Point
    IL_0085:  ldloc.2
    IL_0086:  ldc.i4.2
    IL_0087:  ldelema    Point
    IL_008c:  ldloca.s   V_3
    IL_008e:  initobj    Point
    IL_0094:  ldloc.3
    IL_0095:  stobj      Point
    IL_009a:  ldloc.2
    IL_009b:  stelem.ref
    IL_009c:  ldloc.1
    IL_009d:  ldc.i4.2
    IL_009e:  ldc.i4.3
    IL_009f:  newarr     Point
    IL_00a4:  stloc.2
    IL_00a5:  ldloc.2
    IL_00a6:  ldc.i4.0
    IL_00a7:  ldelema    Point
    IL_00ac:  ldloca.s   V_3
    IL_00ae:  initobj    Point
    IL_00b4:  ldloc.3
    IL_00b5:  stobj      Point
    IL_00ba:  ldloc.2
    IL_00bb:  ldc.i4.1
    IL_00bc:  ldelema    Point
    IL_00c1:  ldloca.s   V_3
    IL_00c3:  initobj    Point
    IL_00c9:  ldloc.3
    IL_00ca:  stobj      Point
    IL_00cf:  ldloc.2
    IL_00d0:  ldc.i4.2
    IL_00d1:  ldelema    Point
    IL_00d6:  ldloca.s   V_3
    IL_00d8:  initobj    Point
    IL_00de:  ldloc.3
    IL_00df:  stobj      Point
    IL_00e4:  ldloc.2
    IL_00e5:  stelem.ref
    IL_00e6:  ldloc.1
    IL_00e7:  ldc.i4.3
    IL_00e8:  ldc.i4.3
    IL_00e9:  newarr     Point
    IL_00ee:  stloc.2
    IL_00ef:  ldloc.2
    IL_00f0:  ldc.i4.0
    IL_00f1:  ldelema    Point
    IL_00f6:  ldloca.s   V_3
    IL_00f8:  initobj    Point
    IL_00fe:  ldloc.3
    IL_00ff:  stobj      Point
    IL_0104:  ldloc.2
    IL_0105:  ldc.i4.1
    IL_0106:  ldelema    Point
    IL_010b:  ldloca.s   V_3
    IL_010d:  initobj    Point
    IL_0113:  ldloc.3
    IL_0114:  stobj      Point
    IL_0119:  ldloc.2
    IL_011a:  ldc.i4.2
    IL_011b:  ldelema    Point
    IL_0120:  ldloca.s   V_3
    IL_0122:  initobj    Point
    IL_0128:  ldloc.3
    IL_0129:  stobj      Point
    IL_012e:  ldloc.2
    IL_012f:  stelem.ref
    IL_0130:  ldloc.1
    IL_0131:  stloc.0
    IL_0132:  ret
  } // end of method ArrayTest::Main

因此,在您使用的 2012/2013 编译器中, Debug模式进行了大量的堆栈分配,可能是为了让您可以在编辑期间智能感知整个锯齿状数组结构并继续,或者可能这样您可能进入每个单独的对象构造。我对此完全不确定。

我不是 IL 方面的专家,但在我看来,它正在为每个点分配,然后为每个数组再次分配,然后再次为锯齿状数组分配,导致分配过多。

关于c# - 初始化大型锯齿状数组会占用超过 1 GB 的 RAM 并因 StackOverflowException 而崩溃,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/39583023/

相关文章:

javascript - 如何按业力对 Reddit 用户进行排序?

java - 分配给Java中的字节数组

c# - 如何使用 MigraDoc 让表情符号出现在生成的 PDF 中

arrays - 如何构建 if 语句并与各种值进行比较?

javascript - 从数组中查找所有可能的匹配字符串和子字符串

c# - 用户控件属性设计器属性

arrays - 基于循环的VBA重新调整数组尺寸

c# - Windows Phone IsolatedStorageSettings 和 Mutex

c# - Application.Run(new Form1()) 抛出 System.ArgumentException : 'Value does not fall within the expected range.'

c# - 在 C# 中打印收据