为什么语言默认情况下不会在整数溢出上引发错误?

时间:2008-09-19 16:53:11

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

在几种现代编程语言(包括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 BigIntegerchecked关键字和/checked的类型编译器在C#中切换。

然而,我感兴趣的问题是为什么这些语言默认设计为允许整数溢出首先发生,而不是例如在运行时执行操作时引发异常导致溢出。看起来这种行为有助于避免在编写执行可能导致溢出的算术运算的代码时开发人员忽略解释溢出可能性的情况下的错误。 (这些语言可能包含类似“未经检查”的关键字,它可以指定允许发生整数溢出的块,而不会引发异常,在开发人员明确表示该行为的情况下; C#实际上{{3} }。)

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

除了性能考虑之外,还有其他原因可以做出这种语言设计决定吗?

9 个答案:

答案 0 :(得分:37)

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

当C#是新的时,微软希望很多C ++开发人员能够切换到它。他们知道许多C ++人认为C ++是快速的,尤其比那些“浪费”在自动内存管理等方面的语言更快。

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

他们的测试表明C#比本机编译的C ++慢,这种事情可以让人们迅速摆脱C#。您的C#应用​​程序将自动捕获溢出/下溢的事实是他们可能会错过的事情。所以,它默认是关闭的。

我认为很明显我们想要/检查的时间是99%。这是一个不幸的妥协。

答案 1 :(得分:26)

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

答案 2 :(得分:15)

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

有时整数溢出是期望的行为。我见过的一个例子是将绝对航向值表示为固定点数。给定unsigned int,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;

}

可能还有其他情况可以接受溢出,类似于此示例。

答案 3 :(得分:8)

C / C ++永远不会要求陷阱行为。即使是明显的除以0也是C ++中未定义的行为,而不是指定类型的陷阱。

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

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

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

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

答案 4 :(得分:7)

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

答案 5 :(得分:6)

可能性能达到99%。在x86上,必须检查每个操作的溢出标志,这将是一个巨大的性能损失。

另外1%将覆盖人们正在进行奇特的位操作或混合有符号和无符号操作并且想要溢出语义的“不精确”的情况。

答案 6 :(得分:4)

向后兼容性是一个很大的问题。使用C,假设您对数据类型的大小给予足够的重视,如果发生上溢/下溢,那就是您想要的。然后使用C ++,C#和Java,“内置”数据类型的工作方式几乎没有变化。

答案 7 :(得分:0)

如果将整数溢出定义为立即引发信号,引发异常或以其他方式使程序执行偏离,则任何可能溢出的计算都需要按指定的顺序执行。即使在整数溢出检查不会直接花费任何东西的平台上,整数溢出被严格限制在程序执行序列中正确位置的要求也会严重阻碍许多有用的优化。

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

从实用的角度来看,大多数程序都不在乎溢出何时发生,而是需要确保它们不会由于溢出而产生错误的结果。不幸的是,编程语言的整数溢出检测语义还​​没有赶上让编译器产生有效代码所必需的条件。

答案 8 :(得分:-4)

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

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