gcc 将带有常量数据的结构传递给C语言中的函数

jv2fixgn  于 2022-11-12  发布在  其他
关注(0)|答案(1)|浏览(127)

我有一段代码,其中的循环调用了一个编译时常量结构的函数。我知道必须使用一个 compound literal 才能有一个匿名的结构(或数组)。
由于复合字面值的生命周期是在其创建时的作用域中,因此我担心使用复合字面值可能会导致每次循环运行时不必要地重新创建和销毁结构体。为了避免这种情况,我将结构体移到了循环外部的变量中。

struct Struct {
    int x, y, z;
};
extern void func(struct Struct);
void test1(void) {
    for (int i = 1000000; i--;)
        func((struct Struct){1, 2, 3});
}
void test0(void) {
    const struct Struct s = {1, 2, 3};
    for (int i = 1000000; i--;)
        func(s);
}

我通过Godbolt编写了tested the above,正如预期的那样,test1似乎在循环中构造了数据。我还希望优化编译器能够识别这种构造并进行相应的优化,所以我添加了-Os,但函数仍然是differ in assembly,尽管它们具有相同的行为。
有人告诉我,一个优化编译器应该为具有相同行为的结构生成最优(因此也是相同的)代码。

哪个选项实际上更有可能提供更好的性能?

wf82jlnq

wf82jlnq1#

哪个选项实际上更有可能提供更好的性能?
这取决于不同编译器的特定版本中编译器遗漏的优化错误。编译器 * 应该 * 为此生成相同asm
GCC从内存中加载常量数据而不是在循环内加载mov esi, 3是很愚蠢的。在循环外加载mov r12, 0x200000001也比加载常量更有意义。第一个版本在x86-64的gcc -Os中看起来不错。在-O3中情况就更糟了,但仍然没有GCC编译第二个版本的方式那么糟糕,在第二个版本中,它从.rodata加载这两个版本。
当你发现相同的源代码有不同的asm时,通常是因为缺少优化或者是两个不同但有效的调优选择。在这种情况下,从内存加载是GCC中缺少优化的bug,你应该报告(https://gcc.gnu.org/bugzilla/,使用关键字[missed-optimization])。
Clang在这两种情况下都做得很好:https://godbolt.org/z/TP1Mdx6vn

test1:                                  # @test1
        push    rbp
        push    rbx
        push    rax       # dummy push to align the stack
        mov     ebp, -1000000
        movabs  rbx, 8589934593
.LBB0_1:                               # =>This Inner Loop Header: Depth=1
        mov     rdi, rbx
        mov     esi, 3
        call    func@PLT
        inc     ebp
        jne     .LBB0_1            # }while(++i != 0)

        add     rsp, 8
        pop     rbx
        pop     rbp
        ret

将10字节的mov r64, imm64指令从循环中提升出来是有意义的,就像clang和GCC一直在做的那样。根据Agner Fog's microarch guide,从Sandybridge-family上的uop缓存中获取指令可能需要一个额外的周期。循环运行多次迭代,足以分摊保存/恢复另一个调用保留寄存器的额外成本。
但是mov esi, 3只是一个5字节的指令,即使func实际上很短,也足够便宜,可以把它留在循环中,所以这是一个非常紧密的循环。
当GCC或clang需要一个整数寄存器中的64位常量时,它们通常使用mov-immediate。64位值是2个结构体成员的一些东西似乎会让GCC在某些情况下从.rodata加载它。
但是使用-Os(针对大小进行优化),它能够避免这种情况,这有点令人惊讶。确实,10字节的mov-立即数比8字节的数据加上7字节的mov r64, [rip+rel32](雷克斯+ opcode + ModRM + rel 32)的总大小要小。
但是,如果它能够看到一个移动立即作为一个选项,IDK为什么它不会选择它的性能时,GCC通常会这样做时,调用int foo(uint64_t, int)
编译器是一种非常复杂的机器,它通过转换程序逻辑的内部表示来工作。通常,它们从不同的起点得到同样的有效结果,但并不总是如此。

相关问题