【问题标题】:Coding in C: efficiency of temporary local variablesC中的编码:临时局部变量的效率
【发布时间】:2014-05-02 20:38:11
【问题描述】:

我想知道:用 C 编程,假设我们有两个函数:

int get_a_value();
int calculate_something(int number);

还有第三个版本的两个版本:

/* version 1 */
int main()
{
    int value = get_a_value();
    int result = calculate_something(value);
    return result;
}

/* version 2 */
int main()
{
    int result = calculate_something(get_a_value());
    return result;
}

理论上,同一事物的这两个版本在正确性、内存使用和效率方面有什么区别?他们会产生不同的指令吗?另一方面,在什么情况下,可能的差异会在现实中变得显着?

提前致谢。

【问题讨论】:

  • 任何现代编译器编译器都应该产生相同的代码。提高可读性,让微优化成为编译器的工作。
  • 嗯...因为我不知道如何检查汇编程序。你看,我是新来的。但在这一点上也欢迎提供帮助。
  • 我有兴趣了解编译器是如何工作的。抱歉,如果我不识字...如果我知道该怎么做,我不会问。建设性的 cmets 比几乎粗鲁的 cmets 更受欢迎。如果你觉得我应该换个方式问,你可以客气地说。
  • 这个问题是相关的,因为 Martin Fowler 的 refactoring 原则 Introduce Explaining Variable aka Extract Variable

标签: c function variables coding-style


【解决方案1】:

复制两个版本,使用gcc -S 编译每个版本以获得机器语言输出,使用sdiff 并排比较。

使用 gcc 版本 4.1.2 20070115 (SUSE Linux) 的结果:

没有优化:

main:                                         main:
.LFB2:                                        .LFB2:
        pushq   %rbp                                  pushq   %rbp
.LCFI0:                                       .LCFI0:
        movq    %rsp, %rbp                            movq    %rsp, %rbp
.LCFI1:                                       .LCFI1:
        subq    $16, %rsp                             subq    $16, %rsp
.LCFI2:                                       .LCFI2:
        movl    $0, %eax                              movl    $0, %eax
        call    get_a_value                           call    get_a_value
        movl    %eax, -8(%rbp)              |         movl    %eax, %edi
        movl    -8(%rbp), %edi              <
        movl    $0, %eax                              movl    $0, %eax
        call    calculate_something                   call    calculate_something
        movl    %eax, -4(%rbp)                        movl    %eax, -4(%rbp)
        movl    -4(%rbp), %eax                        movl    -4(%rbp), %eax
        leave                                         leave
        ret                                           ret

基本上,一个额外的移动指令。两者都分配相同数量的堆栈空间(subq $16, %rsp 为堆栈保留 16 个字节),因此在内存方面没有区别。

一级优化(-O1):

main:                                       main:
.LFB2:                                      .LFB2:
        subq    $8, %rsp                              subq    $8, %rsp
.LCFI0:                                     .LCFI0:
        movl    $0, %eax                              movl    $0, %eax
        call    get_a_value                           call    get_a_value
        movl    %eax, %edi                            movl    %eax, %edi
        movl    $0, %eax                              movl    $0, %eax
        call    calculate_something                   call    calculate_something
        addq    $8, %rsp                              addq    $8, %rsp
        ret                                           ret

没有区别。

使用 gcc 版本 2.96 20000731(Red Hat Linux 7.2 2.96-112.7.2)的结果:

没有优化:

