单声道下的堆栈大小
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了单声道下的堆栈大小相关的知识,希望对你有一定的参考价值。
我编写了一个很小的递归F#代码来查看我可以在.NET / Mono下的堆栈中放入多少级别的递归。它只是在精确的2次幂时打印递归深度,所以我发现最大深度在2的范围内。
我使用System.Threading.Thread (ThreadStart, int)
在具有定义的堆栈空间量的线程中启动代码。在.Net下,每个递归级别似乎需要大约100个字节,而我可以在2G堆栈上获得大约1600万个级别。 Mono下的内存使用情况大致相似,但是我只能获得大约3万个级别。增加传递给Thread
的堆栈大小值超过大约600000
不会增加递归深度。
ulimit
报告堆栈大小限制为1G。
一个明显的解释是,如果它太大,Mono将不服从Thread
的第二个论点。有没有人知道如何说服Mono分配一个大堆栈?
代码是微不足道的,但它只是在某些人关心的情况下:
let rec f i =
if popcount i = 1 then // population count is one on exact powers of 2
printf "Got up to %d\n" i
stdout.Flush ()
if i = 1000000000 then 0 else 1 + f (i+1)
选项1:更改单声道堆栈大小
一个明显的解释是,如果它太大,Mono将不服从
Thread
的第二个论点。有没有人知道如何说服Mono分配一个大堆栈?
你是正确的,Mono将限制堆栈大小,即使你传入一个大的值。例如,在我的Cent OS 64位测试机器上,Mono将分配的最大堆栈大小为2兆字节。 Mono C# source file Thread.cs向我们展示了创建Mono线程时会发生什么:
public Thread (ThreadStart start, int maxStackSize) { if (start == null) throw new ArgumentNullException ("start"); threadstart = start; Internal.stack_size = CheckStackSize (maxStackSize); } static int CheckStackSize (int maxStackSize) { if (maxStackSize < 0) throw new ArgumentOutOfRangeException ("less than zero", "maxStackSize"); if (maxStackSize < 131072) // make sure stack is at least 128k big return 131072; int page_size = Environment.GetPageSize (); if ((maxStackSize % page_size) != 0) // round up to a divisible of page size maxStackSize = (maxStackSize / (page_size - 1)) * page_size; int default_stack_size = (IntPtr.Size / 4) * 1024 * 1024; // from wthreads.c if (maxStackSize > default_stack_size) return default_stack_size; return maxStackSize; }
上面的代码对堆栈大小设置了硬性限制。
理论上,您可以在上述一个或两个函数(粗线)中更改代码,以便分配更大的堆栈大小。完成此操作后,您必须构建Mono运行时,然后运行您的函数以查看更改是否有所不同。
我应该强调一点,我对Mono不太了解,了解分配更大的堆栈是否有助于您的具体情况。我只会这样做作为最后的手段(如果我的其他答案都不起作用)。
选项2:F#尾调用
一种选择是重写您的方法,以便您使用递归tail calls。从之前的(维基百科)链接:
尾递归函数是递归的一种特殊情况,其中在该方法中执行的最后一条指令是递归调用。 F#和许多其他函数语言可以优化尾递归函数;因为在递归调用之后不执行额外的工作,所以函数不需要记住它来自何处,因此没有理由在堆栈上分配额外的内存。
F#通过告诉CLR在执行目标函数之前删除当前堆栈帧来优化尾递归函数。因此,尾递归函数可以无限递归,而不会占用堆栈空间。
有一个警告--Mono不完全支持尾递归调用 - 你应该做的是先在.Net运行时测试,然后看看代码是否会在Mono中运行。
下面是使用尾递归调用重写的示例函数 - 它在.NET(使用Visual Studio 2010)和Mono(Mono版本3.2.0,F#3.0)中都有效:
let f i =
let rec loop acc counter =
// display progress every 10k iterations
let j = counter % 10000
if j = 0 then
printf "Got up to %d\n" counter
stdout.Flush ()
if counter < 1000000000 then
// tail recursive
loop (acc + 1) (counter + 1)
else
acc
loop 0 i
对于输入值1:
let result = f 1
printf "result %d\n" result
输出:
Got up to 10000
Got up to 20000
Got up to 30000
...
Got up to 999980000
Got up to 999990000
Got up to 1000000000
result 999999999
输入值999,999,998:
let result = f 999999998
printf "result %d\n" result
输出:
Got up to 1000000000
result 2
上面的代码使用两个变量来跟踪进度:
acc
- 累加器,用于存储计算结果
counter
- 只是递归调用的数量
为什么您的示例代码不是尾递归?
解释维基百科文章中的How To Write Tail-Recursive Functions部分,我们可以重写这行代码:
if i = 1000000000 then 0 else 1 + f (i+1)
如
if i = 1000000000 then 0
else
let intermediate = f (i+1) // recursion
let result = 1 + intermediate // additional operations
result
尾递归的定义是在递归调用之后不能有其他操作。正如我们在代码的重写版本中所看到的,生成结果需要额外的操作。
资源
- MSDN上的Tail calls in F#
- 维基百科上的F Sharp Programming/Recursion
- Tail calls in Mono - 尾部调用示例,在Mono中不起作用
- 在Mono bug追踪器中的Tail call support in F#
- How can I implement a tail-recursive list append?
选项3:使其迭代
一种选择是重写您的方法,使其不是递归的,而是迭代的。也就是说,您更改方法以使其成为计算结果的循环:
let f i =
let mutable acc = 0
for counter in i .. 1000000000 do
// display progress every 10k iterations
let j = counter % 10000
if j = 0 then
printf "Got up to %d\n" counter
stdout.Flush ()
if counter < 1000000000 then
acc <- acc + 1
acc
对于输入值1:
let result = f 1
printf "result %d\n" result
输出:
Got up to 10000
Got up to 20000
Got up to 30000
...
Got up to 999980000
Got up to 999990000
Got up to 1000000000
result 999999999
输入值999,999,998:
let result = f 999999998
printf "result %d\n" result
输出:
Got up to 1000000000
result 2
上面的代码使用两个变量来跟踪进度:
qazxsw poi - 累加器,存储计算结果
acc
- 只是迭代调用的次数(循环次数)
由于我们使用循环来计算结果,因此没有分配任何额外的堆栈。
此代码绕过单声道限制。它对竞争性编程中的dfs很有用。
counter
以上是关于单声道下的堆栈大小的主要内容,如果未能解决你的问题,请参考以下文章