if(a-b <0)和if(a <b)之间的差异


252

我正在阅读Java的ArrayList源代码,并注意到if语句中的一些比较。

在Java 7中,该方法grow(int)使用

if (newCapacity - minCapacity < 0)
    newCapacity = minCapacity;

在Java 6 grow中不存在。ensureCapacity(int)但是该方法使用

if (newCapacity < minCapacity)
    newCapacity = minCapacity;

更改背后的原因是什么?是性能问题还是风格?

我可以想象与零进行比较会更快,但是对我来说,执行完全减法只是为了检查它是否为负数似乎有点过大。同样在字节码方面,这将涉及两个指令(ISUBIF_ICMPGE)而不是一个指令(IFGE)。


35
@Tunaki有if (newCapacity - minCapacity < 0)什么比if (newCapacity < minCapacity)防止溢出更好的了?
伊兰2015年

3
我不知道上述标志是否确实是原因。减法似乎更适合溢出。该组件可能会说“尽管如此,它不会溢出”,也许两个变量都是非负数。
朱普·艾根

12
仅供参考,您认为进行比较比执行“完全减法”要快。以我的经验,在机器代码级别上,通常通过执行减法,丢弃结果并检查结果标志来进行比较。
大卫·杜波依斯

6
@David Dubois:OP并未假设比较的速度比减法要快,但是与零的比较可能比对两个任意值的比较要快,并且还正确地假设在您首先执行实际减法时这不成立为了得到一个与零比较的值。那是很合理的。
Holger 2015年

Answers:


285

a < b并且a - b < 0可能意味着两个不同的东西。考虑以下代码:

int a = Integer.MAX_VALUE;
int b = Integer.MIN_VALUE;
if (a < b) {
    System.out.println("a < b");
}
if (a - b < 0) {
    System.out.println("a - b < 0");
}

运行时,将仅打印a - b < 0。发生的事情a < b显然是错误的,但是a - b溢出并变为-1,这是负面的。

话虽如此,请考虑一下数组的长度确实接近Integer.MAX_VALUE。中的代码ArrayList如下所示:

int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
    newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
    newCapacity = hugeCapacity(minCapacity);

oldCapacity确实接近,Integer.MAX_VALUE所以newCapacity(是oldCapacity + 0.5 * oldCapacity)可能溢出并变成Integer.MIN_VALUE(即负数)。然后,将minCapacity 下溢相减回正数。

此检查确保if不会执行。如果代码编写为if (newCapacity < minCapacity),则true在这种情况下(因为它newCapacity为负),所以无论否newCapacity都将强制minCapacity执行oldCapacity

此溢出情况由下一个if处理。当newCapacity已经溢出,这将是trueMAX_ARRAY_SIZE被定义为Integer.MAX_VALUE - 8Integer.MIN_VALUE - (Integer.MAX_VALUE - 8) > 0true。将newCapacity因此被正确地处理:hugeCapacity方法返回MAX_ARRAY_SIZEInteger.MAX_VALUE

注意:这就是// overflow-conscious code此方法中的注释。


8
关于数学和CS的区别的很好的演示
Pigbox

36
@piggybox我不会这么说。这是数学。它不是Z中的数学运算,而是2模32的整数模(标准表示的选择方式与平时不同)。这是一个适当的数学系统,而不仅仅是“大声笑的计算机及其怪癖”。
哈罗德

2
我会编写根本不会溢出的代码。
Aleksandr Dubinsky

IIRC处理器通过执行a - b并检查最高位是否为,对有符号整数执行小于指令1。他们如何处理溢出?
Ben Leggiero 2015年

2
@ BenC.R.Leggiero x86等可通过单独寄存器中的状态标志跟踪各种条件,以与条件指令一起使用。该寄存器具有单独的位,用于表示结果的符号,结果的零以及上次算术运算中是否发生上溢/下溢。

105

我找到了这个解释

在2010年3月9日,星期二,03:02,凯文·斯特恩(Kevin L. Stern)写道:

我进行了快速搜索,看来Java确实是基于二进制补码的。但是,请允许我指出,总的来说,这种类型的代码使我感到担忧,因为我完全希望在某个时候有人会来做并完全按照Dmytro的建议进行。也就是说,有人会改变:

if (a - b > 0)

if (a > b)

整个船会沉没 我个人希望避免混淆,例如使整数溢出成为我算法的必要基础,除非有充分的理由这样做。通常,我宁愿完全避免溢出,并使溢出场景更加明确:

if (oldCapacity > RESIZE_OVERFLOW_THRESHOLD) {
   // Do something
} else {
  // Do something else
}

