为什么这些函数调用没有优化?

时间:2016-03-08 12:52:38

标签: c gcc assembly optimization clang

我尝试使用Clang和GCC编译此代码:

struct s { int _[50]; };

void (*pF)(const struct s), (*pF1)(struct s), (*pF2)(struct s *);

main()
{
    struct s a;

    pF2(&a);

    pF(a), pF1(a);
}

结果是一样的。虽然不允许调用pF来修改其唯一参数,但会复制对象a以进行第二次pF1调用。这是为什么?

这是装配输出(来自GCC):

; main

        push    rbx
        sub rsp, 0D0h
        mov rbx, rsp
        mov rdi, rsp
        call    cs:pF2

    ;create argument for pF1 call (as there the argument is modified)
    ;and copy the local a into it
    ;although it seems not needed because the local isn't futher read anyway

        sub rsp, 0D0h
        mov rsi, rbx
        mov ecx, 19h
        mov rdi, rsp
    ;

        rep movsq
        call    cs:pF

    ;copy the local a into the argument created once again
    ;though the argument cannot be modified by the function pointed by pF

        mov rdi, rsp
        mov rsi, rbx
        mov ecx, 19h
        rep movsq
    ;

        call    cs:pF1
        add rsp, 1A0h
        xor eax, eax
        pop rbx
        retn

优化程序可以看到pF指向的函数无法修改其参数(因为它声明为const),因此省略了上次复制操作?另外,最近我看到,由于变量a未在代码中进一步读取,因此可以将其存储用于函数参数。

相同的代码可以写成:

; main

            push    rbx
            sub rsp, 0D0h

            mov rdi, rsp
            call    cs:pF2

            call    cs:pF

            call    cs:pF1
            add rsp, 0D0h
            xor eax, eax
            pop rbx
            retn

我正在使用-O3标志进行编译。我错过了什么吗?

即使我没有调用UB(因为函数指针默认为NULL),它也是一样的,而我将它们初始化为:

#include <stdio.h>

struct s { int _[50]; };

extern void f2(struct s *a);

void (*pF)(const struct s), (*pF1)(struct s), (*pF2)(struct s *) = f2;

extern void f1(struct s a)
{
    a._[2] = 90;
}

extern void f(const struct s a)
{
    for(size_t i = 0; i < sizeof(a._)/sizeof(a._[0]); ++i)
        printf("%d\n", a._[i]);
}

extern void f2(struct s *a)
{
    a->_[6] = 90;

    pF1 = f1, pF = f;
}

2 个答案:

答案 0 :(得分:3)

我不相信这种优化是合法的。您忽略的是具有const参数的函数类型与具有非const参数的函数类型兼容,因此可以将变量其参数的函数分配给指针pF

以下是一个示例程序:

struct s {
    int x;
};

/* Black hole so that DCE doesn't eat everything */
void observe(void *);

void (*pF)(const struct s);

void test(struct s arg) {
    arg.x = 0;
    observe(&arg);
}

void assignment(void) {
    pF = test;
}

底线是参数的const注释为编译器提供了关于参数存储是否被被调用者变异的可靠信息。执行此优化似乎要求ABI要求参数存储不需要变异(或者某种整个程序分析,但不要紧,)。

答案 1 :(得分:2)

我认为该功能仍然需要制作一个副本(请参阅最终我认为最佳的允许版本)。其余的(或多或少可以理解)优化失败。

SysV x86-64 ABI并不保证函数不会修改其stack-args。它没有提及const的任何内容。不能保证任何它不保证的东西。它只是说通过值传递的大对象进入堆栈;被调用函数返回时的状态。被调用者“拥有”它的args,即使它们被声明为const。另请参阅 wiki,但ABI doc本身是wiki中唯一真正相关的链接。

