【问题标题】:How does arithmetic overflow checking work in c#算术溢出检查如何在 C# 中工作
【发布时间】:2014-09-04 15:57:46
【问题描述】:

由于性能原因,c# 中的算术溢出检查似乎默认关闭(参见https://stackoverflow.com/a/108776/2406234 等)。

但是如果我打开它,无论是使用 /checked 标志还是 checked 关键字,运行时实际上是如何执行检查的? (我试图更好地理解这种“性能影响”的含义。)

【问题讨论】:

    标签: c#


    【解决方案1】:

    算术运算,一直到硬件级别,都支持指示所执行的运算是否导致溢出。在许多情况下,这些信息将被简单地忽略(并且通常会在非常低的级别被忽略),但是可以在每次操作后检查此结果,如果发生溢出,则抛出一个新异常。所有这些检查,以及通过各个抽象层传播这些信息,当然是有代价的。

    【讨论】:

    • 我假设实际运行的 x86 指令在每个整数指令之后基本上都会有一个条件分支,测试溢出标志(有符号)或进位标志(无符号)。您可能已将其设置为无溢出情况导致未采用分支。如果您希望能够识别溢出发生的何处,则每个分支都需要自己的目标(可能是存储唯一值然后跳转到公共处理程序的存根)。这个答案至少对那些还不知道 asm 标志/条件分支如何工作的人有用,但对其他人来说很明显。 ://
    【解决方案2】:

    C# 中的溢出检查通过在 CIL 中使用(或不使用)溢出检查来工作。

    以 C# 代码为例:

    public static int AddInts(int x, int y)
    {
        return x + y;
    }
    

    如果不进行溢出检查,它将被编译为:

    .method public hidebysig static int32 AddInts(int32 x, int32 y) cil managed 
    {
        .maxstack 2
        IL_0000: ldarg.0
        IL_0001: ldarg.1
        IL_0002: add
        IL_0003: ret
    }
    

    通过溢出检查,它将被编译为:

    .method public hidebysig static int32 AddInts(int32 x, int32 y) cil managed 
    {
        .maxstack 2
        IL_0000: ldarg.0
        IL_0001: ldarg.1
        IL_0002: add.ovf
        IL_0003: ret
    }
    

    如您所见,CIL 表单使用add 的不同溢出检查和非溢出检查形式,同样适用于checkedunchecked 影响C# 中行为的每个操作。这在一段混合了许多检查和未检查操作的代码中会更方便,但绝大多数情况下,大多数情况下在方法中将检查在一起或未检查在一起,因此 C# 的程序集默认方法是在一个块中覆盖几乎总是对人类编码人员更方便。

    当这个 CIL 被 jitted 时会发生什么当然取决于它被 jitted 到的处理器。一个可能的结果是使用了类似的溢出检查指令(这将导致溢出中断,这可用于产生 .NET 在这里想要的异常)或像 x86 的 jo 这样的溢出跳转指令。

    我正试图更好地了解这种“性能影响”的含义。

    确实,unchecked 几乎总是和checked 一样快,而且通常更快,因为忽略溢出可以让路径更有效,而不是阻止。简而言之,检查溢出有时需要另一个操作,尽管大多数时候在低级别非常快,并且做某事几乎总是比什么都不做要慢。*因此,在您知道不会发生溢出的情况下,它在不检查不可能的情况下确实有轻微的性能优势。

    但是,这应该被视为unchecked 的次要功能。

    主要特点是它改变了算术运算的含义意思

    对于两个 32 位整数(例如),unchecked(x + y) 表示“将 x 和 y 相加并将结果强制转换为 32 位二进制补码”,而checked(x + y) 表示“将 x 和 y 相加并返回产生的 32 位二进制补码数”。

    因此,当unchecked(int.MaxValue + int.MaxValue) 返回-2 时,这是正确答案,而checked(int.MaxValue + int.MaxValue) 没有正确答案,因此会引发例外。

    它们确实是不同的操作,在很多情况下我们想要-2 第一个返回。

    因此,我们主要关心的是“如果我们超出类型的限制,正确的答案是什么?”

    1. 丢弃位导致的答案是正确答案,因为我们将这些值视为位集:使用unchecked
    2. 我们将这些值视为代表某种计数或度量的整数,但它们如此之高意味着某种错误或我们不准备处理的数量:使用checked
    3. 我们将这些值视为表示某些计数或度量的整数,并且对于这些值仍应正确处理数学:使用longBigInteger 而不是int,这样您就可以正确处理所有可能性. (如果性能分析显示它有显着帮助,也许只需要 int 就可以快速路径)。
    4. 我们将这些值视为整数,但我们要么相信它们永远不会如此庞大,要么可以忍受这种奇怪的结果,因为它必须是“垃圾进垃圾出”的情况":严格来说,这适合 checked 使用,但 unchecked 会得到相同的结果,因此我们可以使用它来获得轻微的性能提升。

    人们与每个人打交道的频率取决于他们的编程目的。可能大多数程序最常处理第四种情况(你的程序多久处理一次超过几百万的事情?),我们应该在语义上使用checked,但它没有真正的区别,所以我们不妨获得unchecked 的性能提升。

    第一种情况在某类情况下很常见,尤其是比较低级的代码;通常,程序算术在其大多数“业务逻辑”中都适合第四种情况,而在许多低级库正在处理的东西中,第一种情况。

    当我们需要一些接近“真实世界”数学的算术并且int 无法解决时,我们通常仍然需要“真实”结果,所以我们处于第三种情况的某种变体中。

    真的,做一些算术并偶尔说“对不起,我无法处理这个”的第一种情况很少是理想的行为; OverflowException 通常是一种异常,它告诉开发人员他们在哪里遇到问题,而不是我们捕获的那种异常,然后变成对用户有帮助的错误消息。因此,大多数情况下,当我们遇到第一个案例时,我们认为我们遇到了第四个案例,但我们错了。

    因此,它可以用于:

    1. 将所有符合第一种情况的代码(绝对应该是@​​987654349@)标记为unchecked,即使这对于项目默认值来说是多余的。
    2. 将所有真正应该抛出OverflowException 的代码标记为checked,因为你会用它(很少见)做一些有用的事情。
    3. 大部分时间使用unchecked 来提高性能,但始终使用checked 进行调试,偶尔运行您的单元测试,或者如果您有一些奇怪的行为。 (此处需要谨慎平衡的地方取决于应用程序)。

    *也有可能发生分支错误预测,尽管非溢出情况很可能是最常预测的情况,也是最常遇到的情况。一般来说,分支错误预测在这里的风险并不像在其他情况下那样大,包括手动检查溢出。

    【讨论】:

    • 如果这个检查过的 C# 代码调用了导致溢出的原生 C++ 代码会发生什么?溢出位会打开吗?即使检查指令没有编译成代码。我以后会得到这些溢出异常吗?与下一次检查?如果选中的部分启动新进程并等待进程退出怎么办。有什么东西可以清理溢出结果吗?我想这些位存储在寄存器中,这就是为什么生命周期会像线程的生命周期一样。这就是为什么它不执行检查子进程的原因,因为它是具有不同寄存器的不同线程。我说的对吗?
    • @qub1n 我不得不说我真的不知道这两者之间的接口会发生什么。
    • @qub1n:抱歉耽搁了这么久;我现在才看到这个。您正确地注意到调用与算术控制位混淆的非托管代码是运行时提供程序可以想象的最噩梦的全局状态。早在 1990 年代,我就在 MSFT 脚本引擎上工作,它依赖于算术控制位处于特定状态,而 delphi 编写的控件依赖于它们处于相反状态,因此调用 delphi 控件的脚本得到与控制位如何设置的斗争。那不是编写或调试的有趣代码。
    猜你喜欢
    • 2010-12-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多