为什么 apply 在大列表上抛出 CONTROL-STACK-EXHAUSTED-ERROR?

Why does apply throw a CONTROL-STACK-EXHAUSTED-ERROR on a large list?

(apply #'+ (loop for i from 1 to x collect 1))
如果 x 具有值 253391

有效,但在 253392* 上出现 (SB-KERNEL::CONTROL-STACK-EXHAUSTED-ERROR) 时失败。这比 call-arguments-limit**.

小几个数量级

递归是否耗尽堆栈?如果有,是在 apply 中吗?为什么还没有优化出来?


*也很有趣,(apply #'max (loop for i from 1 to 253391 collect 1)) 抛出错误,但 253390 没问题。

**call-arguments-limit 计算为 4611686018427387903(借助 format~R,原来这是 四五六百一十一四万六一百八十六亿一十八亿四亿二千七百三十八万七千九百三)

parameters that can be passed to a function in SBCL

您没有传递 参数。您传递 个参数

(defun foo (x y) (list x y))

xy 是函数 foo.

参数
(foo 20 22)

2022 是函数 foo.

调用中的 参数

查看变量 call-arguments-limitlambda-parameters-limit

SBCL 和 call-arguments-limit

如果一个函数几乎不能处理声称的参数数量,那么这看起来像是 SBCL 中的一个错误。您可能想要报告此错误。也许他们需要更改 call-arguments-limit.

的值

测试

APPLY 是一种测试方法。

另一个:

(eval (append '(drop-params)
               (loop for i from 1 to 2533911 collect 1)))

也可以使用 FUNCALL 并散布多个参数。

为什么存在限制?

编写 Common Lisp 标准是为了允许在各种不同的计算机上高效地实现。人们认为某些 machine-level 函数调用实现仅支持有限数量的参数。该标准表示支持的参数数量可以低至 50。实际上有些实现支持的参数数量相对较少。

因此 Common Lisp 中的 apply 不是 列表处理 的工具,而是使用计算的参数列表调用函数的工具。

对于列表和向量处理,使用 REDUCE,而不是 APPLY

如果我们想对列表中的所有数字求和,替换

(apply #'+ list)     ; don't use this

(reduce #'+ list)    ; can handle arbitrary long lists

递归

apply is a non-optimized recursive function

我不明白为什么函数 APPLY 应该使用递归。

例如,如果您想到

(apply #'+ '(1 2 3 4 5))

参数的重复求和是由函数 + 而不是 apply 完成的。

这不同于

(reduce #'+ '(1 2 3 4 5))

其中使用两个参数重复调用函数 + 是由 reduce 完成的。

什么导致堆栈耗尽?

虽然递归通常是堆栈耗尽的罪魁祸首,但在这种情况下并非如此。根据 SBCL Internals Manual:

In full call, the arguments are passed creating a partial frame on the stack top and storing stack arguments into that frame.

每个生成的列表元素都存储在新的堆栈帧中,快速耗尽堆栈。据推测,通过 –control-stack-size 向 SBCL 传递一个更大的值会增加函数调用中可以传递的参数数量的实际限制。

为什么 call-arguments-limit 比实际限制大这么多?

对有类似问题的人的 SBCL mailing list response 解释了为什么堆栈大小的实际限制没有反映在 call-arguments-limit 中:

The condition you're seeing here is not due to a fundamental implementation limitation, but rather because of the particular choice of stack size that someone chose -- if the stack size were bigger, this call of yours would not error. [...] Consider that, given our strategy of passing excess arguments on the stack, that the actual maximum number of arguments passable at any given time depends on the program state, and isn't in fact a constant.

规范说 call-arguments-limit must be a constant, so SBCL seems to have defined it as most-positive-fixnum.

来源中有一个 couple of bug reports discussing the issue, and a TODO 表明至少有一个贡献者认为应该将其降低到一个不那么荒谬的值:

;; TODO: Reducing CALL-ARGUMENTS-LIMIT to something reasonable to
;; allow DWORD ops without it looking like a bug would make sense.
;; With a stack size of about 2MB, the limit is absurd anyway.

SBCL 的特殊实现方式 call-arguments-limit 可能有改进的空间并可能导致意外行为,但它确实遵循 ANSI 规范。

实际限制因堆栈中剩余的 space 而异,因此根据此值定义 call-arguments-limit 不符合常量值的规范要求。