按引用和按值传递时的 gcc 程序集

Posted

技术标签:

【中文标题】按引用和按值传递时的 gcc 程序集【英文标题】:gcc assembly when passing by reference and by value 【发布时间】:2015-10-17 15:29:29 【问题描述】:

我有一个计算乘积的简单函数 两个双数组:

#include <stdlib.h>
#include <emmintrin.h>

struct S 
    double *x;
    double *y;
    double *z;
;

void f(S& s, size_t n) 
    for (int i = 0; i < n; i += 2) 
        __m128d xs = _mm_load_pd(&s.x[i]);
        __m128d ys = _mm_load_pd(&s.y[i]);
        _mm_store_pd(&s.z[i], _mm_mul_pd(xs, ys) );
    
    return;


int main(void) 
    S s;
    size_t size = 4;
    posix_memalign((void **)&s.x, 16, sizeof(double) * size);
    posix_memalign((void **)&s.y, 16, sizeof(double) * size);
    posix_memalign((void **)&s.z, 16, sizeof(double) * size);
    f(s, size);
    return 0;

请注意,函数 f 的第一个参数是通过引用传入的。 让我们看一下 f() 的结果汇编(我删除了一些不相关的 件,插入 cmets 并放置一些标签):

$ g++ -O3 -S asmtest.cpp 


        .globl      _Z1fR1Sm
_Z1fR1Sm:
        xorl        %eax, %eax
        testq       %rsi, %rsi
        je  .L1
.L5:
        movq        (%rdi), %r8             # array x   (1)
        movq        8(%rdi), %rcx           # array y   (2)
        movq        16(%rdi), %rdx          # array z   (3)
        movapd      (%r8,%rax,8), %xmm0     # load x[0]
        mulpd       (%rcx,%rax,8), %xmm0    # multiply x[0]*y[0]
        movaps      %xmm0, (%rdx,%rax,8)    # store to y
        addq        $2, %rax                # and loop
        cmpq        %rax, %rsi
        ja  .L5

注意数组 x、y、z 的地址被加载到通用 每次迭代的寄存器,参见语句 (1),(2),(3)。为什么 gcc 不动 这些指令在循环之外?

现在制作结构的本地副本(不是深层副本):

void __attribute__((noinline)) f(S& args, size_t n) 
    S s = args;
    for (int i = 0; i < n; i += 2) 
        __m128d xs = _mm_load_pd(&s.x[i]);
        __m128d ys = _mm_load_pd(&s.y[i]);
        _mm_store_pd(&s.z[i], _mm_mul_pd(xs, ys) );
    
    return;

组装:

_Z1fR1Sm:
.LFB525:
        .cfi_startproc
        xorl        %eax, %eax
        testq       %rsi, %rsi
        movq        (%rdi), %r8     # (1)
        movq        8(%rdi), %rcx   # (2)
        movq        16(%rdi), %rdx  # (3)
        je  .L1
.L5:
        movapd      (%r8,%rax,8), %xmm0
        mulpd       (%rcx,%rax,8), %xmm0
        movaps      %xmm0, (%rdx,%rax,8)
        addq        $2, %rax
        cmpq        %rax, %rsi
        ja  .L5
.L1:
        rep ret

请注意,与前面的代码不同, 负载 (1)、(2)、(3) 现在位于循环之外。

我希望能解释一下为什么这两个程序集 代码不同。内存别名在这里是否相关? 谢谢。

$ gcc --version gcc (Debian 5.2.1-21) 5.2.1 20151003

【问题讨论】:

你的 gcc 版本是... @KarolyHorvath 显然至少从 4.4.7 到 5.2.0。 icc 也可以这样做,但 clang 不会。 $ gcc --version gcc (Debian 5.2.1-21) 5.2.1 20151003。我附加到原帖。 【参考方案1】:

是的,gcc 会在循环的每次迭代中重新加载 s.xs.y,因为 gcc 不知道 &amp;s.z[i] 是否为某些 i 别名部分 S 对象的一部分通过引用传递给 f(S&amp;, size_t) .

使用 gcc 5.2.0,将__restrict__ 应用于S::z 并将s 引用参数应用于f(),即:

struct S 
    double *x;
    double *y;
    double *__restrict__ z;
;

void f(S&__restrict__ s, size_t n) 
    for (int i = 0; i < n; i += 2) 
        __m128d xs = _mm_load_pd(&s.x[i]);
        __m128d ys = _mm_load_pd(&s.y[i]);
        _mm_store_pd(&s.z[i], _mm_mul_pd(xs, ys));
    
    return;

.. 导致 gcc 生成:

__Z1fR1Sm:
LFB518:
    testq   %rsi, %rsi
    je  L1
    movq    (%rdi), %r8
    xorl    %eax, %eax
    movq    8(%rdi), %rcx
    movq    16(%rdi), %rdx
    .align 4,0x90
L4:
    movapd  (%r8,%rax,8), %xmm0
    mulpd   (%rcx,%rax,8), %xmm0
    movaps  %xmm0, (%rdx,%rax,8)
    addq    $2, %rax
    cmpq    %rax, %rsi
    ja  L4
L1:
    ret

使用 Apple Clang 700.1.76,只需要 s 引用上的 __restrict__

__Z1fR1Sm:                              ## @_Z1fR1Sm
    .cfi_startproc
## BB#0:
    pushq   %rbp
Ltmp0:
    .cfi_def_cfa_offset 16
Ltmp1:
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
Ltmp2:
    .cfi_def_cfa_register %rbp
    testq   %rsi, %rsi
    je  LBB0_3
## BB#1:                                ## %.lr.ph
    movq    (%rdi), %rax
    movq    8(%rdi), %rcx
    movq    16(%rdi), %rdx
    xorl    %edi, %edi
    .align  4, 0x90
LBB0_2:                                 ## =>This Inner Loop Header: Depth=1
    movapd  (%rax,%rdi,8), %xmm0
    mulpd   (%rcx,%rdi,8), %xmm0
    movapd  %xmm0, (%rdx,%rdi,8)
    addq    $2, %rdi
    cmpq    %rsi, %rdi
    jb  LBB0_2
LBB0_3:                                 ## %._crit_edge
    popq    %rbp
    retq
    .cfi_endproc

【讨论】:

奇怪的是,-fstrict-aliasing 没有帮助。我可能误解了该选项的作用。 我很确定你是对的,令人困惑的部分是三个额外的负载是从 %rdi 加载地址而不是实际的数组数据。 @DanielTrebbien 谢谢,我理解那部分(我在原始帖子中将其在 cmets 中写入程序集)。问题是要了解为什么涉及混叠;当我们写 s.z[i]=s.x[i]*s.y[i] 时,s.z 可以与 s.x 或 s.y 别名,如果我们稍后在代码中重用 s.z[i] 将需要额外的负载,但就像你说的那样正在重新加载地址。输入指针(指向结构的指针)没有任何别名,即我们没有 f(S&s1, S&s2) 并且不应该重新加载 s1, s2 指向的内容。我可能需要多考虑一下。 @Jester -fstrict-aliasing 我相信告诉 gcc 如果取消引用不同类型的指针,它们将不会引用相同的内存。

以上是关于按引用和按值传递时的 gcc 程序集的主要内容,如果未能解决你的问题,请参考以下文章

Python按值传递参数和按引用传递参数

我是不是正确理解了 C 中的按值传递和按引用传递?

按引用传递然后复制和按值传递在功能上是不是不同?

js按值传递和按引用传递

(转)Java:按值传递和按引用传递详细解说

理解按值传递和按引用传递