关于C#:我应该何时通过值传递或返回结构?

When should I pass or return a struct by value?

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

一般的共识是前者在大多数情况下可以应用于小结构而不受惩罚。看有没有哪种情况下直接返回结构是好的做法?在C中按值传递结构,而不是传递指针,有什么缺点吗?

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

1
struct Point { int x, y; };

我们可以通过价值传递而相对不受惩罚:

1
2
3
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内核模式栈!)但是中号的呢?我假设小于寄存器的结构是可以的。但是这些呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
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;
};

确定一个结构是否足够小以安全地通过值传递它的最佳经验法则是什么(除了一些可减轻的情况,如一些深层递归)?

最后,请不要告诉我我需要介绍一下。当我太懒的时候,我要求一个启发式的方法来使用/这不值得进一步调查。

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

  • 如果结构实际上比指向它的指针小怎么办?

  • 如果一个浅拷贝是所需的行为(被调用的函数无论如何都将执行一个浅拷贝),该怎么办?

  • 编辑:不知道为什么这个被标记为可能的副本,因为我实际上链接了我问题中的另一个问题。我要求澄清什么构成一个小结构,我很清楚大部分时间结构应该通过引用传递。


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

    无论哪种情况,都需要加载结构的地址,然后需要计算感兴趣字段的偏移量…

    传递整个结构时,如果不通过引用传递,然后

  • 它没有放在堆栈上
  • 它被复制,通常通过对memcpy()的隐藏调用
  • 它被复制到一个现在是"保留"的内存部分。不适用于程序的任何其他部分。
  • 对于按值返回结构的情况,也存在类似的考虑。

    然而,"小"结构,它可以完全保存在一个工作寄存器中到两个在那些寄存器中传递尤其是在使用某些优化级别时在编译语句中。

    所谓"小"的细节取决于编译器和底层硬件架构。


    关于小型嵌入式建筑物(8/16-Bitters)——总是通过指针,因为非微型结构不适合这样的小型登记册,而这些机器一般都是注册的。

    在PC-Like Architectures(32和64位处理器)——经过一个价值结构是可以提供EDOCX1&0)和函数没有很多(通常超过3个机器字的其他论据。在这些圆环下,典型的优化编译器将通过/返回一对寄存器或一对寄存器结构。然而,在X86-32中,由于异常压力下的X86-32编译器必须与----通过指针,由于减少了寄存器的研磨和填充,本咨询意见应当以一种杂质颗粒的盐为基础。

    根据同一规则,通过对PC-LIKES的估价,在其他手中恢复一个结构,但要记住的事实是,当一个结构通过指针返回时,填充的结构应通过指针和其他方式——其他方面,Callee和Caller就如何管理该结构的记忆达成了一致意见。


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

    如果PC实际上允许在寄存器中传递一个结构,那么这不仅取决于它的大小,还取决于它在参数列表中的位置以及前置参数的类型。例如,arm-pcs(aapcs)将参数打包到前4个寄存器中,直到它们满为止,并将进一步的数据传递到堆栈中,即使这意味着参数被拆分(如果感兴趣,则全部简化:文档可以从arm免费下载)。

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

    有些PC可能对参数结构执行相同的操作,而其他PC只使用与scalar相同的机制。无论如何,您会推迟这样的优化,直到您真正知道需要它们为止。还可以阅读目标平台的PC。记住,您的代码可能在不同的平台上执行得更差。

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

    如果只是为了原版的不变性:通过一个const mystruct *ptr。除非丢弃const,否则至少在写入结构时会给出警告。指针本身也可以是常量:const mystruct * const ptr

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


    既然通过部分问题的论点已经被回答了,我将集中讨论返回部分。

    IMO最好不要返回结构或指向结构的指针,而是将指向"result struct"的指针传递给函数。

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

    这有以下优点:

    • 根据调用方的判断,result结构可以位于堆栈上,也可以位于堆上。
    • 没有所有权问题,因为显然调用方负责分配和释放结果结构。
    • 结构甚至可能比需要的更长,或者嵌入到更大的结构中。

    实际上,当通过引用而不是按值将结构作为参数传递给函数时,最好的经验法则是避免按值传递。风险几乎总是大于收益。

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

  • 结构的所有成员都复制到堆栈上
  • 如果按值返回结构,则所有成员都将从函数的堆栈内存复制到新的内存位置。
  • 操作容易出错-如果结构的成员是指针,则常见的错误是假定您可以安全地按值传递参数,因为您是在指针上操作-这可能会导致很难发现错误。
  • 如果您的函数修改了输入参数的值,而您的输入是按值传递的结构变量,则必须记住始终按值返回结构变量(我已经多次看到这一点)。这意味着复制结构成员的时间加倍。
  • 现在,了解结构大小的"足够小"意味着什么-这样它就"值得"通过值传递它,这取决于以下几点:

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


    注:Reasons to do so one or the other overlap.

    When to pass/return by value:

  • 该对象是一种基本类型,如intdouble,指针。
  • 必须制作对象的二进制拷贝,对象不宽。
  • 速度是重要的,价值是快速的。
  • 这个对象概念上是一个小数字

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    struct quaternion {
      long double i,j,k;
    }
    struct pixel {
      uint16_t r,g,b;
    }
    struct money {
      intmax_t;
      int exponent;
    }
  • BLCK1/

  • 不确定的价值或指针是更好的——所以这是缺陷的选择。
  • 这个对象很宽。
  • 速度是重要的,通过指针对对象是快速的。
  • 堆栈使用是关键。(Strictly this may favor by value in some cases)
  • 需要修改过去的对象。
  • 对象需要存储器管理

    ZZU1

  • 注:Recall that in C,nothing is truely passed by reference.即使经过一个指针,也要通过一个值,因为指针的值被复制和传输。

    我喜欢通过号码,是他们intpixel的价值,因为它在概念上容易理解代码。通过地址的数字是一个概念上更困难的比特。用较大的数字对象,可能会很快通过地址。

    有地址的对象可以使用restrict通知对象的功能。


    在一个典型的PC中,性能不应是一个问题,甚至是一个公平的大型结构(许多字节的狗)。因此,其他标准很重要,特别是语言学:你不想复制吗?Or on the same object,E.G.when manipulating linked lists?指导方针应以最适当的语言来表达所希望的语种,以便使代码可以实现和维持。

    他说,如果有任何性能的影响,它可能不会像一个人想的那样清晰。

    • Mempy is fast,and memory locality(which is good for the stack)may be more important than data size:the copying may all happen in the chese,if you pass and return a stack back on the value.此外,回归价值的优化应避免重复拷贝当地变量,以供回归(在前20年或30年内)。

    • 经过指针周围引入异形来存储定位,这样就不会被有效地隐藏任何长度。现代语言往往更有价值,因为所有数据都是从侧面效应中分离出来的,从而提高了编译器的优化能力。

    底线是对的,除非你运行到问题中,如果它更合适或更合适的话,你可以自由通过价值。这可能会很快。


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