main:                                         main:
        pushl   %ebp                                  pushl   %ebp
        movl    %esp, %ebp                            movl    %esp, %ebp
        subl    $8, %esp                              subl    $8, %esp
                                             >        subl    $12, %esp
                                             >        subl    $4, %esp
        call    get_a_value                           call    get_a_value
                                             >        addl    $4, %esp
        movl    %eax, %eax                            movl    %eax, %eax
        movl    %eax, -4(%ebp)               |        pushl   %eax
        subl    $12, %esp                    <
        pushl   -4(%ebp)                     <
        call    calculate_something                   call    calculate_something
        addl    $16, %esp                             addl    $16, %esp
        movl    %eax, %eax                            movl    %eax, %eax
        movl    %eax, -8(%ebp)               |        movl    %eax, -4(%ebp)
        movl    -8(%ebp), %eax               |        movl    -4(%ebp), %eax
        movl    %eax, %eax                            movl    %eax, %eax
        leave                                         leave
        ret                                           ret

指令数量大致相同,排序略有不同。

一级优化(-O1):

main:                                         main:
        pushl   %ebp                                    pushl   %ebp
        movl    %esp, %ebp                              movl    %esp, %ebp
        subl    $8, %esp                      |         subl    $24, %esp
        call    get_a_value                             call    get_a_value
        subl    $12, %esp                     |         movl    %eax, (%esp)
        pushl   %eax                          <
        call    calculate_something                     call    calculate_something
        leave                                           leave
        ret                                             ret

看起来第二个版本保留了更多的堆栈空间。

因此,对于使用这些特定编译器的这个特定示例,两个版本之间没有太大区别。在这种情况下,我倾向于使用第一个版本,原因如下:

  1. 在调试器中更容易跟踪;您可以检查从get_a_value 返回的值,然后再将其传递给calculate_something
  2. 它为您提供了一个进行完整性检查的地方,以防 calculate_something 对于某些输入的行为不正常;
  3. 眼睛更容易一些。

请记住,简洁并不一定意味着快速高效,在一种特定的编译器/硬件组合下快速/高效可能在不同的编译器/硬件组合下被无可救药地破坏。一些编译器实际上更容易优化以清晰方式编写的代码。

您的代码应该是:

  1. 正确 - 如果不满足要求,它的速度或使用的内存量如何无关紧要
  2. 安全 - 如果它是恶意软件载体或有将敏感数据暴露给未经授权方的风险(是的,我说的是 Heart-流血);
  3. 稳健 - 如果因为有人在另一个房间打喷嚏而转储核心,它 并不重要 使用多少内存;
  4. 可维护 - 如果由于需求发生变化(他们确实如此)而不得不报废和重写它,那么它的速度有多快或使用的内存有多少并不重要;
  5. 高效 - 现在您可以开始担心性能和效率了。

【讨论】:

    【解决方案2】:

    我做了一个小测试,为 2 个版本生成汇编代码。简单地从 bash 运行 diff 命令表明第一个版本比第二个版本多 2 条指令。
    如果您想自己尝试,只需使用此命令编译即可

    gcc -S main.c -o asmout.s
    gcc -S main2.c -o asmout2.s
    

    然后检查差异

    diff asmout.s asmout2.s
    

    我得到了第一个更多的这两条指令:

    movl    %eax, -8(%rbp)
    movl    -8(%rbp), %eax
    

    编辑:
    正如 Keith Thompson 所建议的,如果使用优化选项进行编译,则生成的汇编代码对于两个版本都是相同的。

    【讨论】:

    • 你编译时没有优化选项,这是告诉编译器你不关心性能的一种方式。当我用gcc -O3 -S编译时,完全没有区别。
    • 谢谢,我试试。您能想到这种差异显着的真实情况吗?
    • @KeithThompson 你是对的,使用优化选项运行会导致相同的结果。对于 OP,我真的认为更多的是你编写代码更舒服的方式而不是性能问题。
    【解决方案3】:

    这确实取决于平台和编译器,但是通过优化它们通常应该生成相同的代码。在最坏的情况下,一个版本会为额外的 int 分配空间。如果将 get_a_value 的值放在变量中会使您的代码更具可读性,那么我会继续这样做。我唯一建议不要这样做是在深度递归函数中。

    【讨论】:

      猜你喜欢
      • 2013-06-26
      • 2021-03-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-09-26
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多