簡體   English   中英

C# 如何返回結構體

[英]How C# returns Structs

結構是值類型,因此每次對結構進行操作時都會完全復制。 由於它們是值類型,因此結構在堆棧中而不是在堆中分配。

當結構體作為參數傳遞時,我可以看到結構體如何降低方法的性能,因為它們總是會被復制到堆棧中,特別是如果它們很大並且有很多內部字段。

但我很好奇 C# 如何處理結構體的返回。

在 C 中,返回是通過寄存器進行的,或者如果要返回的值對於寄存器來說太大,則通過使用堆的引用進行返回。 實際上,所有 C# 結構體教程都說結構體存在於堆棧中,而不是堆中。

所以在下面的代碼中:

MyStruct ms = GetMyValue();

GetMyValue()在哪里

MyStruct GetMyValue();

C# 將如何處理 ms 變量的結構體的返回? 特別是如果它對於寄存器來說太大了? 它實際上是否會將其復制到堆中,然后再次將其復制回方法的調用者並將其分配給 ms?


編輯:

要解決帖子中留下的評論:

  1. 在發布這篇文章之前,我已經閱讀了一些關於 C# 結構的教程,特別是本教程使用單詞stack次數比我費心去數的次數還要多。 這個 MSDN 教程也談到了堆棧,雖然它是從 2003 年開始的,但我認為從那時起結構沒有改變。

  2. 我知道這可能與 C# 完全不同,但實際上是 JIT 編譯器本身或 CLR 或其他我不知道的問題。 這就是我的問題的目的,了解更多關於 C# 的內部工作原理,即使這實際上與語言本身無關。

  3. 有 C 函數調用約定,對我的 Post 的最好支持是這個 StackOverflow post 當我第一次在這里發布它時,我只是說了我記得的,但是因為 SO 的答案是:

    至於你的具體問題,這取決於 ABI。 有時如果返回值大於4個字節但不大於8個字節,則可以拆分為EAX和EDX。 但大多數情況下,調用函數只會分配一些內存(通常在堆棧上)並將指向該區域的指針傳遞給被調用函數。

    在這個問題上我可能是錯的,我說可能是,因為答案通常是

  4. 我想了解結構如何處理的真正原因是因為我有一個項目,我必須多次讀取串行端口來輪詢數據,這些數據將通過一種方法返回。

    由於數據只是一些字節,我認為我可以從結構中獲得一些性能,而不是使用類來抽象串行端口傳入的字節,但是如果return會將struct作為堆分配傳遞,我對性能提升的期望可能假的。

    是的,我可以做一個簡單的測試並比較性能,我知道,但我想真正了解它是如何在幕后完成的,而不僅僅是記住我的模擬結果。 我喜歡知道我使用的東西實際上是如何工作的,而不僅僅是學習如何使用它們。

值類型不僅位於堆棧上。 它們也生活在字段和數組中。 引用類型的主要區別在於值類型按值復制並且沒有標識。 堆棧與堆的想法是錯誤的。

在 C 中,返回是通過寄存器進行的,或者如果要返回的值對於寄存器來說太大,則使用堆進行引用

不涉及堆。 調用者為要放置的返回值分配空間。它傳遞一個指向該空間的指針。 被調用者可以填充該空間。 .NET CLR 也這樣做。 當然,這是一個實現細節。

但我想真正學習

這是非常好的。 您無法測試我剛剛告訴您的內容。 你需要對你相信別人說的話多一點批評。 要么你有糟糕的教程,要么你以不精確的方式閱讀它們。

當結構體作為參數傳遞時,我可以看到結構體如何降低方法的性能,因為它們將始終被復制到堆棧中

我認為情況並非總是如此。 我不太確定,但我認為 JIT 有時可以在寄存器中傳遞結構。 .NET JIT 確實沒有進行太多優化,但我認為這是在一定程度上起作用的優化。 可能是由一些單字段結構的存在驅動的,例如DateTime

