C11 中的输出参数或 return 结构?
Output parameter or return struct in C11?
我知道 C++11 具有移动语义,这意味着您可以直接从函数中 return 一个结构而不用担心它被复制(假设是一个简单的结构),而不是编写结构通过输出参数。
C11有这样的吗?还是 returned 结构每次都会被复制?输出参数还是这里的"best practice"吗?
我认为这里有些混乱需要澄清。 C++的语义和实现是不同的。
"Move" 与 C++ 中的 "copy" 的区别只是调用哪个构造函数(或 operator=
)的问题。
结构成员的表示是否被复制的问题是一个完全不同的问题。换句话说,"does the processor have to move these bytes around?" 不是语言语义的一部分。
语义
MyClass func() {
MyClass x;
x.method(...);
...
return x;
}
此 return 使用移动语义(如果可用),但即使在 C++11 之前,return 值优化也是可用的。
我们更喜欢使用移动语义的原因是因为移动对象不会导致深度复制,例如,如果您移动 std::vector<T>
,则不必复制所有 T
。但是,您仍在复制数据!所以,std::move(x)
在语义上是一个移动操作(认为它是使用线性而不是经典逻辑)但它仍然是通过在内存中复制数据来实现的。
除非你的 ABI 让你避免复制。这将我们带到...
实施
当您调用一个 return 是一个大型结构的函数时(术语 "large" 是相对的,它可能只是几个词),大多数 ABI 将调用该结构以通过函数的引用。所以当你写这样的东西时:
MyClass func() { ... }
一旦你在汇编中查看它,它可能看起来更像这样:
void func(MyClass *ptr) { ... }
当然,这是一个简化!指针通常是隐式的。但重要的一点是,我们已经在避免复制,有时。
案例研究
这是一个简单的例子:
struct big {
int x[100];
};
struct big func1(void);
int func2() {
struct big x = func1();
struct big y = func1();
return x.x[0] + y.x[0];
}
当我在 x64 上用 gcc -O2
编译它时,我得到以下汇编输出:
subq 8, %rsp
movq %rsp, %rdi
call func1
leaq 400(%rsp), %rdi
call func1
movl 400(%rsp), %eax
addl (%rsp), %eax
addq 8, %rsp
ret
您可以看到无处被struct big
复制。 func2()
的结果简单地放在堆栈上等待 func1()
,然后堆栈被移动,因此下一个结果被放在其他地方。
但是
在常见的 ABI 中,大型函数结果不会通过多个函数堆栈进行线程化。从上面的 func2()
返回 x
或 y
将导致结构被复制。
但是记住!这与 移动语义 与 复制语义 无关,因为复制的结构数据只是一个实现细节,而不是语言语义。在 C++ 中,使用 std::move()
可能仍会导致结构被复制,只是不会调用复制构造函数。
结论:return从 C 或 C++ 构建大型结构可能会导致复制结构,取决于 ABI 的细节,函数如何已优化,并且有问题的代码。但是,如果结构只有几个字,我不会担心。
我知道 C++11 具有移动语义,这意味着您可以直接从函数中 return 一个结构而不用担心它被复制(假设是一个简单的结构),而不是编写结构通过输出参数。
C11有这样的吗?还是 returned 结构每次都会被复制?输出参数还是这里的"best practice"吗?
我认为这里有些混乱需要澄清。 C++的语义和实现是不同的。
"Move" 与 C++ 中的 "copy" 的区别只是调用哪个构造函数(或
operator=
)的问题。结构成员的表示是否被复制的问题是一个完全不同的问题。换句话说,"does the processor have to move these bytes around?" 不是语言语义的一部分。
语义
MyClass func() {
MyClass x;
x.method(...);
...
return x;
}
此 return 使用移动语义(如果可用),但即使在 C++11 之前,return 值优化也是可用的。
我们更喜欢使用移动语义的原因是因为移动对象不会导致深度复制,例如,如果您移动 std::vector<T>
,则不必复制所有 T
。但是,您仍在复制数据!所以,std::move(x)
在语义上是一个移动操作(认为它是使用线性而不是经典逻辑)但它仍然是通过在内存中复制数据来实现的。
除非你的 ABI 让你避免复制。这将我们带到...
实施
当您调用一个 return 是一个大型结构的函数时(术语 "large" 是相对的,它可能只是几个词),大多数 ABI 将调用该结构以通过函数的引用。所以当你写这样的东西时:
MyClass func() { ... }
一旦你在汇编中查看它,它可能看起来更像这样:
void func(MyClass *ptr) { ... }
当然,这是一个简化!指针通常是隐式的。但重要的一点是,我们已经在避免复制,有时。
案例研究
这是一个简单的例子:
struct big {
int x[100];
};
struct big func1(void);
int func2() {
struct big x = func1();
struct big y = func1();
return x.x[0] + y.x[0];
}
当我在 x64 上用 gcc -O2
编译它时,我得到以下汇编输出:
subq 8, %rsp
movq %rsp, %rdi
call func1
leaq 400(%rsp), %rdi
call func1
movl 400(%rsp), %eax
addl (%rsp), %eax
addq 8, %rsp
ret
您可以看到无处被struct big
复制。 func2()
的结果简单地放在堆栈上等待 func1()
,然后堆栈被移动,因此下一个结果被放在其他地方。
但是
在常见的 ABI 中,大型函数结果不会通过多个函数堆栈进行线程化。从上面的
func2()
返回x
或y
将导致结构被复制。但是记住!这与 移动语义 与 复制语义 无关,因为复制的结构数据只是一个实现细节,而不是语言语义。在 C++ 中,使用
std::move()
可能仍会导致结构被复制,只是不会调用复制构造函数。
结论:return从 C 或 C++ 构建大型结构可能会导致复制结构,取决于 ABI 的细节,函数如何已优化,并且有问题的代码。但是,如果结构只有几个字,我不会担心。