簡體   English   中英

我什么時候應該按值傳遞或返回結構?

[英]When should I pass or return a struct by value?

在 C 中,結構可以按值傳遞/返回,也可以通過引用(通過指針)傳遞/返回。

普遍的共識似乎是前者可以在大多數情況下應用於小型結構而不會受到懲罰。 請參閱在任何情況下直接返回結構是好的做法嗎? 在 C 中按值傳遞結構而不是傳遞指針有什么缺點嗎?

從速度和清晰度的角度來看,避免取消引用可能是有益的。 但是什么算 我想我們都同意這是一個小結構:

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 kernel 模式堆棧。)? 但是中產怎么辦。 我認為小於寄存器的結構可以嗎? 但是這些呢?

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. 如果淺拷貝是所需的行為(稱為 function 無論如何都會執行淺拷貝)怎么辦?

編輯:不知道為什么這被標記為可能的重復,因為我實際上在我的問題中鏈接了另一個問題。 我要求澄清什么是結構,並且我很清楚大多數時候結構應該通過引用傳遞。

我的經驗,近40年的實時嵌入,使用C持續20年; 是最好的方法是傳遞指針。

在任何一種情況下,都需要加載結構的地址,然后需要計算感興趣的字段的偏移量...

傳遞整個結構時,如果沒有通過引用傳遞,那么

  1. 它沒有放在堆棧上
  2. 它被復制,通常是通過對memcpy()的隱藏調用
  3. 它被復制到現在“保留”的內存部分,並且不可用於程序的任何其他部分。

當按值返回結構時,存在類似的考慮因素。

但是,可以完全保存在工作寄存器中的“小”結構在這些寄存器中傳遞,特別是如果在編譯語句中使用了某些優化級別。

被認為是“小”的細節取決於編譯器和底層硬件架構。

在小型嵌入式架構(8/16位)上 - 總是通過指針傳遞,因為非平凡的結構不適合這種微小的寄存器,並且這些機器通常也是寄存器缺乏的。

