49

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

例如,考虑这个(人为的)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()(因为intC#中的类型是32位有符号整数,列表中的值之和超过了最大32位有符号整数的值)。sum 变量的值为 -294967296(不是 4000000000);这很可能不是 sumList 方法的(假设的)开发人员想要的。

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

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

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

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

4

9 回答 9

43

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

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

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

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

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

于 2008-09-20T17:15:15.730 回答
26

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

于 2008-09-19T16:55:49.297 回答
16

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

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

}

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

于 2008-09-19T17:01:33.197 回答
8

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

C 语言没有任何捕获的概念,除非您计算信号。

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

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

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

于 2008-09-19T18:12:24.290 回答
7

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

于 2008-09-19T16:57:42.007 回答
6

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

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

于 2008-09-19T16:56:57.983 回答
4

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

于 2008-09-19T16:59:59.893 回答
1

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

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

From a practical standpoint, most programs don't care about precisely when overflows occur, so much as they need to guarantee that they don't produce erroneous results as a consequence of overflow. Unfortunately, programming languages' integer-overflow-detection semantics have not caught up with what would be necessary to let compilers produce efficient code.

于 2020-03-17T21:31:43.873 回答
-4

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

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

于 2008-09-19T16:59:17.447 回答