gpt4 book ai didi

c - 结构和 union :从性能的角度来看哪个更好?通过值或指针传递参数?

转载 作者:行者123 更新时间:2023-12-01 11:11:22 25 4
gpt4 key购买 nike

这可能是一个愚蠢的问题,但是每次我想“优化”大量参数(例如结构)传递给只读取它们的函数时,都会使我有些slightly跷。我在传递指针之间犹豫不决:

struct Foo
{
int x;
int y;
int z;
} Foo;

int sum(struct Foo *foo_struct)
{
return foo_struct->x + foo_struct->y + foo_struct->z;
}

或一个常数:
struct Foo
{
int x;
int y;
int z;
} Foo;

int sum(const struct Foo foo_struct)
{
return foo_struct.x + foo_struct.y + foo_struct.z;
}

指针的目的不是复制数据,而只是发送其地址,这几乎不花钱。

对于常量,尽管我不知道如何优化常量遍,但它可能在编译器或优化级别之间有所不同。如果是这样,那么编译器可能会比我做的更好。

仅从性能角度来看(即使在我的示例中可以忽略不计),首选的处理方式是什么?

最佳答案

结构与数组很像,是数据的容器。每次使用容器时,都会将其数据布置在连续的内存块中。容器本身由其起始地址标识,并且每次使用容器进行操作时,您的程序都需要通过专用指令进行低级指针算术运算,以便应用偏移量从起始地址到达所需字段(或数组中的元素)。编译器唯一需要了解的用于结构的东西是(大致):

  • 内存中的起始地址。
  • 每个字段的偏移量。
  • 每个字段的大小。

  • 如果结构作为指针传递或不传递给,则编译器可以以相同的方式优化在结构上工作的代码,稍后我们将介绍如何。不过,不同之处在于将结构传递给每个函数的方式。

    首先让我澄清一件事: const限定符对于理解将结构作为指针或按值传递之间的区别没有用。它只是告诉编译器,在函数内部,参数本身的值不会被修改。作为值或作为指针传递之间的性能差异通常不受 const影响。 const关键字仅对其他类型的优化有用,而对这一优化则无效。

    这两个签名之间的主要区别是:
    void first(const struct mystruct x);
    void second(struct mystruct *x);

    是第一个函数期望 整个结构作为参数传递,因此这意味着在调用函数之前将整个结构复制到堆栈上。但是,第二个函数仅需要指向该结构的指针,因此可以将 作为单个值传递给堆栈上的参数,或者像在x86-64中通常那样在寄存器中传递。

    现在,为了更好地了解会发生什么,让我们分析以下程序:
    #include <stdio.h>

    struct mystruct {
    unsigned a, b, c, d, e, f, g, h, i, j, k;
    };

    unsigned long __attribute__ ((noinline)) first(const struct mystruct x) {
    unsigned long total = x.a;
    total += x.b;
    total += x.c;
    total += x.d;
    total += x.e;
    total += x.f;
    total += x.g;
    total += x.h;
    total += x.i;
    total += x.j;
    total += x.k;

    return total;
    }

    unsigned long __attribute__ ((noinline)) second(struct mystruct *x) {
    unsigned long total = x->a;
    total += x->b;
    total += x->c;
    total += x->d;
    total += x->e;
    total += x->f;
    total += x->g;
    total += x->h;
    total += x->i;
    total += x->j;
    total += x->k;

    return total;
    }

    int main (void) {
    struct mystruct x = {0};
    scanf("%u", &x.a);

    unsigned long v = first(x);
    printf("%lu\n", v);

    v = second(&x);
    printf("%lu\n", v);

    return 0;
    }
    __attribute__ ((noinline))只是为了避免自动内联函数,出于测试目的,该函数非常简单,因此很可能会使用 -O3内联。

    现在让我们借助 objdump编译和反汇编结果。

    没有优化

    让我们先进行编译而不进行优化,看看会发生什么:
  • 这是main()调用first()的方式:

     86a:   48 89 e0                mov    rax,rsp
    86d: 48 8b 55 c0 mov rdx,QWORD PTR [rbp-0x40]
    871: 48 89 10 mov QWORD PTR [rax],rdx
    874: 48 8b 55 c8 mov rdx,QWORD PTR [rbp-0x38]
    878: 48 89 50 08 mov QWORD PTR [rax+0x8],rdx
    87c: 48 8b 55 d0 mov rdx,QWORD PTR [rbp-0x30]
    880: 48 89 50 10 mov QWORD PTR [rax+0x10],rdx
    884: 48 8b 55 d8 mov rdx,QWORD PTR [rbp-0x28]
    888: 48 89 50 18 mov QWORD PTR [rax+0x18],rdx
    88c: 48 8b 55 e0 mov rdx,QWORD PTR [rbp-0x20]
    890: 48 89 50 20 mov QWORD PTR [rax+0x20],rdx
    894: 8b 55 e8 mov edx,DWORD PTR [rbp-0x18]
    897: 89 50 28 mov DWORD PTR [rax+0x28],edx
    89a: e8 81 fe ff ff call 720 <first>

    这是函数本身:

    0000000000000720 <first>:
    720: 55 push rbp
    721: 48 89 e5 mov rbp,rsp
    724: 8b 45 10 mov eax,DWORD PTR [rbp+0x10]
    727: 89 c0 mov eax,eax
    729: 48 89 45 f8 mov QWORD PTR [rbp-0x8],rax
    72d: 8b 45 14 mov eax,DWORD PTR [rbp+0x14]
    730: 89 c0 mov eax,eax
    732: 48 01 45 f8 add QWORD PTR [rbp-0x8],rax
    736: 8b 45 18 mov eax,DWORD PTR [rbp+0x18]
    739: 89 c0 mov eax,eax
    ... same stuff happening over and over ...
    783: 48 01 45 f8 add QWORD PTR [rbp-0x8],rax
    787: 48 8b 45 f8 mov rax,QWORD PTR [rbp-0x8]
    78b: 5d pop rbp
    78c: c3 ret

    很明显,在调用函数之前,整个结构已被复制到堆栈上。

    然后,该函数将结构中的每个值每次都查看堆栈中结构中包含的每个值(DWORD PTR [rbp + offset])。
  • 这是main()调用second()的方式:

     8bf:   48 8d 45 c0             lea    rax,[rbp-0x40]
    8c3: 48 89 c7 mov rdi,rax
    8c6: e8 c2 fe ff ff call 78d <second>

    这是函数本身:

    000000000000078d <second>:
    78d: 55 push rbp
    78e: 48 89 e5 mov rbp,rsp
    791: 48 89 7d e8 mov QWORD PTR [rbp-0x18],rdi
    795: 48 8b 45 e8 mov rax,QWORD PTR [rbp-0x18]
    799: 8b 00 mov eax,DWORD PTR [rax]
    79b: 89 c0 mov eax,eax
    79d: 48 89 45 f8 mov QWORD PTR [rbp-0x8],rax
    7a1: 48 8b 45 e8 mov rax,QWORD PTR [rbp-0x18]
    7a5: 8b 40 04 mov eax,DWORD PTR [rax+0x4]
    7a8: 89 c0 mov eax,eax
    ... same stuff happening over and over ...
    81f: 48 01 45 f8 add QWORD PTR [rbp-0x8],rax
    823: 48 8b 45 f8 mov rax,QWORD PTR [rbp-0x8]
    827: 5d pop rbp
    828: c3 ret

    您可以看到参数是作为指针传递的,而不是被复制到堆栈上的,这只是两个非常简单的指令(lea + mov)。但是,由于现在该函数必须使用->运算符与指针一起使用,因此我们看到,每次需要访问结构中的值时,都需要将内存解除引用两次,而不是一次(第一个将指针指向)。堆栈中的结构,然后获取结构中指定偏移量处的值)。

  • 似乎这两个函数之间并没有真正的区别,因为在第一种情况下,仍然需要线性数量的指令(就结构成员而言是线性的)将结构加载到堆栈上,时间在第二种情况下。

    虽然我们在谈论优化,但是不优化代码没有任何意义。让我们看看如果这样做会发生什么。

    进行优化

    实际上,当使用struct时,我们并不真正在意它在内存中的位置(堆栈,堆,数据段等)。只要我们知道它从哪里开始,就可以归结为应用相同的简单指针算法来访问字段。无论结构位于何处或是否已动态分配,都始终需要这样做。

    如果我们使用-O3优化上面的代码,我们现在将看到以下内容:
  • 这是main()调用first()的方式:

     61a:   48 83 ec 30             sub    rsp,0x30
    61e: 48 8b 44 24 30 mov rax,QWORD PTR [rsp+0x30]
    623: 48 89 04 24 mov QWORD PTR [rsp],rax
    627: 48 8b 44 24 38 mov rax,QWORD PTR [rsp+0x38]
    62c: 48 89 44 24 08 mov QWORD PTR [rsp+0x8],rax
    631: 48 8b 44 24 40 mov rax,QWORD PTR [rsp+0x40]
    636: 48 89 44 24 10 mov QWORD PTR [rsp+0x10],rax
    63b: 48 8b 44 24 48 mov rax,QWORD PTR [rsp+0x48]
    640: 48 89 44 24 18 mov QWORD PTR [rsp+0x18],rax
    645: 48 8b 44 24 50 mov rax,QWORD PTR [rsp+0x50]
    64a: 48 89 44 24 20 mov QWORD PTR [rsp+0x20],rax
    64f: 8b 44 24 58 mov eax,DWORD PTR [rsp+0x58]
    653: 89 44 24 28 mov DWORD PTR [rsp+0x28],eax
    657: e8 74 01 00 00 call 7d0 <first>

    这是函数本身:

    00000000000007d0 <first>:
    7d0: 8b 44 24 0c mov eax,DWORD PTR [rsp+0xc]
    7d4: 8b 54 24 08 mov edx,DWORD PTR [rsp+0x8]
    7d8: 48 01 c2 add rdx,rax
    7db: 8b 44 24 10 mov eax,DWORD PTR [rsp+0x10]
    7df: 48 01 d0 add rax,rdx
    7e2: 8b 54 24 14 mov edx,DWORD PTR [rsp+0x14]
    7e6: 48 01 d0 add rax,rdx
    7e9: 8b 54 24 18 mov edx,DWORD PTR [rsp+0x18]
    7ed: 48 01 c2 add rdx,rax
    7f0: 8b 44 24 1c mov eax,DWORD PTR [rsp+0x1c]
    7f4: 48 01 c2 add rdx,rax
    7f7: 8b 44 24 20 mov eax,DWORD PTR [rsp+0x20]
    7fb: 48 01 d0 add rax,rdx
    7fe: 8b 54 24 24 mov edx,DWORD PTR [rsp+0x24]
    802: 48 01 d0 add rax,rdx
    805: 8b 54 24 28 mov edx,DWORD PTR [rsp+0x28]
    809: 48 01 c2 add rdx,rax
    80c: 8b 44 24 2c mov eax,DWORD PTR [rsp+0x2c]
    810: 48 01 c2 add rdx,rax
    813: 8b 44 24 30 mov eax,DWORD PTR [rsp+0x30]
    817: 48 01 d0 add rax,rdx
    81a: c3 ret
  • 这是main()调用second()的方式:

     671:   48 89 df                mov    rdi,rbx
    674: e8 a7 01 00 00 call 820 <second>

    这是函数本身:

    0000000000000820 <second>:
    820: 8b 47 04 mov eax,DWORD PTR [rdi+0x4]
    823: 8b 17 mov edx,DWORD PTR [rdi]
    825: 48 01 c2 add rdx,rax
    828: 8b 47 08 mov eax,DWORD PTR [rdi+0x8]
    82b: 48 01 d0 add rax,rdx
    82e: 8b 57 0c mov edx,DWORD PTR [rdi+0xc]
    831: 48 01 d0 add rax,rdx
    834: 8b 57 10 mov edx,DWORD PTR [rdi+0x10]
    837: 48 01 c2 add rdx,rax
    83a: 8b 47 14 mov eax,DWORD PTR [rdi+0x14]
    83d: 48 01 c2 add rdx,rax
    840: 8b 47 18 mov eax,DWORD PTR [rdi+0x18]
    843: 48 01 d0 add rax,rdx
    846: 8b 57 1c mov edx,DWORD PTR [rdi+0x1c]
    849: 48 01 d0 add rax,rdx
    84c: 8b 57 20 mov edx,DWORD PTR [rdi+0x20]
    84f: 48 01 c2 add rdx,rax
    852: 8b 47 24 mov eax,DWORD PTR [rdi+0x24]
    855: 48 01 c2 add rdx,rax
    858: 8b 47 28 mov eax,DWORD PTR [rdi+0x28]
    85b: 48 01 d0 add rax,rdx
    85e: c3 ret

  • 现在应该清楚哪个代码更好。编译器成功地确定,在两种情况下,它所需要做的就是知道结构的开始位置,然后可以使用相同的简单数学来确定每个字段的位置。地址是在堆栈上还是在其他地方,都没有关系。

    实际上,在first()情况下,我们看到所有字段都通过[rsp + offset]访问,这意味着堆栈本身上的某个地址(rsp)用于计算字段的位置,而在second()情况下,我们看到[rdi + offset],这意味着地址而是使用作为参数传递(在rdi中)。尽管偏移量仍然相同。

    那么这两个函数之间有什么区别?就功能代码本身而言,基本上没有。在参数传递方面,first()函数仍需要按值传递结构,因此即使启用了优化,整个结构仍需要复制到堆栈上,因此我们可以看到 first()函数较重并添加了调用方中有很多代码。

    如前所述,如果结构作为指针传递或不传递,编译器可以以相同的方式优化在结构上工作的代码。但是,正如我们刚刚看到的,传递结构的方式在调用方中有很大的不同。

    有人可能会争辩说,const函数的first()限定符可能会给编译器敲响钟声,并使其理解,实际上并不需要复制堆栈上的数据,并且调用者只需传递一个指针即可。但是,编译器应严格遵守ABI针对给定签名所规定的调用约定,而不是尽力优化代码。毕竟,在这种情况下,并不是真正的编译器错误,而是程序员的错误。

    因此,回答您的问题:

    仅从性能角度来看(即使在我的示例中可以忽略不计),首选的处理方式是什么?

    首选的方法肯定是传递一个指针,而不是struct本身。

    关于c - 结构和 union :从性能的角度来看哪个更好?通过值或指针传递参数?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/59976295/

    25 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com