【问题标题】:Why don't languages raise errors on integer overflow by default?为什么默认情况下语言不会在整数溢出时引发错误?
【发布时间】:2010-09-11 08:17:23
【问题描述】:

在几种现代编程语言(包括 C++、Java 和 C#)中,该语言允许 integer overflow 在运行时发生,而不会引发任何类型的错误条件。

例如,考虑这个(人为的)C# 方法,它没有考虑上溢/下溢的可能性。 (为简洁起见,该方法也不处理指定列表为空引用的情况。)

//Returns the sum of the values in the specified list.
private static int sumList(List<int> list)
{
    int sum = 0;
    foreach (int listItem in list)
    {
        sum += listItem;
    }
    return sum;
}

如果这个方法调用如下:

List<int> list = new List<int>();
list.Add(2000000000);
list.Add(2000000000);
int sum = sumList(list);

sumList()方法会发生溢出(因为C#中int类型是32位有符号整数,列表中的值之和超过了最大32位有符号整数的值)。 sum 变量的值为 -294967296(不是 4000000000);这很可能不是 sumList 方法的(假设的)开发者想要的。

显然,开发人员可以使用多种技术来避免整数溢出的可能性,例如使用 Java 的 BigInteger 之类的类型,或者 C# 中的 checked 关键字和 /checked 编译器开关。

但是,我感兴趣的问题是,为什么这些语言被设计为默认情况下首先允许发生整数溢出,而不是例如在运行时执行操作时引发异常导致溢出。如果开发人员在编写执行可能导致溢出的算术运算的代码时忽略了溢出的可能性,这种行为似乎有助于避免错误。 (这些语言可能包含类似“unchecked”关键字的东西,它可以指定一个允许发生整数溢出而不引发异常的块,在开发人员明确表示该行为的情况下;C# 实际上是does have this。 )

答案是否简单归结为性能 - 语言设计者不希望他们各自的语言默认具有“慢”算术整数运算,运行时需要做额外的工作来检查是否发生溢出,on每个适用的算术运算——在发生意外溢出的情况下,这种性能考虑超过了避免“静默”失败的价值?

除了性能方面的考虑之外,做出这种语言设计决策还有其他原因吗?

【问题讨论】:

    标签: language-agnostic integer language-design integer-overflow


    【解决方案1】:

    在 C# 中,这是一个性能问题。具体来说,就是开箱即用的基准测试。

    当 C# 刚出现时,微软希望很多 C++ 开发人员会改用它。他们知道许多 C++ 人认为 C++ 速度很快,尤其是比那些在自动内存管理等上“浪费”时间的语言更快。

    潜在的采用者和杂志评论者都可能会获得新 C# 的副本,安装它,构建一个在现实世界中没人会编写的微不足道的应用程序,在一个紧密的循环中运行它,并测量它的运行时间拿。然后他们会为他们的公司做出决定或根据该结果发表文章。

    他们的测试表明 C# 比本机编译的 C++ 慢,这一事实会让人们很快放弃 C#。您的 C# 应用程序将自动捕获上溢/下溢这一事实是他们可能会错过的事情。所以,默认情况下它是关闭的。

    我认为很明显 99% 的时间我们都希望 /checked 处于打开状态。这是一个不幸的妥协。

    【讨论】:

    • 整数溢出检查对实际基准测试有多大影响?在我看来,C# 中还有很多其他的东西要糟糕得多(例如,给定一个字段readonly rect foo;,像DoSomething(foo.X, foo.Y 这样的语句);` 需要复制foo,调用它的X 访问器方法,制作foo 的另一个副本,并调用其Y 访问器方法。相当大的开销——足以让整数溢出检查相比之下显得微不足道。
    • @supercat:在大多数现实世界的代码中,默认打开/checked 不会明显变慢,并且会捕获特定类别的重要错误。然而,正如我在回答中所说,C# 的早期审阅者可能要做的第一件事就是在紧密循环中测试整数算术的性能。
    • 请记住,C# 现在已经 12 岁了。当时,许多程序员认为“C++ 快,Java 慢,C# 就像 Java”。今天,我认为,大多数程序员认为上市时间和管理复杂性对业务成功的影响比紧密循环优化更大。
    • 是 perf 的原因吗?我个人发现自己经常依赖于行为(即默认情况下未选中),例如我希望循环的序列计数器或数据库没有无符号值的类型,但它是无符号的,所以强制转换可以工作......
    【解决方案2】:

    我认为性能是一个很好的理由。如果你考虑一个典型程序中增加整数的每条指令,并且如果不是简单的操作来加 1,它必须每次检查加 1 是否会溢出类型,那么额外周期的成本将非常严重。

    【讨论】:

    • 操作溢出时CPU不是给你一个flag吗? (当然,检查那个标志也需要时间)。
    • @Thilo 有没有像 MIPS 这样的标志的 CPU。即使对它们进行简单的大 int 操作也是一种轻微的痛苦
    【解决方案3】:

    您的工作假设整数溢出总是不受欢迎的行为。

    有时整数溢出是期望的行为。我见过的一个例子是将绝对航向值表示为定点数。给定一个无符号整数,0 是 0 或 360 度,最大 32 位无符号整数 (0xffffffff) 是 360 度以下的最大值。

    int main()
    {
        uint32_t shipsHeadingInDegrees= 0;
    
        // Rotate by a bunch of degrees
        shipsHeadingInDegrees += 0x80000000; // 180 degrees
        shipsHeadingInDegrees += 0x80000000; // another 180 degrees, overflows 
        shipsHeadingInDegrees += 0x80000000; // another 180 degrees
    
        // Ships heading now will be 180 degrees
        cout << "Ships Heading Is" << (double(shipsHeadingInDegrees) / double(0xffffffff)) * 360.0 << std::endl;
    
    }
    

    可能还有其他可以接受溢出的情况,类似于这个例子。

    【讨论】:

    • 整数溢出产生错误结果的情况可能比它产生正确结果的情况更多。事实上,它真正产生正确结果的唯一时间是它是预期的行为,并且它实际上被用作一个特性,例如在你的示例中。
    • @Kibbee 我愿意冒险,在整数溢出可能导致错误的情况下,与其说是编程语言失败,不如说是您没有进行适当的范围检查。在不符合预期的情况下,您的代码应该检查它。
    • 更不用说依赖像这样特定于平台的低级细节的固有危险。如果在 64 位平台上重新编译会发生什么?哎呀。
    • @Dan:注意代码使用了 uint32_t。保证是 32 位的(因此得名 :-))。所以作者确实想到了这一点。
    • @sleske:如果您检查编辑,您会看到 Doug 的原始帖子使用“unsigned int”...因此我的评论。也许我的评论导致他更正了他的代码......
    【解决方案4】:

    C/C++ 从不强制执行陷阱行为。即使是明显的除以 0 也是 C++ 中未定义的行为,而不是特定类型的陷阱。

    C 语言没有任何陷阱的概念,除非你计算信号。

    C++ 有一个设计原则,即除非您要求,否则它不会引入 C 中不存在的开销。所以 Stroustrup 不会想要强制要求整数的行为方式需要任何显式检查。

    一些早期的编译器和受限硬件的轻量级实现根本不支持异常,并且通常可以使用编译器选项禁用异常。强制语言内置例外是有问题的。

    即使 C++ 已经检查了整数,如果为了性能提升而关闭,早期 99% 的程序员也会关闭...

    【讨论】:

      【解决方案5】:

      因为检查溢出需要时间。每个原始数学运算(通常转换为单个汇编指令)都必须包括溢出检查,从而导致多个汇编指令,可能导致程序慢几倍。

      【讨论】:

        【解决方案6】:

        这可能是 99% 的性能。在 x86 上,必须检查每个操作的溢出标志,这将对性能造成巨大影响。

        另外 1% 将涵盖人们正在执行花哨的位操作或在混合有符号和无符号操作时“不精确”并想要溢出语义的情况。

        【讨论】:

          【解决方案7】:

          向后兼容性很重要。对于 C,假设您对数据类型的大小给予了足够的关注,以至于如果发生上溢/下溢,那就是您想要的。然后是 C++、C# 和 Java,“内置”数据类型的工作方式几乎没有改变。

          【讨论】:

            【解决方案8】:

            如果整数溢出定义为立即引发信号、抛出异常或以其他方式转移程序执行,则任何可能溢出的计算都需要按指定的顺序执行。即使在整数溢出检查不会直接花费任何成本的平台上,将整数溢出捕获在程序执行序列中的正确点的要求也会严重阻碍许多有用的优化。

            如果一种语言指定整数溢出将设置一个锁存错误标志,则限制函数内对该标志的操作如何影响其在调用代码中的值,并规定该标志不需要设置在溢出不会导致错误输出或行为的情况下,编译器可以生成比程序员可以使用的任何类型的手动溢出检查更有效的代码。举个简单的例子,如果 C 中有一个函数可以将两个数字相乘并返回一个结果,并在溢出的情况下设置一个错误标志,那么无论调用者是否会使用结果,都需要编译器来执行乘法运算.然而,在像我描述的那样具有更宽松规则的语言中,确定没有任何东西使用乘法结果的编译器可以推断出溢出不会影响程序的输出,并完全跳过乘法。

            从实际的角度来看,大多数程序并不关心溢出发生的确切时间,因为它们需要保证它们不会因为溢出而产生错误的结果。不幸的是,编程语言的整数溢出检测语义没有赶上让编译器生成高效代码所必需的语义。

            【讨论】:

              【解决方案9】:

              我对为什么默认情况下不会在运行时引发错误的理解归结为希望创建具有类似 ACID 行为的编程语言的传统。具体来说,您编写代码来执行(或不编写代码)的任何事情,它都会执行(或不执行)的原则。如果你没有编写一些错误处理程序,那么机器会因为没有错误处理程序而“假设”你真的想做你告诉它做的荒谬的、容易崩溃的事情。

              (酸参考:http://en.wikipedia.org/wiki/ACID

              【讨论】:

              • 我看不出 ACID 属性与您所描述的内容之间有任何联系。请解释连接...
              猜你喜欢
              • 1970-01-01
              • 2014-11-15
              • 2014-01-15
              • 1970-01-01
              • 2010-12-15
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多