启动时大型集合初始化程序出现Stackoverflow错误

8

我正在开发一个应用程序,它使用相对较大的表格来完成工作(确切地说是LR表格)。由于我已经在生成代码,而且表格并不是那么大,所以我决定通过生成使用C#集合初始化语法在我的生成程序启动时初始化表格的代码来序列化我的表格:

public static readonly int[,] gotoTable = new int[,]
{
    {
        0,1,0,0,0,0,0,0,0,0,0,0,0,0,(...)
    },
    {
        0,0,4,0,5,6,0,0,0,0,0,7,0,0,(...)
    },
    (...)

奇怪的是,当我生成只有几十万条记录的表格时,所生成的应用程序在启动时会崩溃并出现StackOverflowException。C#编译器可以正常编译它,表格生成应用程序也可以正常运行。事实上,当我切换到发布模式时,该应用程序确实启动了。虽然OutOfMemoryException可能会有些道理,但即使是这样,我使用的表格也太小了。

复制此代码以重现问题:

警告:尝试在发布模式下运行以下代码会导致Visual Studio 2010崩溃,请小心保存未保存的工作。此外,如果您生成的代码存在大量编译错误,Visual Studio也会挂起。

//Generation Project, main.cs:
using (StreamWriter writer = new StreamWriter("../../../VictimProject/Tables.cs"))
{
    writer.WriteLine("using System;");
    writer.WriteLine("public static class Tables");
    writer.WriteLine("{");
    writer.WriteLine("    public static readonly Tuple<int>[] bigArray = new Tuple<int>[]");
    writer.WriteLine("    {");
    for (int i = 0; i < 300000; i++)
        writer.WriteLine("        new Tuple<int>(" + i + "),");
    writer.WriteLine("    };");
    writer.WriteLine("}");
}
//Victim Project, main.cs:
for (int i = 0; i < 1234; i++)
{
    // Preventing the jitter from removing Tables.bigArray
    if (Tables.bigArray[i].Item1 == 10)
        Console.WriteLine("Found it!");
}
Console.ReadKey(true);

运行Tables.cs文件的第一个项目,然后运行第二个程序以获取StackOverflowException。请注意,上述代码在我的计算机上会崩溃:在不同平台上可能不会崩溃;如果出现问题,请尝试将300000增加。
使用发布模式而不是调试模式似乎略微增加了限制,因为我的项目在发布模式下不会崩溃。然而,上面的代码在两种模式下都会崩溃。
使用字面值int或string代替Tuple不会导致崩溃,"new int()"也不会崩溃(但可能会转换为文本0)。使用只包含一个int字段的结构体会导致崩溃。似乎与使用初始化器的构造函数有关。
我猜想集合初始化器以递归方式实现,这可以解释堆栈溢出。但是,作为迭代解决方案,这似乎很奇怪且效率较低。C#编译器本身没有任何问题,它编译得非常快(它甚至处理更大的集合很好,但是当集合非常大时确实会崩溃)。
我猜我可能有一种方法可以直接将表格写入二进制文件,然后链接该文件,但我还没有研究过。
我想我有两个问题:为什么会出现上述情况,以及如何解决它?
编辑:反汇编.exe文件后,发现一些有趣的细节。
.maxstack  4
.locals init ([0] class [mscorlib]System.Tuple`1<int32>[] CS$0$0000)
IL_0000:  ldc.i4     0x493e0
IL_0005:  newarr     class [mscorlib]System.Tuple`1<int32>
IL_000a:  stloc.0
IL_000b:  ldloc.0
IL_000c:  ldc.i4.0
IL_000d:  ldc.i4.0
IL_000e:  newobj     instance void class [mscorlib]System.Tuple`1<int32>::.ctor(!0)
IL_0013:  stelem.ref
IL_0014:  ldloc.0
IL_0015:  ldc.i4.1
IL_0016:  ldc.i4.1
IL_0017:  newobj     instance void class [mscorlib]System.Tuple`1<int32>::.ctor(!0)
IL_001c:  stelem.ref
(goes on and on)

这表明,当尝试JIT此方法时,抖动确实会因堆栈溢出而崩溃。但奇怪的是它确实发生了,并且特别是我获得了异常。

2个回答

10

为什么会发生这种情况?

我怀疑这可能是JIT崩溃了。你将会生成一个巨大的类型初始化器(即IL中的.cctor成员)。每个值将由5个IL指令组成。一个包含150万条指令的成员导致问题并不奇怪...

我该如何解决?

将数据包含到嵌入式资源文件中,需要时在类型初始化器中加载它。我假设这是生成的数据 - 所以将数据放在二进制文件中而不是作为文本代码。


我会感到惊讶,如果一个本地的堆栈溢出崩溃在CLR内部被转换为普通的StackOverflowException。我会期望进程失败(fail()),或者是ExecutionEngineException或BadProgramException。 - usr
@usr 当你有class Program { static void Main() { Main(); } }时,你认为哪个堆栈会溢出?这不是IL评估堆栈 - 它纯粹是虚拟的。 - phoog
我可能会预期jitted堆栈溢出,而不是jit内部的堆栈。这个答案中提出的解释完全有可能,但绝对不是特别可能的情况。让我们不要把它当作真相,而是作为没有太多证据的猜测。 - usr

9
如果尝试将所有这些内容预先推送到堆栈上,那么它将需要大量的堆栈空间,因此我个人确实预计会出现堆栈溢出,这取决于编译器如何处理。
在之前做过类似的事情(会破坏像反射器这样的所有工具,因为IL太大),我的经验建议是:通过序列化而不是通过c#来完成。在我的情况下,我几乎完全通过protobuf-net来完成了这一点,即:
- 生成模型(不包括数据)作为代码 - 执行它以从数据库中填充模型 - 将其序列化到文件中 - 在部署时随文件一起发送 - 在初始化期间进行反序列化
但是 - 我似乎最近已经进行了这次讨论;如果是与您自己进行的讨论,那么我完全支持我之前的言论。您尝试的方式仍然有问题。以上方法(通过直接经验)非常有效。作为IL?效果不佳。
注意:如果您绝对想要在没有执行步骤的情况下编写文件,那也是可能的 - 只是比较棘手。

根据IL,它并没有将所有内容都推送到堆栈上。它是逐个将元素存储到数组中。 - usr
@usr 它从哪里获取这些元素?它们可能存储在堆栈上。 - phoog
它们被一个接一个地存储在堆栈上。常量空间就足够了。 - usr
@usr 我还没有查看 IL(在移动设备上)- 它声明了多少本地变量,可能? - Marc Gravell
@MarcGravell:我刚刚在生成的 .exe 文件上使用了 ildasm,并声明了一个类型为 Tuple<int> 的本地变量。它的 .maxstack 是 4。 - Alex ten Brink

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接