簡體   English   中英

C#算術運算正負溢出的區別

[英]Distinction between positive and negative overflow in C# arithmetic operation

我在 C#/.NET 中對已檢查的 scope 中的整數執行算術運算,以便在發生溢出時捕獲。 我想以一種簡短、智能、簡單的方式查明溢出是正數還是負數,而不需要根據操作數或操作進行大量特殊情況和檢查。

checked
{
    try
    {
        // The code below is an example operation that will throw an
        // overflow exception, that I expect to be a positive overflow.
        // In my real code, all arithmetic operations are included in this
        // code block and can result in both positive and negative overflows.
        int foo = int.MaxValue;
        int bar = 1;
        foo += bar;
    }
    catch (OverflowException)
    {
        // I have found out that a overflow occurred,
        // but was it positive or negative?
    }
}

可以嗎? 我在異常本身中沒有發現任何信息可用於查找。

長話短說:

我想以一種簡短、智能、簡單的方式查明溢出是正數還是負數,而不需要根據操作數或操作進行大量特殊情況和檢查。

您不能:C# 不會公開該信息,因為:

  • 今天的 CPU 不容易檢測溢出方向。
  • 雖然可以做到,但檢查 CPU 的 state事后術語所需的必要步驟將破壞現代超標量處理器的性能。
  • 另一種方法是在執行任何算術之前執行安全檢查,但這也會破壞性能。
  • 這僅適用於 x86/64。 .NET CLR 現在支持大約十幾個完全不同的 CPU ISA,並且必須處理它們自己的所有溢出/進位/符號特性,以確保 C# 程序對於您提議的“ checked -with-overflow”表現相同且正確-方向”是不可行的。
  • 知道算術溢出的方向沒有什么價值。 重要的是系統檢測到發生了溢出,這意味着您的代碼中存在需要修復的錯誤:並且因為您需要在正常調試實踐中重現該問題,這意味着您將能夠詳細跟蹤執行並捕獲每個值和 state,這就是您糾正導致溢出的任何潛在問題所需的全部 - 而了解正常運行時執行期間溢出方向的次要細節有助於......如何?
    • (這是一個反問句:我認為它根本沒有多大幫助,甚至可能是一個只會浪費你時間的轉移注意力的問題)

更長的答案:

問題 1:CPU 不關心溢出的方向

一些背景:今天幾乎每個微處理器都有這些特殊功能寄存器(又名CPU 標志,又名狀態寄存器,通常類似於 ARM 中的這 4 個

當然,ALU(執行算術運算的位)的CS 理論基本設計是這樣的,即 integer 操作是相同的,無論它們是有符號的還是無符號的,正的還是負的(例如減法是負操作數的加法) ),並且標志本身不會自動發出錯誤信號(例如,對於無符號算術,溢出標志被忽略,而進位標志在有符號算術中實際上不如無符號算術重要)。

(這篇文章不會解釋它們代表什么或它們如何工作,因為我假設,我博學的讀者, 已經熟悉計算機 integer 算術的基本原理

現在,您可能假設在 C#/.NET 程序的checked塊中,本機機器代碼將在每次算術運算后檢查這些 CPU 標志的狀態,以查看前一個操作是否有符號溢出或意外的進位 - 如果是這樣,則在調用/跳轉中將該信息傳遞給創建並拋出OverflowException的 CLR 內部 function。

...在一定程度上就是這樣,只是實際上從 CPU 獲得的有用信息少得令人吃驚 原因如下:

  • 在 x86/x64 上的 C# checked塊中,CLR 的 JIT 在每條可能溢出的算術指令之后插入一條 x86/x64 jo [CORINFO_HELP_OVERFLOW]指令。
    • 您可以在這個 Godbolt 示例中看到它。
    • CORINFO_HELP_OVERFLOW本機 function JIT_Overflow的地址,它(最終)調用RealCOMPlusThrowWorker以拋出OverflowException
      • 請注意, jo指令只能告訴我們設置了溢出標志:它不會公開或揭示任何其他 CPU 標志的 state,也不會顯示指令操作數的符號,因此無法使用jo指令判斷溢出是(使用您的術語)“負溢出”還是“正溢出”。

      • 因此,如果程序想要的信息不僅僅是“它溢出了,吉姆”,它需要使用 CPU 指令將 CPU 標志 state 的 rest 保存/復制到 memory 中,如果這些標志不足以確定溢出的方向那么 JIT 編譯器還必須在內存中的某個地方保留所有算術操作數的副本,這在實踐中意味着大幅增加堆棧空間或浪費 CPU 寄存器來保存您不想在算術運算成功之前刪除的舊值。

      • ...不幸的是,用於將 CPU 標志復制到 memory 或其他寄存器的 CPU 指令往往會破壞整體系統性能:

        • 考慮一下現代 CPU 設計的絕對復雜性,以及它們的超標量、推測和亂序執行,以及其他巧妙的小發明: 當程序遵循可預測的“快樂路徑”時,現代 CPU 工作得最好,不使用太多笨拙的指令CPU 內部 state 的亂七八糟。因此,將程序更改為更具內省性不僅會損害您自己程序的性能,還會損害整個計算機系統。 哎呀。

          • 來自 Rust 貢獻者Tom-Phinney的評論很好地總結了這種情況

            對“進位位”的指令級訪問,以便該值可以用作后續指令的輸入,在計算的早期實現起來很簡單,因為每條指令都在下一條指令開始之前完成。

            對於現代的、無序的、超標量處理器實現,成本/收益是相反的; “進位功能”的門成本和/或指令周期減慢遠遠超過任何可能的好處。 這就是為什么 RISC-V 是一種最先進的計算機體系結構,其預期實現范圍從 10k 門復雜度的嵌入式處理器(例如 RV32EC)到具有 100 倍以上門的超標量處理器,但沒有具體化指令-流同步進位。

問題 2:異質性

  • .NET CLR 表面上是可移植的:.NET 必須在 Windows 支持的每個平台上運行,以及根據微軟 C 級和 D 級的奇思妙想的其他平台:今天它運行在 x86/x64 上, 不同種類的 ARM (包括Apple Silicon ), 過去板谷 c ,雖然 XNA 構建在 Xbox 360 的 PowerPC 芯片上運行,並且 Compact Framework 支持 SH-3/SH-4、MIPS,我敢肯定還有許多其他的。 哦,不要忘記 Silverlight 是如何擁有自己的 CLR 版本的,它最終成為 .NET Core 和現在的 .NET 5 的基礎——它取代了 .NET Framework 4.x——而 Silverlight 也在 2007 年在 PowerPC 上運行

  • 或者以列表形式,官方.NET CLR 實現支持的所有 ISA 的我頭腦中的列表......我能想到的:

    • x86/x64
    • ARM / ARM-拇指
    • SH-3(緊湊型框架)
    • SH-4(緊湊型框架)
    • MIPS(緊湊型框架)
    • PowerPC(PPC Mac 上的 Silverlight 1.0,Xbox 360 上的 XNA)
    • 安騰 IA-64
  • 所以這是一個很好的品種 - 我敢肯定還有其他我忘記了,更不用說 Mono 支持的所有平台了。

  • 所有這些處理器/ISA 有什么共同點? 嗯,他們都有自己不同的方式來處理 integer 溢出——有時非常不同

暫無
暫無

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

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