在C和Go中,是否可以传递指针而不是返回新的变量?

huangapple go评论80阅读模式
英文:

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

问题

为什么在C和Go中,传递指向变量的指针并修改它,而不是返回一个具有新值的新变量?

在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;
}

在Go中:

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)
}

在这两种语言中,传递指向变量的指针并修改它的方式是一种常见的约定。这种做法有以下几个原因:

  1. 性能优势:通过传递指针并在原始变量上进行修改,可以避免创建新的变量和内存分配操作,从而提高性能。

  2. 节省内存:如果函数需要修改大型结构体或数组等复杂数据类型,通过传递指针可以避免复制整个数据结构,从而节省内存。

  3. 直接修改原始数据:通过传递指针并在原始变量上进行修改,可以直接修改原始数据,而不需要返回新的变量。这在某些情况下可以简化代码逻辑。

需要注意的是,这种做法也存在一些限制和潜在的问题:

  1. 指针安全性:使用指针修改变量时,需要确保指针指向的内存是有效的,并且在修改期间没有被其他代码修改。否则可能导致潜在的错误和不确定的行为。

  2. 可读性和维护性:传递指针并在原始变量上进行修改可能会增加代码的复杂性,降低可读性和维护性。在设计和编写代码时,需要权衡这些因素。

综上所述,传递指向变量的指针并修改它的方式在C和Go中被广泛采用,主要是为了性能优势和节省内存。但在使用时需要注意指针的安全性和代码的可读性。

英文:

Why is it convention in C and Go to pass a pointer to a variable and change it rather return a new variable with the value?

In C:

#include &lt;stdio.h&gt;

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

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

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

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

In Go:

package main

import &quot;fmt&quot;

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

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

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

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

It there any performance benefits or restrictions in doing one or the other?

答案1

得分: 1

首先,我对Go语言了解不够,无法对其进行评判,但是下面的答案适用于C语言。

如果你只是处理像int这样的基本类型,那么我认为这两种技术之间没有性能差异。

当涉及到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

没有优化

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

当通过指针间接引用修改变量时,除了根据传入的指针写入所需的值到内存地址外,没有其他操作。

.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

稍微优化

然而,现在大多数编译器都可以理解你在这里尝试做什么,并且会在这两种技术之间平衡性能。

如果你想要绝对地追求性能,传递指针仍然比返回变量稍微快一些,但差异只有几个时钟周期。

在从函数返回变量时,你仍然至少需要设置返回值的地址。

        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
英文:

First off, I don't know enough about Go to give a judgement on it, but the answer will apply in the case of C.

If you're just working on primitive types like ints, then I'd say there is no performance difference between the two techniques.

When structs come into play, there is a very slight advantage of modifying a variable via pointer (based purely on what you're doing in your code)

#include &lt;stdio.h&gt;

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

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

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

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

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

GCC x86-64 11.2

With No Optimizations

Returning a struct variable through the function's return is slower because the variable has to be "built" by assigning the desired values, after which, the variable is copied to the return value.

When you're modifying a variable by pointer indirection, there is nothing to do except write the desired values to the memory addresses (based off the pointer you passed in)

.LC0:
        .string &quot;Chad&quot;
.LC1:
        .string &quot;123 Someplace St.&quot;
.LC2:
        .string &quot;Software Engineer&quot;
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

With Slight Optimization

However, most compilers today can figure out what you're trying to do here, and will equalize the performance between the two techniques.

If you want to be absolutely stingy, passing pointers is still slightly faster by a few clock cycles at best.

In returning a variable from the function, you still have to at least set the address of the return value.

        mov     rax, rdi

But in passing the pointer, not even this is done.

But other than that, the two techniques have no performance difference.

.LC0:
        .string &quot;Chad&quot;
.LC1:
        .string &quot;123 Someplace St.&quot;
.LC2:
        .string &quot;Software Engineer&quot;
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

答案2

得分: 0

我认为对于你的问题的简短回答是(至少对于C语言而言,我对GO的内部不熟悉),C函数是按值传递的,通常也是按值返回的,因此数据对象必须被复制,人们担心所有的复制会影响性能。对于大型对象或者深度复杂的对象(包含指向其他内容的指针),将被复制的值作为指针更高效或更合理,这样函数就可以在不需要复制的情况下对数据进行操作。

话虽如此,现代编译器在确定参数数据是否适合寄存器或者高效地复制返回的结构方面非常聪明。

总之,对于现代C代码,根据你的应用选择最好的方式或者最清晰的方式。至少在开始阶段,避免过早进行优化,以免影响可读性。

此外,编译器资源浏览器(https://godbolt.org/)是你的朋友,如果你想研究不同风格的效果,特别是在优化方面。

英文:

I think the short answer to you question (At least for C, I am not familiar with GO internals) is that C functions are pass by value and generally also return by value so data objects must be copied and people worried about the performance of all the copying. For large objects or objects that are complex in their depth (containing pointers to other stuff) it is often just more efficient or logical for the value being copied to be a pointer so the function can "operate" on the data without needing to copy it.
That being said, modern compilers are pretty smart about figuring out stuff like whether the parameter data will fit in registers or efficiently copying returned structures.
Bottom line is for modern C code do what seems best for your application or what is clearest to you. Avoid premature optimization if it detracts from readability at least in the beginning.
Also Compiler Explorer (https://godbolt.org/) is your friend if you want to examine the effect of different styles, especially in light of optimization.

huangapple
  • 本文由 发表于 2021年12月5日 21:41:06
  • 转载请务必保留本文链接:https://go.coder-hub.com/70234885.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定