【问题标题】:Why aren't those function calls optimized?为什么这些函数调用没有优化?
【发布时间】:2016-06-22 10:21:06
【问题描述】:

我尝试使用 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;
}

【问题讨论】:

  • 你期望优化器知道一些它没有代码的东西。正如@JoachimPileborg 指出的那样,保持按值传递工作的唯一方法是给pF 和pF1 一份副本。
  • @MichaelFoukarakis:我猜OP的目标是:编译器可以看到有问题的数据仍在堆栈上,不需要第二个副本,只需调整@987654336 @。但这只有在函数 impls 不做一些“非常有趣”的事情时才有效 - 例如将 const 丢弃。
  • @peterchen 像这样“有趣”的东西不会是 UB(因此默认情况下不是预期的)?
  • @peterchen 这个问题被标记为 c 而不是 c++。
  • 参数类型的顶级 const 实际上并没有做任何事情。两个函数声明,其中一个将顶级 const 添加到某些参数类型是兼容的。 (这里有一个 C++ 现场演示:coliru.stacked-crooked.com/a/15a735168f34cd46)

标签: c gcc assembly optimization clang


【解决方案1】:

我不认为这种优化是合法的。您忽略的是具有 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 使得参数存储不需要发生突变(或某种整体程序分析,但不要介意)。

【讨论】:

  • 有趣。不过,即使没有函数指针,gcc 和 clang 也不会进行优化。 (pF* 函数只是用普通原型声明的,所以编译器会发出普通的 call 指令而无需间接)。 goo.gl/3vk9I3
  • Peter:问题还是一样——函数的实现可能会改变存储。底线是参数的const 注释不会给编译器提供可靠的信息。
  • 我认为执行此优化将要求 ABI 使得参数存储不需要被突变(或某种整个程序分析,但没关系)。
  • 同意,这就是我在您发帖时的想法。但是,这里仍然存在主要的编译器优化失败,因为它们制作的副本超出了必要的数量。看我的回答
  • @martinkunev tbh 应该由发布它的人来更新答案 - 毕竟这只是很短的时间。
【解决方案2】:

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


SysV x86-64 ABI 不保证函数不会修改其堆栈参数。它没有提到const。任何它不能保证的东西都不能假设。它只是说按值传递的大对象在堆栈上;被调用函数返回时的状态无关紧要。被调用者“拥有”它的参数,即使它们被声明为const。另请参阅 wiki,但 ABI 文档本身是 wiki 中唯一真正相关的链接。

同样,窄整数类型可以在寄存器中使用高位垃圾作为参数或返回值。 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(或任何其他英特尔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);
}

modifyarg 的 gcc 输出很有趣:

    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

顺便说一句,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 微秒。在我的版本中,它只复制一次(并且只需要一个 LEA),这也是代码字节的损失。

【讨论】:

    猜你喜欢
    • 2013-10-19
    • 2017-05-15
    • 2017-07-25
    • 1970-01-01
    • 2012-03-31
    • 2019-06-28
    • 1970-01-01
    • 2022-12-18
    • 1970-01-01
    相关资源
    最近更新 更多