我有一个问题,关于保存算术计算以限制堆栈使用是好的。
我们说我有一个像这样的递归函数:
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 - 1
和z - 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_x
和minus_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]);
这种方法限制了所需的实际数学,但在每次调用时,它必须将数组放入数组而不是从寄存器中读取。
我正在努力寻找最佳解决方案,但如果有最佳实践或我在这里缺少的东西,请告诉我。
答案 0 :(得分:1)
FWIW,我用gcc尝试过两个函数foo_1()
(没有额外变量)和foo_2()
(额外变量)。
使用-03 gcc展开for循环(!),两个函数的大小完全相同,但代码不完全相同。我很遗憾没有时间弄清楚他们的不同之处和原因。
使用-02 gcc为foo_1
和foo_2
生成完全相同的代码。正如人们可能期望它为x
,z
,x-1
,z-1
和i
分配了一个注册表,并推送/弹出这些注册表以保留父项的值 - - 为每个呼叫使用6 x 8(64位机器)字节堆栈(包括返回地址)。
您报告使用的24个字节的堆栈...是32位计算机吗?
使用-O0时,图片不同,foo_1
每次绕循环执行x-1
和z-1
,并且在两种情况下变量都保存在内存中。 foo_1
略短,我怀疑减法对现代处理器没有影响!在这种情况下,foo_1
和foo_2
使用相同数量的堆栈。这是因为foo
中的所有变量都是unsigned char
,而额外的minus_x
和minus_z
与i
一起打包,使用的是填充空格。如果您将minus_x
和minus_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位模式)字节堆栈。但是,嘿,这就是你为优化付出的代价!
我不太清楚你最关心的是运行时还是堆栈使用。无论哪种方式,消息是将它留给编译器并且担心当且仅当事情太慢,然后只担心分析显示的位是一个问题!