提问人:Jon Schneider 提问时间:9/20/2008 最后编辑:phuclvJon Schneider 更新时间:11/18/2023 访问量:5796
为什么语言默认不会在整数溢出时引发错误?
Why don't languages raise errors on integer overflow by default?
问:
在几种现代编程语言(包括 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#实际上确实有这个。
答案是否简单地归结为性能 - 语言设计者不希望他们各自的语言默认具有“慢速”算术整数运算,其中运行时需要做额外的工作来检查是否发生了溢出,在每个适用的算术运算上 - 这种性能考虑超过了在无意中发生溢出的情况下避免“静默”故障的价值?
除了性能考虑之外,此语言设计决策是否还有其他原因?
答:
我认为性能是一个很好的理由。如果你考虑一个典型程序中增加一个整数的每条指令,如果不是简单的运算加 1,它必须检查每次加 1 是否会溢出类型,那么额外周期的成本将非常严重。
评论
它可能是 99% 的性能。在 x86 上,必须检查每个操作的溢出标志,这将是一个巨大的性能打击。
另外 1% 将涵盖人们在进行花哨的位操作或在混合有符号和无符号操作时“不精确”并希望溢出语义的情况。
因为检查溢出需要时间。每个原始数学运算(通常转换为单个汇编指令)都必须包括溢出检查,从而导致多个汇编指令,从而可能导致程序速度慢几倍。
我对为什么在运行时默认情况下不会引发错误的理解归结为希望创建具有类似 ACID 行为的编程语言的遗产。具体来说,就是你编码它要做(或不编码)的任何事情,它都会做(或不做)。如果你没有编写一些错误处理程序,那么机器就会“假设”,因为你没有错误处理程序,你真的想做你告诉它做的荒谬的、容易崩溃的事情。
(ACID 参考:http://en.wikipedia.org/wiki/ACID)
评论
向后兼容性是一个很大的问题。对于 C,假设你对数据类型的大小给予了足够的关注,如果发生上溢/下溢,这就是你想要的。然后,在C++,C#和Java中,“内置”数据类型的工作方式几乎没有变化。
您假设整数溢出始终是不希望的行为。
有时,整数溢出是所需的行为。我见过的一个例子是将绝对航向值表示为定点数。给定一个无符号整数,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;
}
可能还有其他情况下,溢出是可以接受的,类似于此示例。
评论
C/C++ 从不强制要求陷阱行为。即使是明显的除以 0 也是 C++ 中未定义的行为,而不是一种指定的陷阱。
C 语言没有任何陷阱的概念,除非你计算信号。
C++ 有一个设计原则,即它不会引入 C 中不存在的开销,除非你要求它。因此,Stroustrup 不想强制要求整数的行为方式需要任何显式检查。
一些早期的编译器和受限硬件的轻量级实现根本不支持异常,并且通常可以使用编译器选项禁用异常。强制要求语言内置的例外是有问题的。
即使 C++ 检查了整数,早期 99% 的程序员也会为了性能提升而关闭......
在 C# 中,这是一个性能问题。具体来说,就是开箱即用的基准测试。
当 C# 刚刚出现时,Microsoft 希望很多 C++ 开发人员能够切换到它。他们知道许多 C++ 人认为 C++ 速度很快,尤其是比那些在自动内存管理等上“浪费”时间的语言更快。
潜在的采用者和杂志审稿人都可能得到一份新 C# 的副本,安装它,构建一个在现实世界中没有人会编写的琐碎应用程序,在紧密循环中运行它,并测量它花费了多长时间。然后他们会为他们的公司做出决定,或者根据该结果发表一篇文章。
事实上,他们的测试表明 C# 比本地编译的 C++ 慢,这种事情会让人们很快关闭 C#。事实上,你的 C# 应用将自动捕获溢出/下溢,这是他们可能会错过的那种事情。因此,默认情况下它是关闭的。
我认为很明显,我们希望/检查的 99% 的时间都处于开启状态。这是一个不幸的妥协。
评论
readonly rect foo;
DoSomething(foo.X, foo.Y
foo
X
foo
Y
/checked
如果整数溢出被定义为立即引发信号、抛出异常或以其他方式偏离程序执行,那么任何可能溢出的计算都需要按指定的顺序执行。即使在整数溢出检查不会直接花费任何费用的平台上,要求将整数溢出捕获在程序执行序列中的正确点也会严重阻碍许多有用的优化。
如果一种语言指定整数溢出将设置一个锁定错误标志,则限制函数中对该标志的操作如何影响其在调用代码中的值,并规定在溢出不会导致错误输出或行为的情况下无需设置该标志, 然后,编译器可以生成比任何类型的手动溢出检查程序员都更有效的代码。举个简单的例子,如果一个 C 语言中的函数将两个数字相乘并返回一个结果,在溢出时设置一个错误标志,那么无论调用者是否会使用结果,都需要编译器来执行乘法。然而,在像我所描述的那样具有更宽松规则的语言中,如果编译器确定没有任何东西使用乘法的结果,则可以推断溢出不会影响程序的输出,并完全跳过乘法。
从实际的角度来看,大多数程序并不关心溢出的确切时间,而是需要保证它们不会因溢出而产生错误的结果。不幸的是,编程语言的整数溢出检测语义还没有赶上让编译器生成高效代码所需的内容。
K&R,Stroustrup,编译器和库的作者以及所有C,C++,Java,JS用户的工作保障。静态分析行业,书籍,很长的课程,咨询。我很容易错过许多其他人。
麦克利普在 1970-1971 年左右拥有任意精度的算术和有理数......为什么在LISP中,没有数量限制?
评论
下一个:为什么原位突变用“IO”表示?
评论