堆栈关注:局部变量与Arithmetics

时间:2014-07-30 09:10:07

标签: c performance recursion stack

我有一个问题,关于保存算术计算以限制堆栈使用是好的。

我们说我有一个像这样的递归函数:

void foo (unsigned char x, unsigned char z) {
    if (!x || !z)
        return;
    // Do something
    for (unsigned char i = 0; i < 100; ++i) {
        foo(x - 1, z);
        foo(x, z - 1);
    }
}

这里要看的主要内容是每次循环评估x - 1z - 1
为了提高性能,我会做这样的事情:

const unsigned char minus_x = x - 1;
const unsigned char minus_z = z - 1;
for (unsigned char i = 0; i < 100; ++i) {
    foo(minus_x, z);
    foo(x, minus_z);
}

但这样做意味着每次通话时,minus_xminus_z都会保存在堆叠中。递归函数可能被调用了数千次,这意味着堆栈中使用了千字节。此外,所涉及的数学并不像-1那么简单。

这是个好主意吗? 编辑:它实际上是无用的,因为它是编译器非常标准的优化:Loop-invariant code motion (参见HansPassant的评论)

使用包含以下计算的静态数组会更好吗

static const char minuses[256] = {/* 0 for x = 0; x - 1 for x = 1 to 255 */}

然后执行:

foo(minuses[x], z);
foo(x, minuses[z]);

这种方法限制了所需的实际数学,但在每次调用时,它必须将数组放入数组而不是从寄存器中读取。

我正在努力寻找最佳解决方案,但如果有最佳实践或我在这里缺少的东西,请告诉我。

1 个答案:

答案 0 :(得分:1)

FWIW,我用gcc尝试过两个函数foo_1()(没有额外变量)和foo_2()(额外变量)。

使用-03 gcc展开for循环(!),两个函数的大小完全相同,但代码不完全相同。我很遗憾没有时间弄清楚他们的不同之处和原因。

使用-02 gcc为foo_1foo_2生成完全相同的代码。正如人们可能期望它为xzx-1z-1i分配了一个注册表,并推送/弹出这些注册表以保留父项的值 - - 为每个呼叫使用6 x 8(64位机器)字节堆栈(包括返回地址)。

您报告使用的24个字节的堆栈...是32位计算机吗?

使用-O0时,图片不同,foo_1每次绕循环执行x-1z-1,并且在两种情况下变量都保存在内存中。 foo_1略短,我怀疑减法对现代处理器没有影响!在这种情况下,foo_1foo_2使用相同数量的堆栈。这是因为foo中的所有变量都是unsigned char,而额外的minus_xminus_zi一起打包,使用的是填充空格。如果您将minus_xminus_z更改为unsigned long long,则会有所不同。奇怪的是,foo_1也使用了6 x 8字节的堆栈。堆栈帧中有16个未使用的字节,因此即使考虑将RSP和RBP对齐到16字节边界,它似乎使用了超过它需要的...我不知道为什么。

我快速查看了x - 1的静态数组。对于-O0,它对堆栈的使用没有任何影响(出于与之前相同的原因)。对于-O2,只需查看foo(x, minuses[z]);并将minuses[z]提升到循环之外!哪一个应该预期......并且堆栈使用保持不变(6 x 8)。

更一般地说,正如其他地方所指出的那样,任何有效的优化量都会在可能的情况下从循环中提升计算。正在进行的另一件事是大量使用寄存器来保存变量 - 包括实变量(你已命名的变量)和“伪”变量(用于保存已提升的东西的预先计算结果)。这些寄存器需要通过子程序的调用保存 - 无论是调用者还是被调用者。 x86 push / pop操作整个寄存器,因此寄存器中保存的unsigned char将需要一个完整的8或4(64位或32位模式)字节堆栈。但是,嘿,这就是你为优化付出的代价!

我不太清楚你最关心的是运行时还是堆栈使用。无论哪种方式,消息是将它留给编译器并且担心当且仅当事情太慢,然后只担心分析显示的位是一个问题!