这是一个好点。

ArrayList我们不能做到这一点(或至少不兼容),因为 ensureCapacity是一个公共的API,有效地接受已经为负数的不能满足一个积极的容量请求。

当前的API的用法如下:

int newcount = count + len;
ensureCapacity(newcount);

如果要避免溢出,则需要更改为不太自然的内容,例如

ensureCapacity(count, len);
int newcount = count + len;

无论如何,我保留了对溢出敏感的代码,但添加了更多警告注释,并“概述了”巨大的数组创建,因此 ArrayList现在的代码如下所示:

/**
 * Increases the capacity of this <tt>ArrayList</tt> instance, if
 * necessary, to ensure that it can hold at least the number of elements
 * specified by the minimum capacity argument.
 *
 * @param minCapacity the desired minimum capacity
 */
public void ensureCapacity(int minCapacity) {
    modCount++;

    // Overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

/**
 * The maximum size of array to allocate.
 * Some VMs reserve some header words in an array.
 * Attempts to allocate larger arrays may result in
 * OutOfMemoryError: Requested array size exceeds VM limit
 */
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

/**
 * Increases the capacity to ensure that it can hold at least the
 * number of elements specified by the minimum capacity argument.
 *
 * @param minCapacity the desired minimum capacity
 */
private void grow(int minCapacity) {
    // Overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);

    // minCapacity is usually close to size, so this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity);
}

private int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();
    return (minCapacity > MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE :
        MAX_ARRAY_SIZE;
}

Webrev重新生成。

马丁

在Java 6中,如果将API用作:

int newcount = count + len;
ensureCapacity(newcount);

并且newCount溢出(变为负数),if (minCapacity > oldCapacity)将返回false,并且您可能会错误地假定的ArrayList增大了len


2
不错的主意,但与执行ensureCapacity矛盾; 如果minCapacity为负,那么您永远都无法理解到这一点,就像复杂的实现假装要防止的那样,它被默默地忽略了。因此,为了实现公共API兼容性,“我们不能这样做”是一个奇怪的说法,就像他们已经做的那样。唯一依赖此行为的调用者是内部调用者。
Holger

1
@Holger If minCapacity是非常负的(即,由于int将ArrayList的当前大小添加到希望添加的元素数时minCapacity - elementData.length溢出), 再次溢出并变为正数。这就是我的理解。
伊兰2015年

1
@Holger但是,他们在Java 8中再次将其更改为if (minCapacity > minExpand),我不明白。
伊兰2015年

是的,这两种addAll方法是唯一相关的情况,因为当前大小的总和和新元素的数量可能会溢出。但是,这些是内部调用,而“我们不能更改它,因为ensureCapacity是公共API”这样的参数实际上ensureCapacity却忽略了负值,这是一个奇怪的参数。Java 8 API并没有改变这种行为,它所做的只是忽略ArrayList处于初始状态时的容量(低于默认容量)(即,使用默认容量初始化并且仍然为空)。
Holger 2015年

换句话说,关于newcount = count + len内部使用的推理是正确的,但是,它不适用于该public方法ensureCapacity()……
Holger 2015年

19

看一下代码:

int newCapacity = oldCapacity + (oldCapacity >> 1);

如果oldCapacity很大,则将溢出,并且newCapacity为负数。像这样的比较将无法newCapacity < oldCapacity正确评估true,并且ArrayList将无法增长。

取而代之的是,编写的代码(newCapacity - minCapacity < 0返回false)将允许newCapacity在下一行中进一步评估的负值,从而导致newCapacity通过调用hugeCapacitynewCapacity = hugeCapacity(minCapacity);)进行重新计算,以允许ArrayList增长到MAX_ARRAY_SIZE

这是// overflow-conscious code评论试图传达的内容,尽管有点倾斜。

因此,最重要的是,新的比较可以防止分配ArrayList比预定义更大的值,MAX_ARRAY_SIZE同时允许它根据需要增长到该限制。


1

除非表达式a - b溢出,否则这两种形式的行为完全相同,在这种情况下它们是相反的。如果a是一个大的负数,而又b是一个大的正数,那么(a < b)显然是正确的,但是a - b会溢出变成正数,所以(a - b < 0)是错误的。

如果您熟悉x86汇编代码,请考虑(a < b)由x 实现,jge当SF = OF时,它将在if语句的主体周围分支。另一方面,当SF = 0时,(a - b < 0)它将像a一样jns分支。因此,当OF = 1时,它们的行为恰好不同。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.