在 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的情况。
如果您只是处理像 int
s 这样的基本类型,那么我会说这两种技术之间没有性能差异。
当 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/) 也是您的好帮手。
为什么 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的情况。
如果您只是处理像 int
s 这样的基本类型,那么我会说这两种技术之间没有性能差异。
当 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/) 也是您的好帮手。