给定一个程序,我们如何确定执行的保存/恢复操作的数量?

时间:2013-02-04 01:10:18

标签: c

假设我们有以下简单程序:

int foo() {
    int a = 1, b = 2, c = 3, w;
    c = abs(c);
    c = c + b + 7;
    for (w = 0; w<10; w++) {
      b += bar(b);
    }
    return c;
  }

int bar(int v) {
    int a = 1, b = 2;
    a += v + b;
    printf(“v=%d\n”, v);
    return v;
  }

如果调用foo()一次,我们如何确定为变量abc和{{1}执行的保存/恢复操作的数量当存储在调用者 - 存档寄存器vs 被调用者 - 存储寄存器中时?可能是我不理解术语,而是解释一个过程,在这个过程中,每个变量在每种情况下确定每个变量的加载/存储量是多少。

感谢您的时间。

2 个答案:

答案 0 :(得分:2)

唯一的答案是“至少为零”。

  1. 现代编译器将从a中删除bcfoo()。由于这些变量不存在,因此永远不会加载或存储它们。

  2. w变量可能存在,也可以通过展开循环来消除。

  3. a中的bbar()变量不需要,因此它们也可能会被删除。

  4. 输出结果如下:

    编译器可能会产生如下输出:

    int foo()
    {
        printf("v=%d\n", 2);
        printf("v=%d\n", 4);
        printf("v=%d\n", 8);
        printf("v=%d\n", 16);
        printf("v=%d\n", 32);
        printf("v=%d\n", 64);
        printf("v=%d\n", 128);
        printf("v=%d\n", 256);
        printf("v=%d\n", 512);
        printf("v=%d\n", 1024);
        return 12;
    }
    

    如您所见,此优化版本中没有要保存或恢复的变量。

    更新

    使用-O2进行编译时,GCC会将rbxrbp寄存器保存在x86-64上,这些寄存器是被调用者保存的。这些寄存器保存一次并恢复一次。 GCC的foo()输出从不会调用bar(),而是直接调用printf()

    foo:
            pushq   %rbp                ; save register rbp
            movl    $2, %ebp            ; b = 2
            pushq   %rbx                ; save register rbx
            movl    $10, %ebx           ; ctr = 10
            subq    $8, %rsp
    .L4:                                ; begin loop
            movl    %ebp, %esi
            xorl    %eax, %eax
            movl    $.LC0, %edi
            call    printf              ; printf("v=%d\n", b)
            addl    %ebp, %ebp          ; b += b
            subl    $1, %ebx            ; ctr--
            jne     .L4                 ; end loop (when ctr == 0)
            addq    $8, %rsp
            movl    $12, %eax           ; return 12
            popq    %rbx                ; restore register rbx
            popq    %rbp                ; restore register rbp
            ret
    

答案 1 :(得分:0)

术语“调用者保存寄存器”和“被调用者保存寄存器”分别指由函数调用者保存的寄存器和被调用函数保存的寄存器。什么寄存器取决于处理器架构,以及称为“ABI”(应用程序二进制接口)的规范 - 这从处理器系列到处理器系列各不相同,并且在操作系统之间通常也不同。但基本原则是编译器产生的机器代码是按照一定的期望构建的:任何函数都可以自由使用R0,R1,R2,并且调用者必须期望在调用中修改它们。 R3-R6由被调用函数保存,如果调用函数使用R7或R8,它必须在调用任何函数之前保存它们。 [这是一个“任意处理器”,实际上并不存在]。

所以要分析你的功能......

让我们先选择这个功能:

int foo() {
    int a = 1, b = 2, c = 3, w;
    c = abs(c);
    c = c + b + 7;
    for (w = 0; w<10; w++) {
      b += bar(b);
    }
    return c;
  }

未使用变量'a',因此可以删除。 函数'abs'可以在编译时解析,因此可以删除该行。 c = c + b + 7;可以在编译时解析为2 + 3 + 7 = 12.我们只需将常量12移动到return语句并删除c。 所以,现在我们离开了:

int foo() {
    int b = 2, w;

    for (w = 0; w<10; w++) {
      b += bar(b);
    }
    return 12;
  }

至少使用2个寄存器。

对下一个功能做同样的事情:

int bar(int v) {
    int a = 1, b = 2;
    a += v + b;
    printf(“v=%d\n”, v);
    return v;
  }

我们可以删除a = 1, b = 2a += v + b,因为未使用ab。 这留下了:

int bar(int v) {
    printf(“v=%d\n”, v);
    return v;
  }

现在,一个聪明的编译器会将bar内联到foo中,现在我们得到:

int foo() {
    int b = 2, w;

    for (w = 0; w<10; w++) {
      printf(“v=%d\n”, b);
      b += b;
    }
    return 12;
  }

实际上,这可以通过两个或三个寄存器来完成,加上printf需要做的任何事情(我们无法确切地告诉它是什么......)。编译器可能会进行另一步并“展开”循环,在这种情况下,可能会删除一个以上的寄存器。