在類似PC的架構(32位和64位處理器)上 - 按值傳遞結構是正確的,提供sizeof(mystruct_t) <= 2*sizeof(mystruct_t*)並且函數沒有很多(通常超過3個機器字')值得)其他論點。 在這些情況下,典型的優化編譯器將在寄存器或寄存器對中傳遞/返回結構。 然而,在x86-32上,由於x86-32編譯器必須處理的非常大的寄存壓力,這個建議應該帶有大量的鹽 - 由於減少了寄存器溢出和填充,傳遞指針可能仍然更快。

另一方面,在PC-like上按值返回結構遵循相同的規則,除了當指針返回結構時,要填充的結構也應該通過指針傳入 - 否則,被調用者和調用者不得不就如何管理該結構的內存達成一致。

如何將結構傳遞給函數或從函數傳遞結構取決於應用程序二進制接口(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

所以:沒有經驗法則; 這取決於太多因素。

由於問題的論證傳遞部分已經回答,我將重點關注回歸部分。

做IMO的最好的事情是根本不返回結構或指向結構的指針,而是將指向“結構結構”的指針傳遞給函數。

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

這具有以下優點:

  • result結構可以在堆棧上或堆上存在,由調用者自行決定。
  • 沒有所有權問題,因為很明顯調用者負責分配和釋放結果結構。
  • 結構甚至可以比需要的更長,或嵌入更大的結構中。

實際上最好的經驗法則是,通過引用和值將結構作為參數傳遞給函數,就是避免按值傳遞它。 風險幾乎總是超過收益。

為了完整起見,我將指出當通過值傳遞/返回結構時,會發生一些事情:

  1. 所有結構的成員都被復制到堆棧中
  2. 如果按值返回結構,則所有成員都將從函數的堆棧內存復制到新的內存位置。
  3. 該操作容易出錯 - 如果結構的成員是指針,則常見錯誤是假設您可以安全地按值傳遞參數,因為您正在操作指針 - 這可能導致很難發現錯誤。
  4. 如果你的函數修改了輸入參數的值,你的輸入是結構變量,按值傳遞,你必須記住總是按值返回一個struct變量(我已經看過很多次)。 這意味着復制結構成員的時間加倍。

現在,在結構的大小方面達到足夠小的意義 - 這樣它“值得”通過值傳遞它,這取決於一些事情:

  1. 調用約定:編譯器在調用該函數時自動保存在堆棧中的內容(通常是幾個寄存器的內容)。 如果您的結構成員可以利用這種機制復制到堆棧上,那么就不會受到懲罰。
  2. 結構成員的數據類型:如果你的機器的寄存器是16位,你的結構成員數據類型是64位,它顯然不適合一個寄存器,因此只需要為一個副本執行多個操作。
  3. 你的機器實際擁有的寄存器數量:假設你的結構只有一個成員,一個字符(8位)。 當按值或通過引用傳遞參數時(理論上),這應該導致相同的開銷。 但是還有另外一個危險。 如果您的體系結構具有單獨的數據和地址寄存器,則通過值傳遞的參數將占用一個數據寄存器,通過引用傳遞的參數將占用一個地址寄存器。 按值傳遞參數會對數據寄存器施加壓力,這些數據寄存器通常比地址寄存器使用得多。 這可能會導致堆棧溢出。

底線 - 很難說什么時候按值傳遞結構是可以的。 只是不這樣做更安全:)

注意:這樣做的原因是這樣或那樣重疊。

何時通過值傳遞/返回:

  1. 該對象是一個基本類型,如intdouble ,pointer。
  2. 必須創建對象的二進制副本 - 並且對象不大。
  3. 速度很重要,價值傳遞更快。
  4. 該對象在概念上是一個小數字

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

何時使用指向對象的指針

  1. 不確定值或指向值的指針是否更好 - 因此這是默認選擇。
  2. 對象很大。
  3. 速度很重要,通過指向對象的指針傳遞速度更快。
  4. 堆棧使用至關重要。 (在某些情況下,這可能會受到價值的影響)
  5. 需要修改傳遞的對象。
  6. 對象需要內存管理。

     struct mystring { char *s; size_t length; size_t size; } 

注意:回想一下,在C中,沒有任何內容真正通過引用傳遞。 當復制並傳遞指針的值時,即使傳遞指針也會通過值傳遞。

我更喜歡傳遞數字,無論是int還是pixel值,因為它在概念上更容易理解代碼。 通過地址傳遞數字在概念上有點困難。 對於較大的數字對象,通過地址傳遞可能更快

傳遞了地址的對象可以使用restrict來通知函數對象不重疊。

在典型的PC上,即使對於相當大的結構(許多幾十個字節),性能也不應成為問題。 因此,其他標准很重要,尤其是語義:你真的想要復制嗎? 或者在同一個對象上,例如操作鏈表時? 指南應該是用最合適的語言結構表達所需的語義,以使代碼可讀和可維護。

也就是說,如果有任何性能影響,它可能不像人們想象的那么清晰。

  • Memcpy很快,內存局部性(對堆棧有利)可能比數據大小更重要:如果在堆棧上按值傳遞和返回結構,則復制可能都發生在緩存中。 此外,返回值優化應該避免冗余復制要返回的局部變量(20或30年前這些天真的編譯器做了)。

  • 傳遞指針會將別名引入內存位置,然后無法再高效緩存。 現代語言通常更注重價值,因為所有數據都與副作用隔離開來,從而提高了編譯器的優化能力。

底線是肯定的,除非遇到問題,如果更方便或更合適,可以隨意傳遞值。 它甚至可能更快。

我們不會按值傳遞結構,也不會一直使用裸指針(喘氣)。 例子。

ERR_HANDLE mx_multiply ( MX_HANDLE result, MX_HANDLE left, MX_HANDLE right ) ;
  • 結果 left 和 right 是 2D 矩陣的相同(結構)類型的實例
  • 乘是其他一些錯誤(結構)類型
  • 'handle' 是 memory 'slab' 上為相同類型的實例預先分配的結構的地址

這安全嗎? 非常。 這慢嗎? 與裸指針相比要慢一些。

以抽象的方式,傳遞給函數的一組數據值是按值的結構,盡管這是未聲明的。 您可以將函數聲明為結構,在某些情況下需要類型定義。 當你這樣做時,一切都在堆棧上。 這就是問題所在。 通過將數據值放在堆棧上,如果在使用或復制其他數據之前使用參數調用函數或子函數,則很容易過度寫入。 最好使用指針和類。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM