C语言 直接使用函数的返回值而不将其存储在变量中是否更好?

sqserrrh  于 2023-06-21  发布在  其他
关注(0)|答案(3)|浏览(181)

我正在和一个同事讨论如何在我们的代码库中构造返回代码检查。在很多地方,代码看起来像这样:

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。我的同事认为这不必要地增加了函数的堆栈大小,这可能是一个问题,因为它运行在嵌入式系统上,我们有点内存限制。
我的反对意见是,即使在第一种情况下,返回值也必须在内存中的某个地方,所以我不认为内存占用会增加。
谁说得对

3gtaxfhh

3gtaxfhh1#

一般的好做法是将复杂的表达式拆分为多个跨行的表达式。可读、可维护的代码几乎总是比堆栈使用率甚至执行速度更重要。此外,像这样的微优化通常不值得付出努力-假设编译器的优化器会照顾它,除非你有很好的理由相信不是这样。
值得注意的是,如果栈的使用很重要,那么首先就不应该对函数使用如此繁重的API,而应该传递一个指向struct的指针。这实际上是一种堆栈使用优化技术,有时用于一些非常有限的系统,如低端8位微控制器。例如,PIC因其功能失调的堆栈实现而臭名昭著,除了大小之外,您还必须跟踪调用堆栈深度。
你的假设是正确的,返回值不能存储在稀薄的空气中-程序使用的每个值都必须分配到某个地方,无论是存储在命名变量还是匿名临时位置。不一定在堆栈上,也可以在寄存器内,这取决于ABI和调用约定。
在一个更人工的环境中测试你的人工代码https://godbolt.org/z/bYsE1WT7e...

  • gcc x86_64,第一个版本实际上有点慢
  • clang x86_64,两个版本的代码相同。
  • gcc ARM 32,两个版本的代码相同。
  • gcc AVR,第一个版本实际上有点慢

为什么第一个版本在某些系统上比较慢是因为ABI以及特定的编译器如何根据ABI进行优化。ABI规定了返回代码必须存储在何处以及第一个参数必须存储在何处:不同的地方。这反过来可能意味着编译器必须在函数调用之间对数据进行一点 Shuffle 。
经验教训是,过度思考/过度设计各种微优化,并试图为了性能而编写可读性较低的代码,可能会产生相反的效果。一般的最佳做法是:

  • 编写尽可能简单和可读的代码。易读、简单的代码通常具有最佳性能。
  • 不要手动优化,除非你确实发现了性能问题。
  • 除非您对目标CPU和ABI有深入的了解,否则不要手动优化。

例如,我正要发表一个自信的声明,这些片段肯定会被优化相同,但后来发现它们不在一些目标上。

tf7tbtn2

tf7tbtn22#

在符号调试器(您确实应该使用)中,如果没有赋值,检查或推断返回值可能会很麻烦。同样,“step-into”操作也是在语句级别工作的,所以在您的示例中,step-into 将首先输入functionWithLotsOfParameters(),如果您只打算step-into logOnError(),这可能会很麻烦。
风格、实践和可读性的论点是见仁见智的,而性能的论点则值得怀疑;调试器体验是事实。习惯性地以支持调试的风格编写代码显然是一个好主意。
此外,无论函数具有多少参数,该参数都是有效的,这是无关紧要的(尽管它的可取性是一个单独的问题)。对于任何类型的长列表,最好使用清晰的空白布局(即换行符、缩进),更是见仁见智。

nxowjjhe

nxowjjhe3#

这取决于您使用的优化级别。
让我们试试看编译器给出了什么用处。在这个测试中,我使用了以下代码:

#include <stdio.h>
#include <stdlib.h>

int __attribute__ ((noinline)) square(int num) {
    return num * num;
}

void __attribute__ ((noinline)) log_val(int a, int b, int c, int d)
{
    printf("LOG: %i %i %i %i", a, b, c, d);
}

void test_0(int val)
{
    int retval;
    retval = square(val);
    log_val(0,1,2,retval);
}

void test_1(int val)
{
    log_val(0,1,2,square(val));
}

使用GCC for ARM(因为我更熟悉ARM汇编)和flag-O0,我们得到:

test_0:
        push    {r7, lr}
        sub     sp, sp, #16
        add     r7, sp, #0
        str     r0, [r7, #4]
        ldr     r0, [r7, #4]
        bl      square
        str     r0, [r7, #12]
        ldr     r3, [r7, #12]
        movs    r2, #2
        movs    r1, #1
        movs    r0, #0
        bl      log_val
        nop
        adds    r7, r7, #16
        mov     sp, r7
        pop     {r7, pc}
test_1:
        push    {r7, lr}
        sub     sp, sp, #8
        add     r7, sp, #0
        str     r0, [r7, #4]
        ldr     r0, [r7, #4]
        bl      square
        mov     r3, r0
        movs    r2, #2
        movs    r1, #1
        movs    r0, #0
        bl      log_val
        nop
        adds    r7, r7, #8
        mov     sp, r7
        pop     {r7, pc}

我们可以看到,第一个版本增加了16个字节(sub sp, sp, #16),第二个版本增加了8个字节(sub sp, sp, #8)。实际上,使用单独的变量会使堆栈更大。
但这完全没有优化。现在让我们看看当我们启用最低优化级别-O1时会发生什么:

test_0:
        push    {r3, lr}
        bl      square
        mov     r3, r0
        movs    r2, #2
        movs    r1, #1
        movs    r0, #0
        bl      log_val
        pop     {r3, pc}
test_1:
        push    {r3, lr}
        bl      square
        mov     r3, r0
        movs    r2, #2
        movs    r1, #1
        movs    r0, #0
        bl      log_val
        pop     {r3, pc}

在这里我们看到两个函数是完全相同的,甚至不使用堆栈。这意味着对于-O1,生成的程序集与编码风格无关。
因此,如果您不使用优化,您的同事是正确的,但即使启用了最低的优化,结果也是相同的。
使用可读性最强、最容易维护等的方法。但堆栈大小不是问题。

相关问题