为什么语言默认不会在整数溢出时引发错误?

Why don't languages raise errors on integer overflow by default?

提问人:Jon Schneider 提问时间:9/20/2008 最后编辑:phuclvJon Schneider 更新时间:11/18/2023 访问量:5796

问:

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

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

//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);

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

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

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

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

除了性能考虑之外,此语言设计决策是否还有其他原因?

与语言无关 语言设计 整数溢出

评论

0赞 user894319twitter 11/18/2023
示例不应包含函数、列表和循环等不相关的内容。只需将 2 个整数常量加在一起就足够了。

答:

26赞 David Hill 9/20/2008 #1

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

评论

3赞 Thilo 3/24/2017
当操作溢出时,CPU 不会给你一个标志吗?(当然,检查该标志也需要时间)。
1赞 phuclv 8/14/2017
@Thilo有些 CPU 没有任何标志,例如 MIPS。即使对它们进行简单的 big int 操作也是一种轻微的痛苦
6赞 Rob Walker 9/20/2008 #2

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

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

7赞 Dima 9/20/2008 #3

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

-4赞 devinmoore 9/20/2008 #4

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

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

评论

7赞 Stephen C 9/21/2009
我看不出 ACID 属性与您所描述的内容之间存在任何联系。请解释一下连接...
4赞 Eclipse 9/20/2008 #5

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

16赞 Doug T. 9/20/2008 #6

您假设整数溢出始终是不希望的行为。

有时,整数溢出是所需的行为。我见过的一个例子是将绝对航向值表示为定点数。给定一个无符号整数,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;

}

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

评论

14赞 Kibbee 9/20/2008
可能还有更多的例子,即整数溢出产生错误的结果,而不是当它发出正确的结果时。事实上,它真正产生正确结果的唯一时间是当它是预期的行为时,并且它实际上被用作一个功能,例如在您的示例中。
1赞 OwenP 9/20/2008
@Kibbee我愿意冒昧地说,在整数溢出可能导致错误的情况下,它更像是一个指标,表明你没有进行适当的范围检查,而不是编程语言的失败。在意外的情况下,代码应检查它。
2赞 Dan 12/17/2008
更不用说依赖像这样特定于平台的低级细节的固有危险了。如果在 64 位平台上重新编译,会发生什么情况?哎呀。
5赞 sleske 2/23/2010
@Dan:请注意,代码使用 uint32_t。保证是 32 位(因此得名:-))。所以作者确实想到了这一点。
3赞 Dan 2/27/2010
@sleske:如果你检查编辑,你会看到 Doug 的原始帖子使用了“unsigned int”......因此我的评论。也许我的评论使他纠正了他的代码......
8赞 Steve Jessop 9/20/2008 #7

C/C++ 从不强制要求陷阱行为。即使是明显的除以 0 也是 C++ 中未定义的行为,而不是一种指定的陷阱。

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

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

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

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

44赞 Jay Bazuzi 9/21/2008 #8

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

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

潜在的采用者和杂志审稿人都可能得到一份新 C# 的副本,安装它,构建一个在现实世界中没有人会编写的琐碎应用程序,在紧密循环中运行它,并测量它花费了多长时间。然后他们会为他们的公司做出决定,或者根据该结果发表一篇文章。

事实上,他们的测试表明 C# 比本地编译的 C++ 慢,这种事情会让人们很快关闭 C#。事实上,你的 C# 应用将自动捕获溢出/下溢,这是他们可能会错过的那种事情。因此,默认情况下它是关闭的。

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

评论

2赞 supercat 12/24/2012
整数溢出检查会对任何实际基准产生多大影响?在我看来,C#中还有许多其他更糟糕的事情(例如,给定一个字段,一个像);'需要复制,调用其访问器方法,创建另一个副本,并调用其访问器方法。相当大的开销 - 足以使整数溢出检查相比之下显得微不足道。readonly rect foo;DoSomething(foo.X, foo.YfooXfooY
1赞 Jay Bazuzi 12/24/2012
@supercat:在大多数现实世界的代码中,默认情况下打开不会明显变慢,并且会捕获某些类别的重要错误。但是,正如我在回答中所说,C# 的早期审阅者可能做的第一件事就是在紧密循环中测试整数算术的性能。/checked
8赞 Jay Bazuzi 12/24/2012
请记住,C# 现在已经有 12 年的历史了。当时,许多程序员认为“C++很快,Java很慢,C#就像Java”。我认为,今天,大多数程序员认为上市时间和管理复杂性对业务成功的影响比闭环优化更大。
0赞 markmnl 12/17/2014
不过,原因是什么?我个人发现自己经常依赖行为(即默认未选中),例如,我希望循环的序列计数器或数据库没有无符号值的类型,但它是无符号的,所以强制转换只是工作......
0赞 user894319twitter 11/17/2023
在大多数情况下,一个快速的错误答案不是性能,而是绝大多数应用程序中的一些骗局。
1赞 supercat 3/18/2020 #9

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

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

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

-2赞 user894319twitter 11/17/2023 #10

K&R,Stroustrup,编译器和库的作者以及所有C,C++,Java,JS用户的工作保障。静态分析行业,书籍,很长的课程,咨询。我很容易错过许多其他人。

麦克利普在 1970-1971 年左右拥有任意精度的算术和有理数......为什么在LISP中,没有数量限制?

评论

0赞 Nico Haase 11/17/2023
这如何回答最初的问题?