在几种现代编程语言(包括C++,Java和C#)中,该语言允许在运行时发生整数溢出,而不会引发任何类型的错误条件.
例如,考虑这个(人为的)C#方法,它没有考虑上溢/下溢的可能性.(为简洁起见,该方法也不处理指定列表为空引用的情况.)
//Returns the sum of the values in the specified list.
private static int sumList(List list)
{
int sum = 0;
foreach (int listItem in list)
{
sum += listItem;
}
return sum;
}
如果调用此方法如下:
List list = new List();
list.Add(2000000000);
list.Add(2000000000);
int sum = sumList(list);
sumList()
方法中将发生溢出(因为int
C#中的类型是32位有符号整数,并且列表中值的总和超过了最大32位有符号整数的值).sum变量的值为-294967296(不是值4000000000); 这很可能不是sumList方法的(假设的)开发人员所期望的.
显然,开发人员可以使用各种技术来避免整数溢出的可能性,例如使用类似Java的类型BigInteger
,或者使用C#中的checked
关键字和/checked
编译器开关.
但是,我感兴趣的问题是为什么这些语言默认设计为允许整数溢出首先发生,而不是例如在运行时执行操作时引发异常,从而导致溢出.看起来这种行为有助于避免在编写执行可能导致溢出的算术运算的代码时开发人员忽略解释溢出可能性的情况下的错误.(这些语言可能包含类似"未经检查"的关键字,它可以指定允许发生整数溢出而不会引发异常的块,在开发人员明确意图该行为的情况下; C#实际上确实有这个. )
答案简单归结为性能 - 语言设计者不希望他们各自的语言默认具有"慢"算术整数运算,其中运行时需要做额外的工作来检查是否发生溢出,在每个适用的算术上操作 - 这种性能考虑超过了在无意溢出发生时避免"无声"故障的价值?
除了性能考虑之外,还有其他原因可以做出这种语言设计决策吗?
在C#中,这是一个性能问题.具体而言,开箱即用的基准测试.
当C#是新的时,微软希望很多C++开发人员能够切换到它.他们知道许多C++人认为C++是快速的,尤其比那些"浪费"在自动内存管理等方面的语言更快.
潜在的采用者和杂志评论者都可能获得新C#的副本,安装它,构建一个无人能够在现实世界中编写的简单应用程序,在紧密的循环中运行它,并测量它花了多长时间.然后他们会为他们的公司做出决定或根据该结果发表文章.
事实上,他们的测试表明C#比本机编译的C++慢,这种事情可以让人们迅速摆脱C#.您的C#应用程序将自动捕获溢出/下溢的事实是他们可能会错过的事情.所以,它默认是关闭的.
我认为很明显我们想要/检查的时间是99%.这是一个不幸的妥协.
我认为表演是一个很好的理由.如果你考虑典型程序中的每个指令增加一个整数,并且如果不是简单的op加1,它必须每次检查如果添加1会溢出该类型,那么额外周期的成本将非常严重.
您假设整数溢出始终是不期望的行为.
有时整数溢出是期望的行为.我见过的一个例子是将绝对航向值表示为固定点数.给定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;
}
可能还有其他情况可以接受溢出,类似于此示例.
C/C++从不强制要求陷阱行为.即使明显除以0也是C++中未定义的行为,而不是指定类型的陷阱.
除非你计算信号,否则C语言没有任何陷阱概念.
C++有一个设计原则,它不会引入C中不存在的开销,除非你要求它.所以Stroustrup不希望强制整数的行为方式需要任何明确的检查.
一些早期编译器和受限硬件的轻量级实现根本不支持异常,并且通常可以使用编译器选项禁用异常.强制语言内置函数的异常将是有问题的.
即使C++已经对整数进行了检查,99%的程序员在早期就会因性能提升而关闭...
因为检查溢出需要时间.通常转换为单个汇编指令的每个原始数学运算都必须包括对溢出的检查,从而导致多个汇编指令,从而可能导致程序慢几倍.
这可能是99%的表现.在x86上必须检查每个操作的溢出标志,这将是一个巨大的性能损失.
另外1%将覆盖那些人们正在进行奇特的操作或者在混合有符号和无符号操作时"不精确"并且想要溢出语义的情况.