在 C 和 Go 中传递指针而不是 return 新变量?

Pass a pointer instead of return new variable in C and Go?

为什么 C 和 Go 中的约定是传递一个指向变量的指针并更改它而不是 return 一个具有值的新变量?

在 C:

#include <stdio.h>

int getValueUsingReturn() {
    int value = 42;
    return value;
}

void getValueUsingPointer(int* value ) {
    *value = 42;
}

int main(void) {
  int valueUsingReturn = getValueUsingReturn();
  printf("%d\n", valueUsingReturn);

  int valueUsingPointer;
  getValueUsingPointer(&valueUsingPointer);
  printf("%d\n", valueUsingPointer);
  return 0;
}

围棋:

package main

import "fmt"

func getValueUsingReturn() int {
    value := 42
    return value
}

func getValueUsingPointer(value *int) {
    *value = 42
}

func main() {
    valueUsingReturn := getValueUsingReturn()
    fmt.Printf("%d\n", valueUsingReturn)

    var valueUsingPointer int
    getValueUsingPointer(&valueUsingPointer)
    fmt.Printf("%d\n", valueUsingPointer)
}

做一个或另一个有任何性能优势或限制吗?

首先,我对Go的了解还不够多,无法给出判断,但答案适用于C的情况。

如果您只是处理像 ints 这样的基本类型,那么我会说这两种技术之间没有性能差异。

struct 发挥作用时,通过指针修改变量有 非常轻微的 优势(完全基于您在代码中所做的事情)

#include <stdio.h>

struct Person {
    int age;
    const char *name;
    const char *address;
    const char *occupation;
};

struct Person getReturnedPerson() {
    struct Person thePerson = {26, "Chad", "123 Someplace St.", "Software Engineer"};
    return thePerson;
}

void changeExistingPerson(struct Person *thePerson) {
    thePerson->age = 26;
    thePerson->name = "Chad";
    thePerson->address = "123 Someplace St.";
    thePerson->occupation = "Software Engineer";
}

int main(void) {
  struct Person someGuy = getReturnedPerson();
  

  struct Person theSameDude;
  changeExistingPerson(&theSameDude);
  
  
  return 0;
}

GCC x86-64 11.2

没有优化

通过函数的 return 返回一个 struct 变量比较慢,因为必须通过分配所需的值来“构建”变量,然后将变量复制到 return值。

当您通过指针间接修改变量时,除了将所需的值写入内存地址(基于您传入的指针)外,没有什么可做的

.LC0:
        .string "Chad"
.LC1:
        .string "123 Someplace St."
.LC2:
        .string "Software Engineer"
getReturnedPerson:
        push    rbp
        mov     rbp, rsp
        mov     QWORD PTR [rbp-40], rdi
        mov     DWORD PTR [rbp-32], 26
        mov     QWORD PTR [rbp-24], OFFSET FLAT:.LC0
        mov     QWORD PTR [rbp-16], OFFSET FLAT:.LC1
        mov     QWORD PTR [rbp-8], OFFSET FLAT:.LC2
        mov     rcx, QWORD PTR [rbp-40]
        mov     rax, QWORD PTR [rbp-32]
        mov     rdx, QWORD PTR [rbp-24]
        mov     QWORD PTR [rcx], rax
        mov     QWORD PTR [rcx+8], rdx
        mov     rax, QWORD PTR [rbp-16]
        mov     rdx, QWORD PTR [rbp-8]
        mov     QWORD PTR [rcx+16], rax
        mov     QWORD PTR [rcx+24], rdx
        mov     rax, QWORD PTR [rbp-40]
        pop     rbp
        ret
changeExistingPerson:
        push    rbp
        mov     rbp, rsp
        mov     QWORD PTR [rbp-8], rdi
        mov     rax, QWORD PTR [rbp-8]
        mov     DWORD PTR [rax], 26
        mov     rax, QWORD PTR [rbp-8]
        mov     QWORD PTR [rax+8], OFFSET FLAT:.LC0
        mov     rax, QWORD PTR [rbp-8]
        mov     QWORD PTR [rax+16], OFFSET FLAT:.LC1
        mov     rax, QWORD PTR [rbp-8]
        mov     QWORD PTR [rax+24], OFFSET FLAT:.LC2
        nop
        pop     rbp
        ret
main:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 64
        lea     rax, [rbp-32]
        mov     rdi, rax
        mov     eax, 0
        call    getReturnedPerson
        lea     rax, [rbp-64]
        mov     rdi, rax
        call    changeExistingPerson
        mov     eax, 0
        leave
        ret

稍作优化

但是,当今的大多数编译器都可以弄清楚您在这里要做什么,并且会均衡这两种技术之间的性能。

如果你想绝对吝啬,传递指针最多还是稍微快几个时钟周期。

在 return 从函数中获取变量时,您仍然必须至少设置 return 值的地址。

        mov     rax, rdi

但是在传递指针时,连这个都没有做到。

但除此之外,这两种技术没有性能差异。

.LC0:
        .string "Chad"
.LC1:
        .string "123 Someplace St."
.LC2:
        .string "Software Engineer"
getReturnedPerson:
        mov     rax, rdi
        mov     DWORD PTR [rdi], 26
        mov     QWORD PTR [rdi+8], OFFSET FLAT:.LC0
        mov     QWORD PTR [rdi+16], OFFSET FLAT:.LC1
        mov     QWORD PTR [rdi+24], OFFSET FLAT:.LC2
        ret
changeExistingPerson:
        mov     DWORD PTR [rdi], 26
        mov     QWORD PTR [rdi+8], OFFSET FLAT:.LC0
        mov     QWORD PTR [rdi+16], OFFSET FLAT:.LC1
        mov     QWORD PTR [rdi+24], OFFSET FLAT:.LC2
        ret
main:
        mov     eax, 0
        ret

我认为对你的问题的简短回答(至少对于 C,我不熟悉 GO 的内部结构)是 C 函数是按值传递的,而且通常 return 按值传递,所以数据对象必须是复制,人们担心所有复制的性能。对于大型对象或深度复杂的对象(包含指向其他内容的指针),将被复制的值作为指针通常更有效或更合乎逻辑,因此函数可以在数据上“操作”而无需复制它. 话虽这么说,现代编译器在弄清楚诸如参数数据是否适合寄存器或有效复制 returned 结构等问题时非常聪明。 底线是现代 C 代码做对您的应用程序最合适的事情或您最清楚的事情。如果至少在开始时会降低可读性,请避免过早优化。 如果您想检查不同样式的效果,尤其是在优化方面,Compiler Explorer (https://godbolt.org/) 也是您的好帮手。