我应该何时通过值传递或返回结构?

Kai*_*hen 42 c shallow-copy

结构可以通过值传递/返回,也可以通过C中的引用(通过指针)传递/返回.

普遍的共识似乎是,在大多数情况下,前者可以适用于没有惩罚的小结构.请参阅是否存在直接返回结构的良好做法?是否有任何缺点由值用C传递结构,而不是传递指针?

从速度和清晰度的角度来看,避免取消引用可能是有益的.但什么算?我想我们都同意这是一个小结构:

struct Point { int x, y; };
Run Code Online (Sandbox Code Playgroud)

我们可以通过相对有罪不罚的价值来传递:

struct Point sum(struct Point a, struct Point b) {
  return struct Point { .x = a.x + b.x, .y = a.y + b.y };
}
Run Code Online (Sandbox Code Playgroud)

那个Linux task_struct是一个大型结构:

https://github.com/torvalds/linux/blob/b953c0d234bc72e8489d3bf51a276c5c4ec85345/include/linux/sched.h#L1292-1727

我们希望不惜一切代价避免使用堆栈(特别是那些8K内核模式堆栈!).但是什么是中等的?我假设小于寄存器的结构是好的.但那些呢?

typedef struct _mx_node_t mx_node_t;
typedef struct _mx_edge_t mx_edge_t;

struct _mx_edge_t {
  char symbol;
  size_t next;
};

struct _mx_node_t {
  size_t id;
  mx_edge_t edge[2];
  int action;
};
Run Code Online (Sandbox Code Playgroud)

确定结构是否足够小以确定是否可以安全地通过值传递(缺少某些深度递归等情有可原的情况),最好的经验法则是什么?

最后请不要告诉我需要个人资料.当我太懒的时候,我要求使用启发式方法/它不值得进一步调查.

编辑:到目前为止,根据答案我有两个后续问题:

  1. 如果结构实际上小于指向它的指针怎么办?

  2. 如果浅拷贝是期望的行为怎么办(被调用的函数无论如何都会执行浅拷贝)?

编辑:不知道为什么这被标记为可能重复,因为我实际上链接了我的问题中的其他问题.我要求澄清什么构成一个结构,并且我很清楚大多数时间结构应该通过引用传递.

use*_*249 24

我的经验,近40年的实时嵌入,使用C持续20年; 是最好的方法是传递指针.

在任何一种情况下,都需要加载结构的地址,然后需要计算感兴趣的字段的偏移量......

传递整个结构时,如果没有通过引用传递,那么

  1. 它没有放在堆栈上
  2. 它被复制,通常是通过对memcpy()的隐藏调用
  3. 它被复制到现在"保留"的内存部分,并且不可用于程序的任何其他部分.

当按值返回结构时,存在类似的考虑因素.

但是,可以完全保存在工作寄存器中的"小"结构在这些寄存器中传递,特别是如果在编译语句中使用了某些优化级别.

被认为是"小"的细节取决于编译器和底层硬件架构.

  • 对于当前的32位CPU /平台而言,第1点是明显错误的,而现代8/16位MCU(例如MSP430)则错误.同样的3,出于类似的原因.如果没有这个,默认情况下该函数不会是线程安全的,这使得这些函数无法用于线程. (8认同)
  • 恕我直言,另一个原因是它避免了混淆。如果您总是传递/返回指针,那么您永远不必怀疑代码中的任何特定引用是指针还是副本。 (2认同)
  • 再次阅读你的答案!你写的对象是**不是**放在堆栈上.让部分完全不使用堆栈的实现(C不要求堆栈),现代架构**确实将它放在堆栈上.他们还应该在哪里?使用(隐藏)全局变量是无意义的,而不是线程安全的.你写的是80年代和90年代的一部分实践,但最终不是像OP所指的现代实现和架构. (2认同)
  • 这个答案没有多大意义。完全荒谬的是你的“它被复制到现在‘保留’并且不可用于程序的任何其他部分的内存部分”的声明。好吧,我非常希望我的结构所驻留的内存在其生命周期内不可用。然后,正如奥拉夫指出的那样,它当然是在堆栈上传递的,否则怎么办(除非它适合寄存器)?是的,除了(某种)mempcpy() 之外还有什么其他方法呢?不确定你所说的“隐藏”是什么意思,除了你不必明确地写下来,谢天谢地。 (2认同)

LTh*_*ode 17

在小型嵌入式架构(8/16位)上 - 总是通过指针传递,因为非平凡的结构不适合这种微小的寄存器,并且这些机器通常也是寄存器缺乏的.

在类似PC的架构(32位和64位处理器)上 - 按值传递结构是可以提供的,sizeof(mystruct_t) <= 2*sizeof(mystruct_t*)并且该函数没有很多(通常超过3个机器字值)其他参数.在这些情况下,典型的优化编译器将在寄存器或寄存器对中传递/返回结构.然而,在x86-32上,由于x86-32编译器必须处理的非常大的寄存压力,这个建议应该带有大量的盐 - 由于减少了寄存器溢出和填充,传递指针可能仍然更快.

