堆栈溢出初始化大型列表时异常

本文关键字:异常 列表 大型 栈溢出 初始化 堆栈 | 更新日期: 2023-09-27 18:35:07

我正在执行以下操作:

public static class DataHelper
{
  public static List<Seller> Sellers = new List<Seller> {
    {new Seller {Name = "Foo", Location = new LatLng {Lat = 12, Lng = 2}, Address = new Address {Line1 = "blah", City = "cokesville"}, Country = "UK"},
    //plus 3500 more Sellers
  };
}

当我从我的MVC网站内部访问DataHelper.Sellers时,我得到了一个StackOverflowException。 当我使用 Visual Studio 进行调试时,堆栈只有六个帧,并且没有通常明显的堆栈溢出迹象(即没有重复的方法调用)。

应用调用可以像这样简单,以引发异常:

public ActionResult GetSellers()
{
  var sellers = DataHelper.Sellers;
  return Content("done");
}

额外信息:

  • 当我从单元测试中运行相同的代码时,这很好
  • 如果我删除一半的卖家(上半部分或下半部分),在 Web 应用程序中很好,因此任何特定的都不是问题卖方
  • 我尝试将卖家更改为房产并在第一次调用时初始化列表 - 没有帮助
  • 我还尝试将一半添加到一个列表中,然后一半添加到另一个列表并组合 2 - 再次没有帮助

这个的正确答案会给我留下深刻的印象!

堆栈溢出初始化大型列表时异常

这是由于以下事实造成的:实际上,内联列表初始化对于堆栈来说太大了 - 请参阅 msdn 论坛上的这个非常相关的问题,其中的方案几乎相同。

.Net 中的方法同时具有堆栈深度和大小。 StackOverflowException不仅由堆栈上的调用次数引起,还由堆栈中每个方法分配内存的总体大小引起。 在这种情况下,您的方法太大了 - 这是由局部变量的数量引起的。

例如,请考虑以下代码:

    public class Foo
    {
            public int Bar { get; set;}
    }
    public Foo[] GetInts()
    {
        return new Foo[] { new Foo() { Bar = 1 }, new Foo() { Bar = 2 }, 
          new Foo() { Bar = 3 }, new Foo() { Bar = 4 }, new Foo() { Bar = 5 } };
    }

现在看看编译时该方法的导入 IL(这也是一个发布版本):

.maxstack 4
.locals init (
    [0] class SomeExample/Foo '<>g__initLocal0',
    [1] class SomeExample/Foo '<>g__initLocal1',
    [2] class SomeExample/Foo '<>g__initLocal2',
    [3] class SomeExample/Foo '<>g__initLocal3',
    [4] class SomeExample/Foo '<>g__initLocal4',
    [5] class SomeExample/Foo[] CS$0$0000
)

注意 - /之前的实际位,即 SomeExample将取决于定义方法和嵌套类的命名空间和类 - 我不得不编辑几次才能从我正在编写的一些正在进行的代码中删除类型名!

为什么是所有这些当地人? 由于执行内联初始化的方式。 每个对象都更新并存储在"隐藏"本地中,这是必需的,以便可以在每个Foo的内联初始化上执行属性分配(需要对象实例来生成Bar的属性集)。 这也说明了内联初始化只是一些 C# 语法糖。

在您的情况下,正是这些局部变量导致堆栈爆炸(至少有几千个局部变量仅用于顶级对象 - 但您也有嵌套初始化器)。

C# 编译器也可以将每个引用所需的引用数量预加载到堆栈中(为每个属性分配弹出每个引用),但随后会滥用堆栈,其中使用局部变量的性能会好得多。

它也可以使用单个本地,因为每个本地也只是写入,然后通过数组索引存储在列表中,因此再也不需要本地。 这可能是 C# 团队要考虑的一个 - 如果 Eric Lippert 偶然发现这个线程,他可能会对此有一些想法。

现在,这个检查也为我们提供了一个潜在的途径,围绕你的非常庞大的方法使用局部变量:使用迭代器:

public Foo[] GetInts()
{
    return GetIntsHelper().ToArray();
}
private IEnumerable<Foo> GetIntsHelper()
{
    yield return new Foo() { Bar = 1 };
    yield return new Foo() { Bar = 2 };
    yield return new Foo() { Bar = 3 };
    yield return new Foo() { Bar = 4 };
    yield return new Foo() { Bar = 5 };
}

现在 IL for GetInts() 现在只有.maxstack 8在头,没有当地人。 查看迭代器函数GetIntsHelper()我们有:

.maxstack 2
.locals init (
    [0] class SomeExample/'<GetIntsHelper>d__5'
)

所以现在我们已经停止在这些方法中使用所有这些当地人......

......

查看由编译器自动生成的类SomeExample/'<GetIntsHelper>d__5' - 我们看到局部变量仍然存在 - 它们刚刚被提升到该类的字段:

.field public class SomeExample/Foo '<>g__initLocal0'
.field public class SomeExample/Foo '<>g__initLocal1'
.field public class SomeExample/Foo '<>g__initLocal2'
.field public class SomeExample/Foo '<>g__initLocal3'
.field public class SomeExample/Foo '<>g__initLocal4'

所以问题是 - 如果应用于您的场景,该对象的创建是否也会破坏堆栈? 可能不是,因为在内存中,它应该像尝试初始化大型数组一样 - 百万元素数组是完全可以接受的(假设在实践中有足够的内存)。

因此 - 您可以通过转向使用yield每个元素的 IEnumerable 方法来非常简单地修复您的代码。

但最佳实践是,如果您绝对必须对此进行静态定义 - 请考虑将数据添加到磁盘上的嵌入式资源或文件(XML 和 Linq to XML 可能是一个不错的选择),然后根据需要从那里加载它。

更好的是 - 将其粘贴在数据库:)

您如何访问控制器中的 DataHelper.Sellers,您是为此控制器使用 GEt 还是 POST?对于如此大量的数据,您应该使用 POST。

您还需要检查 IIS 堆栈大小:http://blogs.msdn.com/b/tom/archive/2008/03/31/stack-sizes-in-iis-affects-asp-net.aspx

尝试在 ASP 中启用 32 位应用程序。NET 的应用程序池。