首页 文章

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

提问于
浏览
41

在几种现代编程语言(包括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方法的(假设的)开发人员所期望的 .

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

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

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

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

8 回答

  • 26

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

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

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

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

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

  • 8

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

  • 7

    您假设整数溢出始终是不期望的行为 .

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

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

  • -4

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

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

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

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

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

  • 7

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

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

  • 36

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

  • 5

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

  • 15

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

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

相关问题