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 可能有一定的意义,但即便如此,我使用的表对于 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。

使用发布模式而不是调试模式似乎稍微增加了限制,因为我的项目在发布模式下不会崩溃。但是,对我来说,上面的代码在两种模式下都会崩溃。

使用文字ints 或strings 代替Tuple<int>s 不会导致崩溃,“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)

这表明抖动确实会因试图抖动此方法的堆栈溢出而崩溃。尽管如此,它确实很奇怪,特别是我得到了一个例外。

4

2 回答 2

10

为什么会发生上述情况

我怀疑这可能是 JIT 崩溃了。您将生成一个巨大的类型初始化程序(IL 中的 .cctor 成员)。每个值将是 5 条 IL 指令。拥有 150 万条指令的成员会导致问题,我并不完全感到惊讶……

我该如何解决?

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

于 2012-04-12T21:57:57.377 回答
9

如果它试图将所有这些预先推送到堆栈上,那么它将需要大量的堆栈空间,所以我个人确实希望这里的堆栈溢出,这取决于编译器是如何做到的。

之前做过类似的事情(因为 IL 太大而破坏了诸如反射器之类的所有工具),我的经验建议是:通过序列化而不是通过 c# 来做到这一点。就我而言,我通过 protobuf-net 几乎完全做到了这一点,即

  • 将模型(无数据)生成为代码
  • 执行它以从数据库中填充模型
  • 将其序列化为文件
  • 将文件与我的部署一起发送
  • 在初始化期间反序列化

但是——我好像记得最近有过这个讨论;如果是你自己,那么我完全支持我之前的言论。您尝试执行此操作的方式仍然存在问题。上述方法(来自直接经验)效果很好。作为伊利诺伊?没那么多。

注意:如果您绝对想在没有执行步骤的情况下编写文件,那也是可能的 - 只是更棘手。

于 2012-04-12T22:00:52.537 回答