另一方面,在PC-like上按值返回结构遵循相同的规则,除了当指针返回结构时,要填充的结构也应该通过指针传入 - 否则,被调用者和调用者不得不就如何管理该结构的内存达成一致.

  • @Claudiu-这基本上是一种说法,“它占用的内存不超过两个机器字” (3认同)
  • 我可能缺少明显的东西,但是为什么“ sizeof(mystruct_t)&lt;= 2 * sizeof(mystruct_t *)”呢? (2认同)

ala*_*ain 8

由于问题的论证传递部分已经回答,我将重点关注回归部分.

做IMO的最好的事情是根本不返回结构或指向结构的指针,而是将指向"结构结构"的指针传递给函数.

void sum(struct Point* result, struct Point* a, struct Point* b);
Run Code Online (Sandbox Code Playgroud)

这具有以下优点:

  • result结构能活多久在栈或堆,在调用者的自由裁量权.
  • 没有所有权问题,因为很明显调用者负责分配和释放结果结构.
  • 结构甚至可以比需要的更长,或嵌入更大的结构中.


too*_*ite 7

如何将结构传递给函数或从函数传递结构取决于应用程序二进制接口(ABI)和程序调用标准(PCS,有时包含在ABI中),用于您的目标平台(CPU/OS,对于某些平台,可能有多个一个版本).

如果 PCS实际上允许在寄存器中传递结构,这不仅取决于它的大小,还取决于它在参数列表中的位置和前面参数的类型.例如,ARM-PCS(AAPCS)将参数打包到前4个寄存器中,直到它们已满并将更多数据传递到堆栈,即使这意味着参数被拆分(所有简化,如果感兴趣:文档可从ARM免费下载) ).

对于返回的结构,如果它们不通过寄存器传递,则大多数PCS由调用者分配堆栈上的空间,并将指向结构的指针传递给被调用者(隐式变体).这与调用者中的局部变量相同,并且显式地传递指针 - 对于被调用者.但是,对于隐式变体,结果必须复制到另一个结构,因为无法获得对隐式分配的结构的引用.

某些PCS可能对参数结构执行相同的操作,其他PCS只使用与标量相同的机制.无论如何,你推迟这样的优化,直到你真的知道你需要它们.另请阅读目标平台的PCS.请记住,您的代码可能在不同的平台上执行得更糟.

注意:现代PCS不使用通过全局临时结构传递结构,因为它不是线程安全的.但是,对于某些小型微控制器架构,这可能会有所不同.大多数情况下,如果他们只有一个小堆栈(S08)或限制功能(PIC).但是对于这些,大多数时候结构都不会在寄存器中传递,强烈建议使用pass-by-pointer.

如果只是为了原始的不变性:通过a const mystruct *ptr.除非你抛弃它const,否则至少在写入结构时会发出警告.指针本身也可以是常量:const mystruct * const ptr.

所以:没有经验法则; 这取决于太多因素.

  • 如果没有对每个ABI /调用约定有任何更深入的了解,我认为对于大多数处理器/ ABI来说,经验法则是"如果结构大小小于或等于CPU的数据总线,那么通过值很好".8位PIC为1字节,32位ARM为4字节,64位Intel PC为8字节,依此类推.但是,如果涉及可移植性,最好的经验法则可能总是通过引用传递. (2认同)

Pan*_*rei 5

实际上最好的经验法则是,通过引用和值将结构作为参数传递给函数,就是避免按值传递它.风险几乎总是超过收益.

为了完整起见,我将指出当通过值传递/返回结构时,会发生一些事情:

  1. 所有结构的成员都被复制到堆栈中
  2. 如果按值返回结构,则所有成员都将从函数的堆栈内存复制到新的内存位置.
  3. 该操作容易出错 - 如果结构的成员是指针,则常见的错误是假设您可以安全地按值传递参数,因为您正在操作指针 - 这可能导致很难发现错误.
  4. 如果你的函数修改了输入参数的值,你的输入是结构变量,按值传递,你必须记住ALENDS按值返回一个struct变量(我已经看过很多次).这意味着复制结构成员的时间加倍.

现在,在结构的大小方面达到足够小的意义 - 这样它"值得"通过值传递它,这取决于一些事情:

  1. 调用约定:编译器在调用该函数时自动保存在堆栈中的内容(通常是几个寄存器的内容).如果您的结构成员可以利用这种机制复制到堆栈上,那么就不会受到惩罚.
  2. 结构成员的数据类型:如果你的机器的寄存器是16位,你的结构成员数据类型是64位,它显然不适合一个寄存器,因此只需要为一个副本执行多个操作.
  3. 你的机器实际拥有的寄存器数量:假设你的结构只有一个成员,一个字符(8位).当按值或通过引用传递参数时(理论上),这应该导致相同的开销.但是还有另外一个危险.如果您的体系结构具有单独的数据和地址寄存器,则通过值传递的参数将占用一个数据寄存器,通过引用传递的参数将占用一个地址寄存器.按值传递参数会对数据寄存器施加压力,这些数据寄存器通常比地址寄存器使用得多.这可能会导致堆栈溢出.

底线 - 很难说什么时候按值传递结构是可以的.只是不这样做更安全:)