【问题标题】:How much stack does a recursion use (removing node from list)?递归使用多少堆栈(从列表中删除节点)?
【发布时间】:2016-09-11 09:51:20
【问题描述】:

更新:请注意,就目前而言,这个问题没有多大意义,因为调用堆栈将独立于类型 T 增长。调用堆栈仅取决于正在处理的列表中的节点数- 这些节点中包含的数据对调用堆栈的大小没有影响。


假设我有一个“remove-node-from-list”方法的递归 java 实现,看起来像这样:

// head
public void remove(T data) {
    if (data == null)
        throw new IllegalArgumentException();
    head = remove(head, data);
}

// other nodes of list
private ListNode remove(ListNode current, T data) {
    if (current == null)
        return null;
    if (current.data.compareTo(data) == 0)
        return current.next;
    current.next = remove(current.next, data);
    return current;
} 

让我们进一步假设T 的类型是int

我的问题是: 与我正在处理的列表的大小相比,调用堆栈(在最坏的情况下)会有多大?

到目前为止我的想法: 从我所见,Java 8 并没有优化递归(对吗?我的整个分析都假设这是真的)。因此,我们将拥有一个随列表大小线性增长的调用堆栈。

更准确地说:这个方法remove(ListNode current, T data) 基本上会在我的列表中的每个节点的调用堆栈上放置一个节点引用(32 位在 32 位拱上)和一个 int(也是 32 位)。

现在,由于列表中的一个节点还包含一个引用和一个int,因此一个节点的大小实际上与这些递归调用之一几乎相同。实际上,其中一个调用甚至可能使用更多空间(返回地址也必须被压入堆栈,对吗?或者可以以某种方式优化它吗?)。

所以如果我是正确的,那么在最坏的情况下调用堆栈基本上会和列表本身一样大(当要删除的元素是列表中的最后一个时)!真的是这样吗?如果我是正确的,那么这将意味着处理ints 列表所需的调用堆栈的一个因子甚至更大!因此,在处理大型列表(实际上甚至没有那么大)时,如果使用默认设置的 stacksize (0.5M) 运行它,比如说 1M,就会得到一个 stackoverflow

我知道对于更大的数据类型,这个因素会更少,但无论如何我仍然坚持的基本上是这样的:如果没有进行优化,那么调用堆栈将随着列表的大小线性增长 - 因此人们应该更喜欢迭代而不是递归过程。

总的来说,我会说,调用堆栈将大致线性增长,系数(size_of_all_arguments_in_call + size_of_return_address)/size_of_a_single_listelementints 列表的情况下几乎相同,对吧?请不要误会size_of_all_arguments 的意思:我知道我们只将引用的值传递给那些调用中的对象,而不是整个对象本身。因此,对于大型物体,这个因素可能不是那么引人注目,但我仍然会说人们不应该接受这种不必要的线性空间开销。但是,对于ints 的列表,该因子约为。 1.

这个分析是正确的还是我遗漏了什么?

背景:与我讨论这个问题的人试图让我相信 java 中的调用堆栈不会如此显着增长,并指出我遗漏了一些明显的东西(很遗憾没有告诉我什么对我来说,这真的一点都不明显)-因为我不知道我错过了什么,所以我很想学习;)

相关:

对我来说,相关问题中缺少的主要内容是调用堆栈实际使用多少空间

【问题讨论】:

标签: java list recursion linked-list stack-overflow


【解决方案1】:

递归使用多少堆栈(从列表中删除节点)?

如果不分析代码或了解特定 JVM 的细节,这是一个很难回答的问题。那是因为Java Virtual Machine Specification (JVMS) 给实现者留下了很多细节。

JVMS Chapter 2: 不属于 Java 虚拟机的规范会不必要地限制 实施者的创造力。比如运行时的内存布局 数据区域,使用的垃圾收集算法,以及任何内部 Java 虚拟机指令的优化(例如, 将它们翻译成机器代码)由管理员自行决定 实现者。

这些细节的一部分是堆栈和堆栈框架的实现。而且由于您似乎实际上是在询问递归删除创建的堆栈与算法正在处理的列表之间的关系,因此我们必须考虑堆栈框架甚至可能不像您想象的那么大(或在您的问题中提出建议)。

JVMS (§2.5.2): 因为 Java 虚拟机堆栈永远不会 直接操作,除了推送和弹出框架,框架可能是 已分配堆。

这意味着对于每个堆栈帧,堆栈上可能只有一个引用。如果是这样,那将使堆栈和它正在处理的列表之间的关系对于您提供的示例真正无关紧要。让我们看一些数字:

根据Algorithms (Sedgewick, Wayne)

  • Integer 是 24 个字节
    开销 + 实例变量 (int) + 填充
    = 16 + 4 + 4 = 24 字节

  • 一个非静态递归定义的Node 类嵌套在关联的 List 是 40 个字节 开销 + 引用 + 额外开销(引用 List 类)
    = 16 + 8 + 8 + 8 = 40 字节

因此,我们可以看到列表中的每个条目有 64 个字节。因此,如果有一个框架是堆分配的实现,那么堆栈和您正在处理的列表之间的关系将是8/64 = 1/8 bytes

当栈帧在栈上分配时,栈的大小更难确定。事实上,我们需要进一步的实现细节来确定它。

JVMS (§2.6. Frames): 局部变量数组和操作数栈的大小为 在编译时确定并与代码一起提供 与框架关联的方法(§4.7.3)。因此尺寸 frame 数据结构只依赖于Java的实现 虚拟机,并且可以为这些结构分配内存 同时在方法调用上。

即便如此,我还是要说,以您的示例为例,堆栈相对于列表不太可能以 1:1 或接近 1:1 的比例增长(这里讨论的是内存中的总大小)。当然,您可以提出一个可以验证您的论点的特定问题,但这可能是微不足道的,并且可能与您提供的示例不同。

【讨论】:

    猜你喜欢
    • 2015-03-08
    • 1970-01-01
    • 2012-11-06
    • 2016-07-19
    • 2023-02-23
    • 2022-07-08
    • 1970-01-01
    • 2020-08-03
    • 1970-01-01
    相关资源
    最近更新 更多