我正在和一个同事讨论如何在我们的代码库中构造返回代码检查。在很多地方,代码看起来像这样:
logOnError(functionWithLotsOfParameters(a, b, c, d, etc),
"Error Message", module_name, some_more_stuff);
我认为这掩盖了这里实际发生的事情,也就是内部函数的函数调用。我想这样构造代码:
ReturnType ret;
ret = functionWithLotsOfParameters(a, b, c, d, etc);
logOnError(ret, "Error Message", module_name, some_more_stuff);
ReturnType
在这个上下文中基本上只是一个unsigned int
。我的同事认为这不必要地增加了函数的堆栈大小,这可能是一个问题,因为它运行在嵌入式系统上,我们有点内存限制。
我的反对意见是,即使在第一种情况下,返回值也必须在内存中的某个地方,所以我不认为内存占用会增加。
谁说得对
3条答案
按热度按时间3gtaxfhh1#
一般的好做法是将复杂的表达式拆分为多个跨行的表达式。可读、可维护的代码几乎总是比堆栈使用率甚至执行速度更重要。此外,像这样的微优化通常不值得付出努力-假设编译器的优化器会照顾它,除非你有很好的理由相信不是这样。
值得注意的是,如果栈的使用很重要,那么首先就不应该对函数使用如此繁重的API,而应该传递一个指向struct的指针。这实际上是一种堆栈使用优化技术,有时用于一些非常有限的系统,如低端8位微控制器。例如,PIC因其功能失调的堆栈实现而臭名昭著,除了大小之外,您还必须跟踪调用堆栈深度。
你的假设是正确的,返回值不能存储在稀薄的空气中-程序使用的每个值都必须分配到某个地方,无论是存储在命名变量还是匿名临时位置。不一定在堆栈上,也可以在寄存器内,这取决于ABI和调用约定。
在一个更人工的环境中测试你的人工代码https://godbolt.org/z/bYsE1WT7e...
为什么第一个版本在某些系统上比较慢是因为ABI以及特定的编译器如何根据ABI进行优化。ABI规定了返回代码必须存储在何处以及第一个参数必须存储在何处:不同的地方。这反过来可能意味着编译器必须在函数调用之间对数据进行一点 Shuffle 。
经验教训是,过度思考/过度设计各种微优化,并试图为了性能而编写可读性较低的代码,可能会产生相反的效果。一般的最佳做法是:
例如,我正要发表一个自信的声明,这些片段肯定会被优化相同,但后来发现它们不在一些目标上。
tf7tbtn22#
在符号调试器(您确实应该使用)中,如果没有赋值,检查或推断返回值可能会很麻烦。同样,“step-into”操作也是在语句级别工作的,所以在您的示例中,step-into 将首先输入
functionWithLotsOfParameters()
,如果您只打算step-intologOnError()
,这可能会很麻烦。风格、实践和可读性的论点是见仁见智的,而性能的论点则值得怀疑;调试器体验是事实。习惯性地以支持调试的风格编写代码显然是一个好主意。
此外,无论函数具有多少参数,该参数都是有效的,这是无关紧要的(尽管它的可取性是一个单独的问题)。对于任何类型的长列表,最好使用清晰的空白布局(即换行符、缩进),更是见仁见智。
nxowjjhe3#
这取决于您使用的优化级别。
让我们试试看编译器给出了什么用处。在这个测试中,我使用了以下代码:
使用GCC for ARM(因为我更熟悉ARM汇编)和flag-O0,我们得到:
我们可以看到,第一个版本增加了16个字节(
sub sp, sp, #16
),第二个版本增加了8个字节(sub sp, sp, #8
)。实际上,使用单独的变量会使堆栈更大。但这完全没有优化。现在让我们看看当我们启用最低优化级别
-O1
时会发生什么:在这里我们看到两个函数是完全相同的,甚至不使用堆栈。这意味着对于
-O1
,生成的程序集与编码风格无关。因此,如果您不使用优化,您的同事是正确的,但即使启用了最低的优化,结果也是相同的。
使用可读性最强、最容易维护等的方法。但堆栈大小不是问题。