結構並不總是存在於堆棧中。 如果你在函數內部分配一個結構體,它就會在堆棧中存活。 如果它是引用類型(類/數組(隱式派生自 System.Array/Object)的字段,則它在堆上生存。就它們如何返回而言,這可能取決於該 CPU 架構的 ABI。

從它的聲音來看,您從未處理過 IL/匯編/代碼生成,因此讓我們構建一個動態方法,該方法等效於 MyStruct ms = GetMyValue()/編譯器將在單詞堆棧的上下文中生成的內容。 “東西”永遠不會真正返回。 事物(s,在元組意義上我敢肯定)被壓入堆棧,然后發出返回指令。 為調用者留下返回值。 我們將假設 GetMyValue() 分配一個新的 MyStruct 並將其分配給一個局部變量。 生成的代碼看起來像這樣(我擴展了 ILGenerator 類):

ILGenerator generator = dynamicMethod.GetILGenerator();

generator
    .DeclareLocal(typeof(MyStruct))
    .EmitCall(OpCodes.Call, typeof(EncapsulatingClass).GetMethod("GetMyValue"))
    .Emit(OpCodes.Stloc, 0);

這里發生的是(其中一些是我對 CLI 運行時如何工作的假設):

  1. 調用函數在當前本地列表索引處保留一個 typeof(MyStruct) 插槽。

  2. GetMyValue() 被調用,以與我們正在構建的方法相同的方式保留本地 MyStruct,發出 OpCodes.Newobj,它以 sizeof(MyStruct) 的量向下分配和調整 ESP(擴展堆棧指針),發出 OpCodes.Stloc將 ESP 減去 sizeof(MyStruct) 存儲到保留的本地索引中,對其字段執行一些操作,調用 Emit(OpCodes.Ldloc, 0) 將本地指向的地址推送到調用函數的計算堆棧上,並發出一個OpCodes.Ret 返回。

  3. 調用函數發出一個 OpCodes.Stloc 來存儲(復制)MyStruct 的內容,評估堆棧的頂部指向(這是如何發生的,我確定答案是它取決於,不幸的是),在本地索引 0。

我不是如何以任何方式構建 CLI 運行時的專家,所以很多這都是對發生的事情的假設。 持保留態度,我絕不是 CPU 工程專家。 OpCodes.Ldloc、OpCodes.Ret、OpCodes.Stloc——ms = GetMyValue()——的指令流段如何處理,可能取決於JITer如何將IL轉換為實際的cpu特定機器指令。 比如X86。 決定結構是否會返回到寄存器中的因素可能僅限於一個寄存器,因此無論最大的寄存器是什么,以及是否適合其中的任何結構。 我知道 CPU 可以為內存偏移組合寄存器,但我不確定這是否適用於返回多個寄存器內的結構。 要記住的另一件事,GetMyValue() 超出了范圍,這意味着在范圍意義上分配的 struct GetMyValue() 不再存在,但在堆棧意義上(分配的位置),它確實存在,所以 JITer 很可能只是將地址 OpCodes.Ldloc 壓入堆棧,並將其直接放入調用者本地索引 0 中。因為由於函數返回,沒有任何東西可以再復制它。 使調用者成為結構的新所有者。 在這種特殊情況下完全避免任何復制和注冊。 這可能也是調用約定發揮作用的地方。 問題是,如果您出於任何原因在 GetMyValue() 中分配了三個結構,則在分配的第一個結構之后返回任何結構都會破壞該優化,這是下一次優化的地方,返回寄存器內的結構(如果適合),上場。 離開最壞的情況,為調用者再次將其內容完全復制到堆棧上。 我可能是錯的,非常歡迎任何人加入並糾正我。 一個很好的起點,將是 github 並查看運行時如何處理結構的 OpCodes.Ldloc/Stloc。 我想這是獲得所需答案的好地方。

編輯:你讀過的任何教程都說結構總是在堆棧上分配,讓它們全部 DDoS。

暫無
暫無

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

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