代码之家  ›  专栏  ›  技术社区  ›  Jon Schneider Stefan

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

  •  51
  • Jon Schneider Stefan  · 技术社区  · 17 年前

    在几种现代编程语言(包括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() 方法(因为 int C#中的类型是一个32位有符号整数,列表中的值之和超过了最大32位有字符整数的值)。sum变量的值为-294967296(不是4000000000的值);这很可能不是sumList方法的(假设的)开发人员所期望的。

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

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

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

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

    9 回复  |  直到 10 年前
        1
  •  40
  •   Jay Bazuzi Buck Hodges    17 年前

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

    当C#是新的时候,微软希望很多C++开发人员会转而使用它。他们知道,许多C++人认为C++很快,尤其是比那些在自动内存管理等方面“浪费”时间的语言快。

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

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

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

        2
  •  26
  •   David Hill    17 年前

    我认为表现是一个很好的理由。如果你考虑一个典型程序中递增整数的每条指令,如果不是简单的加1操作,而是每次都要检查加1是否会溢出类型,那么额外周期的成本将非常严重。

        3
  •  16
  •   phuclv    10 年前

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

    有时整数溢出是理想的行为。我见过的一个例子是将绝对航向值表示为定点数。给定一个无符号整数,0是0或360度,最大32位无符号整数(0xffffff)是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
  •  8
  •   Steve Jessop    14 年前

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

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

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

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

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

        5
  •  7
  •   Dima    17 年前

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

        6
  •  6
  •   Rob Walker    17 年前

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

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

        7
  •  4
  •   Salman A    17 年前

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

        8
  •  0
  •   supercat    6 年前

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

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

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

        9
  •  -4
  •   devinmoore    17 年前

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

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

    推荐文章