类似地,窄整数类型可以在高位中的垃圾寄存器中作为args或返回值。 ABI没有明确说出任何方式,因此不能保证高位被清零。这实际上是gcc的作用:它假设在接收值时存在高垃圾,并且在传递值时会留下高垃圾。对于xmm regs中的float / double也是如此。我最近向一位ABI维护者证实了这一点,同时调查了clang生成的一些不安全的代码。因此,我确信正确的解释是您不应该假设ABI未明确保证的任何内容

gcc没有这样做,但我相信这样的被调用函数实际上不会复制是合法的:

void modifyconstarg(const struct s x) {
  // x.arr[10] = 10;  // This is a compile-time error
  struct s xtmp = x;  // gcc/clang: make a full copy before this
  xtmp.arr[11]=11;
  pFconstval(xtmp);   // gcc/clang: make a full copy here
}

相反,只需存储到其arg和jmp pFconstval

我的猜测是,这是一个错过的优化,而不是gcc和clang在对标准的解释中保守。

gcc和clang似乎并没有很好地优化远远超过寄存器的对象的副本。首先没有复制它们的源代码甚至比编译器可以做的最好的工作要好(例如通过const *或C ++ const-reference),因为我不认为你的建议优化是合法的。

gcc和clang的表现比最佳法律优化要差得多:见the output on godbolt

奇怪的是:使用-march=haswell(或任何其他Intel CPU),gcc会发出对memcpy而不是rep movsq内联代码的函数调用。我不明白。即使使用-ffreestanding / -nostdlib

,它也会执行此操作

IDK,如果其他人一直认为rdi是指向内存的指针,即它是由不可见的引用传递的。我花了很长时间才完全确定按值调用函数根本不接受寄存器中的任何参数。我一直认为rep movsq离开rdi指向高拷贝是很奇怪的。

你不需要函数指针来重现这个;原型的正常函数(以及更具描述性的名称)仍然可以证明它。

struct s { int _[50]; };

//void (*pFconstval)(const struct s), (*pFval)(struct s), (*pFref)(struct s *);
void pFref(struct s *);  void pFconstval(const struct s), pFval(struct s);

void func(void) {
    struct s a;
    pFref(&a);
    pFconstval(a);  pFval(a);
}

void modifyconstarg(const struct s x) {
  // x.arr[10] = 10;  // This is a compile-time error
  struct s xtmp = x;  // full copy here
  xtmp.arr[11]=11;
  pFconstval(xtmp);   // full copy here
}

void modifyarg(struct s x) {
  x.arr[10] = 10;
  pFconstval(x);
}

gcc的modifyarg输出很有趣:

    lea     rdi, [rsp+8]
    mov     DWORD PTR [rsp+48], 10
    mov     ecx, 25
    mov     rsi, rdi                ; src=dest
    rep movsq                       ; in-place "copy"
    jmp     pFconstval

即使您不修改x,也会执行复制。 Clang在尾部调用jmp之前将实际副本发送到其他位置。

您职能的最佳合法版本

据我了解ABI:

    sub     rsp, 416
    mov     rdi, rsp
    call    pFref               ; or call [pF2] if using function pointers.  Is your disassembly in MASM syntax?
    lea     rdi, [rsp+208]      ; aligned by 16 for hopefully better rep movsq perf
                                ; and so the stack is aligned by 16 at each location
    mov     rsi, rsp
    mov     ecx, 25
    rep movsq
    call    pFconstval          ; clobbering the low copy
    add     rsp, 208
    call    pFval               ; clobbering the remaining high copy
    add     rsp, 208
    ret
BTW,gcc对rbx的使用很愚蠢。它节省了四个代码字节:
push / pop:2个字节。 mov rbx, rsp:3B。 2x mov rsi, rbx:2x3B。总计= 12B

用2x lea rsi, [rsp+208]替换所有内容:2x 8B。总计= 16B。

由于还使用mov rdi, rsp,因此无法避免额外的堆栈引擎同步uop。 4B代码不值得花费3 uops。在我的版本中,它只复制一次(并且只需要一个LEA),这也是代码字节的损失。