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

时间:2015-06-22 13:05:21

标签: c shallow-copy

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

普遍的共识似乎是,在大多数情况下,前者可以适用于没有惩罚的小结构。请参阅Is there any case for which returning a structure directly is good practice?Are there any downsides to passing structs by value in C, rather than passing a pointer?

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

struct Point { int x, y; };

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

struct Point sum(struct Point a, struct Point b) {
  return struct Point { .x = a.x + b.x, .y = a.y + b.y };
}

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

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

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

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

  1. 如果结构实际上更小而不是指向它的指针怎么办?

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

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

8 个答案:

答案 0 :(得分:22)

我的经验,近40年的实时嵌入,最后20个使用C;是最好的方法是传递一个指针。

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

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

  1. 它没有放在堆栈上
  2. 通常通过对memcpy()
  3. 的隐藏调用来复制它
  4. 将其复制到现在“保留”的内存部分。 并且对程序的任何其他部分都不可用。
  5. 当按值返回结构时存在类似的注意事项。

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

    所考虑的细节'小' 依赖于编译器和 底层硬件架构。

答案 1 :(得分:12)

在小型嵌入式架构(8/16位)上 - 始终通过指针传递,因为非平凡的结构不适合这种微小的寄存器,而且这些机器通常缺少寄存器同样。

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

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

答案 2 :(得分:6)

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

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

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

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

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

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

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

答案 3 :(得分:5)

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

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

  1. 将所有结构的成员复制到堆栈中
  2. 如果按值返回结构,则所有成员都会从函数的堆栈内存复制到新的内存位置。
  3. 操作容易出错 - 如果结构的成员是指针,则常见错误是假设您可以安全地按值传递参数,因为您正在操作指针 - 这可能导致很难发现错误。
  4. 如果你的函数修改输入参数的值并且你的输入是结构变量,按值传递,你必须记住总是按值返回一个struct变量(我已经看过很多次了)。这意味着复制结构成员的时间加倍。
  5. 现在达到足够小的意义就结构的大小而言 - 所以它“值得”通过值传递它,这取决于一些事情:

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

答案 4 :(得分:5)

由于问题的论证传递部分已经回答,我将专注于回归部分。

做IMO最好的事情是根本不返回结构体的结构或指针,而是将指向“结果结构”的指针传递给函数。

void sum(struct Point* result, struct Point* a, struct Point* b);

这具有以下优点:

  • result结构可以在堆栈上或堆上存在,由调用者自行决定。
  • 没有所有权问题,因为很明显调用者负责分配和释放结果结构。
  • 结构甚至可以比需要的更长,或嵌入更大的结构中。

答案 5 :(得分:3)

注意:这样做的理由是这样或那样重叠。

  

何时通过值传递/返回:

  1. 该对象是基本类型,如intdouble,指针。
  2. 必须制作对象的二进制副本 - 对象不大。
  3. 速度很重要,价值传递更快。
  4. 该对象在概念上是一个小数字

    struct quaternion {
      long double i,j,k;
    }
    struct pixel {
      uint16_t r,g,b;
    }
    struct money {
      intmax_t;
      int exponent;
    }
    
  5.   

    何时使用指向对象的指针

    1. 不确定值或指向值的指针是否更好 - 因此这是默认选择。
    2. 对象很大。
    3. 速度很重要,通过指向对象的指针更快。
    4. 堆栈使用至关重要。 (在某些情况下,严格来说这可能有利于价值)
    5. 需要修改传递的对象。
    6. 对象需要内存管理。

      struct mystring {
        char *s;
        size_t length;
        size_t size;
      }
      
    7. 注意:回想一下,在C中,没有任何内容真正通过引用传递。当复制并传递指针的值时,即使传递指针也会通过值传递。

      我更喜欢传递数字,不管是int还是pixel,因为它在概念上更容易理解代码。通过地址传递数字在概念上有点困难。对于较大的数字对象,可以更快传递地址。

      传递地址的对象可以使用restrict来通知函数对象不重叠。

答案 6 :(得分:1)

在典型的PC上,即使对于相当大的结构(许多几十个字节),性能也不应成为问题。因此,其他标准很重要,尤其是语义:你真的想要复制吗?或者在同一个对象上,例如操纵链表时?指南应该是用最合适的语言结构来表达所需的语义,以使代码可读和可维护。

尽管如此,如果有任何性能影响,它可能不像人们想象的那么清晰。

  • Memcpy速度很快,内存局部性(对堆栈有利)可能比数据大小更重要:如果你在堆栈上传递并返回一个struct,那么复制可能都会在缓存中发生。此外,返回值优化应该避免冗余复制要返回的局部变量(20或30年前这些天真的编译器做过)。

  • 传递指针会将别名引入内存位置,然后无法再高效缓存。现代语言通常更注重价值,因为所有数据都与副作用隔离开来,从而提高了编译器的优化能力。

底线是肯定的,除非遇到问题,如果更方便或更合适,请随意传递值。它甚至可能更快。

答案 7 :(得分:-2)

以抽象的方式传递给函数的一组数据值是按值的结构,尽管未声明。 您可以将函数声明为结构,在某些情况下需要类型定义。当你这样做时,一切都在堆栈上。这就是问题所在。通过将数据值放在堆栈上,如果在使用或复制其他数据之前使用参数调用函数或子函数,则很容易过度写入。最好使用指针和类。