如何减少 C 中深度递归函数的堆栈帧?

Posted

技术标签:

【中文标题】如何减少 C 中深度递归函数的堆栈帧?【英文标题】:How can I reduce the stack frame of a deeply recursive function in C? 【发布时间】:2015-04-14 14:32:36 【问题描述】:

假设我有一些操作图形结构的递归函数:

typedef struct Node 
    Data data;
    size_t visited;
    size_t num_neighbors;
    struct Node *neighbors[];
 Node;

void doSomethingWithNode(Node *node)

    if ((!node) || node->visited)
        return;
    node->visited = 1;
    /* Do something with node->data */
    size_t /* some local variables */;
    size_t i;
    for (i = 0; i < node->num_neighbors; i++)
    
        /* Do some calculations with the local variables */
        if (/* something */)
            doSomethingWithNode(node->neighbors[i]);
    

由于我在循环中使用了局部变量,编译器 (gcc) 为这个函数创建了一个比我想的更大的堆栈帧(大量的 pushqpopq 指令即使使用-O3),这是一个问题,因为它是深度递归的。由于访问节点的顺序无关紧要,我可以重构此代码以使用Node 指针堆栈,从而将开销减少到每次迭代一个指针。

    是否有任何提示可以给编译器 (gcc) 以解决此问题? 如果没有,是否可以在不借助汇编的情况下将调用堆栈本身用于我的指针堆栈?

【问题讨论】:

所有递归代码也可以使用循环表示为非递归。如果默认的 8MB(在 Linux 上)不够,您还可以在链接时增加堆栈大小(使用例如 -z stack-size linker option)。虽然我并不真正认为有必要,因为局部变量的数量相对较小(当然取决于“一些局部变量”)并且没有数组。并且局部变量并没有真正用pushpop 指令处理,所以你真的在看正确的代码吗? 在 gcc 手册页中简短查看后,我看到了一个选项 -fconserve-stack。你试过了吗? @Marian:谢谢!我试试看。 @Marian 我刚刚尝试用-fconserve-stack 编译一个编程语言实现。它对探测最大递归深度的测试程序没有任何影响:无论有没有使用该选项编译的解释器,都可以实现相同数量的递归调用。 time make tests 也没有区别。该选项有一个通用名称,但可能针对在它执行任何操作之前必须出现的特定情况。也许你必须在同一个函数中有很多不重叠的块作用域,它们可以折叠到同一个堆栈空间或其他任何地方。 【参考方案1】:

您可以维护要访问的节点的向量或列表(或一些队列,或者可能是堆栈,甚至是任意无序集合)(并且您可能希望维护已访问节点的集合或哈希表) .

然后你将有一个循环选择要访问的容器前面的节点,并可能在该容器的后面添加一些未访问的节点....

阅读有关continuation passing style 和tail calls 的维基页面

Google 还有Deutsch Schorr Waite Algorithm,它可以给你一些想法。

【讨论】:

这就是我说“我可以重构这段代码以使用堆栈”时的意思。 是的,但是正如我所说,顺序并不重要,堆栈可能更容易使用。 但是感谢关于 Deutsch Schorr Waite 算法的建议。这看起来完全正确!唯一的问题是它似乎只假设两个邻居......【参考方案2】:

你能把计算放到它们自己的非递归函数中吗?这样,当您进行递归调用时,所有临时变量的堆栈都不会存在。

更新:看起来至少局部变量中的一些数据对于递归是必不可少的。您可以使用alloca 在堆栈上显式分配内存。

【讨论】:

不,它们的值需要在循环的迭代中保留。 可能是两个循环?一个是递归的,一个不是?堆栈上需要i,因为它对于递归状态至关重要。在这种情况下,任何其他变量是否必不可少? 为了做到这一点,我需要维护一个指针列表,然后传递给递归调用。到那时,我不妨使用循环而不是递归,这正是我在第 2 项中要问的内容。【参考方案3】:

您希望编译器做什么来解决问题?

您当然可以检查您的代码,并尽量减少局部变量的数量,尽可能清楚地表明它们(例如)仅通过使用 const 分配给一次,等等.如果可能,这可能会使编译器重新使用空间。

如果做不到这一点,您可能可以通过迭代来节省一些内存,因为这样就不需要返回地址了。

【讨论】:

【参考方案4】:

您可以使用mallocrealloc 来管理动态增长的节点堆栈。这是管理堆栈的“类”:

typedef struct Stack 
    void **pointers;
    size_t count;
    size_t alloc;
 Stack;

void Stack_new(Stack *stack)

    stack->alloc = 10;
    stack->count = 0;
    stack->pointers = malloc(stack->alloc * sizeof(void*));


void Stack_free(Stack *stack)

    free(stack->pointers);
    stack->pointers = null;


void Stack_push(Stack *stack, void *value)

    if (stack->alloc < stack->count + 1) 
        stack->alloc *= 2;
        stack->pointers = realloc(stack->pointers, stack->alloc * sizeof(void*));
    
    stack->pointers[stack->count++] = value;


void *Stack_pop(Stack *stack)

    if (stack->count > 0)
        return stack->pointers[--stack->count];
    return NULL;

【讨论】:

