初始化数组时出现堆栈溢出异常

3

我调用了这个方法并得到了一个StackOverflowException的异常。它不是递归调用,只包含数组初始化。我需要一个BigInteger类型的数组,在更大的int类型数组下代码可以正常工作。我展示了一个简化的例子,在实际代码中我无法使用循环来填充数组,因为我无法生成所需的数字,所以必须将它们全部硬编码。

设置: x64模式,.Net Core

错误详情中我们可以看到:

1)堆栈跟踪为空

2)错误可能源自 System.Collections.ListDictionaryInternal

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Before"); // <--- This is displayed

            var a = GetBigIntegers(); // <--- Method is called

            Console.WriteLine("After"); // <--- We will never get there
        }


        static BigInteger[] GetBigIntegers()
        {
            // <--- Crash here
            return new BigInteger[]
            {
                1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
                // Many more lines (850-900) and they are 2-3 times longer than here
                1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
            };
        }
    }

我已经检查了 IL 代码,它看起来是正确的,而且大约有 400,000 行。

.method private hidebysig static 
    valuetype [System.Runtime.Numerics]System.Numerics.BigInteger[] GetBigIntegers () cil managed 
{
    // Method begins at RVA 0x207c
    // Code size 1130123 (0x113e8b)
    .maxstack 4
    .locals init (
        [0] valuetype [System.Runtime.Numerics]System.Numerics.BigInteger[]
    )

    // (no C# code)
    IL_0000: nop
    IL_0001: ldc.i4 66500
    IL_0006: newarr [System.Runtime.Numerics]System.Numerics.BigInteger
    IL_000b: dup
    IL_000c: ldc.i4.0
    //  return new BigInteger[66500]IL_000d: ldc.i4.1
    IL_000e: call valuetype [System.Runtime.Numerics]System.Numerics.BigInteger [System.Runtime.Numerics]System.Numerics.BigInteger::op_Implicit(int32)
    // (no C# code)
    IL_0013: stelem [System.Runtime.Numerics]System.Numerics.BigInteger
    IL_0018: dup
    IL_0019: ldc.i4.1
    IL_001a: ldc.i4.1
    IL_001b: call valuetype [System.Runtime.Numerics]System.Numerics.BigInteger [System.Runtime.Numerics]System.Numerics.BigInteger::op_Implicit(int32)
    IL_0020: stelem [System.Runtime.Numerics]System.Numerics.BigInteger
.....
    IL_113e75: dup
    IL_113e76: ldc.i4 66499
    IL_113e7b: ldc.i4.1
    IL_113e7c: call valuetype [System.Runtime.Numerics]System.Numerics.BigInteger [System.Runtime.Numerics]System.Numerics.BigInteger::op_Implicit(int32)
    IL_113e81: stelem [System.Runtime.Numerics]System.Numerics.BigInteger
    IL_113e86: stloc.0
    IL_113e87: br.s IL_113e89

    IL_113e89: ldloc.0
    IL_113e8a: ret
} // end of method Program::GetBigIntegers

我本以为数组会被初始化并返回,但实际上出现了StackOverflow错误。

我知道我可以使用不同的方法来完成相同的事情,但我想知道为什么它不能以这种方式工作。希望对阅读此问题的每个人都有趣。


2
也贴一下堆栈跟踪信息怎么样? - Uwe Keim
https://dev59.com/FXM_5IYBdhLWcg3wZSXX - Markus Appel
当您删除其中一些行时会发生什么?删除大约90%的这900行并查看错误是否仍然发生。 - LoukMouk
你猜你已经通过了40亿大关了? - Markus Appel
这可能与数组本身无关,而是与为其生成的初始化代码有关(该代码非常庞大,我可以想象Jitter会因此而窒息;BigInteger是自定义值类型,初始化不能像本机类型int一样进行优化)。考虑使用“new BigInteger [size]”声明并用“for”循环填充它(作为奖励,这将使您的代码大大提高可读性)。 - Jeroen Mostert
显示剩余10条评论
1个回答

3
实际原因是评估堆栈帧大小不足以容纳所有推入的内容。
这是由于JIT编译器优化隐藏在大型方法内部进行的结构初始化,导致生成的机器代码性能较差。 来源

我已经阅读了您的参考资料,看起来与我的问题相似。但是我不明白JIT编译器将这些对象放在哪个堆栈上。从IL中,我看到任何时候堆栈上不超过4个项目。数组引用(1),数组引用(2),索引(3),要存储的值(4)。您能否解释一下为什么我没有看到导致溢出的分配? - Andrii Siriak
1
这是JIT处理从调用返回的结构的限制。大型结构通过引用隐式返回。因此,当JIT看到返回大型结构的方法时,它会分配一个“JIT临时”来处理返回值。如果您有像这样的大型初始化程序列表,则每个初始化程序都会变成一个调用,并且每个调用都会获得自己的JIT临时结构。 JIT在重复使用此空间方面并不聪明,因此每个临时结构都在堆栈上获得自己的插槽。 JIT还可能将其他副本复制到其他临时文件中。 - Andy Ayers
1
该方法需要在过程开始时为所有这些临时变量分配空间。因此,在方法开始时,堆栈指针会有一个非常大的调整,而这个调整会触发堆栈溢出。 - Andy Ayers
检测何时可以重复使用临时变量以避免像这样堆栈溢出并不像人们希望的那么简单。但这是我们应该修复的问题。正如https://github.com/dotnet/coreclr/issues/14103所述,解决方法是将初始化程序声明为int[]数组,然后循环填充您的BigInteger数组。 - Andy Ayers

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