我不太明白这与递归函数中的局部变量有什么关系? 是的,我可以。但是如果可能的话,为什么不使用调用堆栈呢?这就是我问这个的原因。 @Lundin:我只是帮助将指针堆栈存储在堆栈之外,以避免使用堆栈进行递归。但现在我看到了这个问题,例如每个递归级别的堆栈上有 10 个变量。 @Lundin 这很重要,因为堆栈数据结构可以用作递归调用中的并行堆栈,甚至可以支持程序逻辑是迭代(或尾递归)并且所有上下文都是的替代算法在显式堆栈中。【参考方案5】:

“它是深度递归的”暗示最深的递归发生在不超过 1 个未访问的 neighbor 的路径中。

只有当有超过 1 个有趣的邻居时才让代码递归,否则就循环。

void doSomethingWithNode(Node *node) 
  while (node) 
    if (node->visited) return;
    node->visited = 1;
    /* Do something with node->data */
    size_t /* some local variables */;
    size_t i;
    Node *first = NULL;
    for (i = 0; i < node->num_neighbors; i++) 
        /* Do some calculations with the local variables */
        if (/* something */) 

          // Save the first interesting node->neighbors[i] for later
          if (first == NULL && 
              node->neighbors[i] != NULL && 
              node->neighbors[i]->visited == 0) 
            first = node->neighbors[i];

          else 
            doSomethingWithNode(node->neighbors[i]);
          
        
    
    node = first;
  

这不会减少堆栈帧,而是在只有 1 层时消除递归。 IOWs:当不需要递归时。

递归深度现在应该不再超过 O(log2(n)) 而不是原来的最坏情况 O(n)

【讨论】:

【参考方案6】:

如果您有更多数量的局部变量和数组,那么您可以尝试使用malloc 分配内存,使用单指针和固定偏移对其进行操作。free 退出函数时的内存。

通过这种方式,您将保存堆栈并为所有迭代重用相同的堆(可能)部分。

【讨论】:

【参考方案7】:

如果其他答案不优雅并且需要大量开销,我会发现很多。可能没有好的方法,任何方法都取决于手头的递归类型。

在你的情况下,递归在最后,只需要变量 i 。为减少堆栈帧,您可以为所有其他变量使用全局空间。

如果你想进一步减少并删除 i,你可以使用 node->visisted 作为计数器:

static struct VARS 
    int iSomething;
    Data *dataptr;
    double avg;
 gVars;

void doSomethingWithNode(Node *node)

    if ((!node) || node->visited)
        return;
    /* Do something with node->data */
    /* some local variables in global space */;
    gVars.iSomething= 1;
    for (; node->visited < node->num_neighbors; node->visited++)
    
        /* Do some calculations with the local variables */
        if (/* something */)
            doSomethingWithNode(node->neighbors[node->visited]);
    

【讨论】:

ps:因为这仍然是一个 hack,它也不优雅。 我不明白这有什么帮助。 i 不是问题。问题是我无法从循环中删除的其他局部变量。 如果循环中的这些局部变量在迭代过程中不携带信息,那么您也可以将它们移动到全局空间。如果是这样,您甚至可以考虑将它们移动到节点中。【参考方案8】:

将所有对递归不是必需的局部变量放入struct locals 并使用plocals-&gt; 访问它们。将计算放入自己的非递归函数(Arkadiy 的答案)的优势在于,如果需要,变量是有效的并在递归中保留它们的值。

#include <stddef.h>

struct Data 
    char data[1];
;

typedef struct Node 
    struct Data data;
    size_t visited;
    size_t num_neighbors;
    struct Node *neighbors;
 Node;

struct Locals 
    /* local variables not essential for recursion */;
;
static void doSomethingWithNodeRecurse(Node *node, struct Locals *plocals)

    if ((!node) || node->visited)
        return;
    node->visited = 1;
    /* Do something with node->data */
    /* local variables essential for recursion */
    size_t i;
    for (i = 0; i < node->num_neighbors; i++)
    
        /* Do some calculations with the local variables */
        if (1/* something */)
            doSomethingWithNodeRecurse(&node->neighbors[i], plocals);
        /* Do some calculations with the local variables */
    


void doSomethingWithNode(Node *node)

    struct Locals locals;

    doSomethingWithNodeRecurse(node, &locals);

如果变量仍然太大而无法在堆栈上分配它们,则可以按照 Vagish 的建议将它们分配在堆上:

#include <stddef.h>
#include <stdlib.h>

struct Data 
    char data[1];
;

typedef struct Node 
    struct Data data;
    size_t visited;
    size_t num_neighbors;
    struct Node *neighbors;
 Node;

struct Locals 
    /* local variables too big for allocation on stack */;
;
void doSomethingWithNode(Node *node)

    struct Locals *plocals;

    if ((!node) || node->visited)
        return;

    /* ---> allocate the variables on the heap <--- */
    if ((plocals = malloc(sizeof *plocals)) == NULL) abort();

    node->visited = 1;
    /* Do something with node->data */
    size_t i;
    for (i = 0; i < node->num_neighbors; i++)
    
        /* Do some calculations with the local variables */
        if (1/* something */)
            doSomethingWithNode(&node->neighbors[i]);
        /* Do some calculations with the local variables */
    
    /* ---> free the variables <--- */
    free(plocals);

【讨论】:

以上是关于如何减少 C 中深度递归函数的堆栈帧?的主要内容,如果未能解决你的问题,请参考以下文章

如何将递归函数转换为使用堆栈?

堆栈溢出一般是由啥原因导致的?

Scheme中的递归和调用堆栈

为啥增加递归深度会导致堆栈溢出错误?

找到最大递归深度

C ++中lambda和常规函数的不同堆栈深度?