if (a - b < 0) 和 if (a < b) 之间的区别
Posted
技术标签:
【中文标题】if (a - b < 0) 和 if (a < b) 之间的区别【英文标题】:Difference between if (a - b < 0) and if (a < b) 【发布时间】:2016-01-13 20:10:37 【问题描述】:我正在阅读 Java 的 ArrayList
源代码并注意到 if 语句中的一些比较。
在 Java 7 中,方法 grow(int)
使用
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
在 Java 6 中,grow
不存在。但是ensureCapacity(int)
方法使用
if (newCapacity < minCapacity)
newCapacity = minCapacity;
改变背后的原因是什么?是性能问题还是风格问题?
我可以想象与零进行比较会更快,但是为了检查它是否为负而执行完全减法对我来说似乎有点矫枉过正。同样在字节码方面,这将涉及两条指令(ISUB
和 IF_ICMPGE
)而不是一条(IFGE
)。
【问题讨论】:
@Tunaki 在防止溢出方面if (newCapacity - minCapacity < 0)
比if (newCapacity < minCapacity)
更好吗?
不知道上面提到的符号溢出是否确实是原因。减法似乎更适合溢出。组件可能会说“这不会溢出”,可能两个变量都是非负数。
仅供参考,您认为进行比较比执行“完全减法”更快。根据我的经验,在机器代码级别,通常通过执行减法、丢弃结果并检查结果标志来完成比较。
@David Dubois:OP 没有假设比较比减法快,但是 与零的比较 可能比两个任意值的比较更快,而且也是正确的假设当您首先执行实际减法以获得与零比较的值时,这不成立。这一切都很合理。
【参考方案1】:
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
underflows 减回正数。
此检查确保不执行if
。如果代码写成if (newCapacity < minCapacity)
,在这种情况下它将是true
(因为newCapacity
是负数)所以newCapacity
将被强制为minCapacity
,而不管oldCapacity
。
这种溢出情况由下一个 if 处理。当newCapacity
溢出时,这将是true
:MAX_ARRAY_SIZE
被定义为Integer.MAX_VALUE - 8
并且Integer.MIN_VALUE - (Integer.MAX_VALUE - 8) > 0
是true
。因此,newCapacity
得到了正确处理:hugeCapacity
方法返回 MAX_ARRAY_SIZE
或 Integer.MAX_VALUE
。
注意:这就是此方法中的// overflow-conscious code
评论的意思。
【讨论】:
关于数学和 CS 区别的好演示 @piggybox 我不会这么说的。这是数学。这不是 Z 中的数学,而是整数模 2^32 的版本(规范表示的选择与通常不同)。这是一个适当的数学系统,而不仅仅是“lol 计算机及其怪癖”。 我会编写完全不会溢出的代码。 IIRC 处理器通过执行a - b
并检查最高位是否为1
来实现对有符号整数的小于指令。他们如何处理溢出?
@BenC.R.Leggiero x86 等通过单独寄存器中的状态标志跟踪各种条件,以便与条件指令一起使用。该寄存器对结果的符号、结果的零以及最后一次算术运算中是否发生上溢/下溢具有单独的位。【参考方案2】:
我找到this explanation:
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;
无论如何,我保留了溢出意识代码,但添加了更多 警告 cmets,并“勾画”巨大的数组创建,以便
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
。
【讨论】:
好主意,但与the implementation ofensureCapacity
相矛盾;如果minCapacity
是否定的,你永远不会到达那个点——它就像复杂的实现假装阻止的那样被默默地忽略。因此,对于公共 API 兼容性,“我们不能这样做”是一个奇怪的论点,就像他们已经做过的那样。唯一依赖此行为的调用者是内部调用者。
@Holger 如果minCapacity
是非常负的(即在将ArrayList 的当前大小添加到您希望添加的元素数量时,int
溢出),minCapacity - elementData.length
再次溢出并变得积极。我是这么理解的。
@Holger 但是,他们在 Java 8 中再次将其更改为 if (minCapacity > minExpand)
,我不明白。
是的,这两个addAll
方法是唯一相关的情况,因为当前大小和新元素数量之和可能会溢出。然而,这些都是内部调用,“我们不能改变它,因为ensureCapacity
是一个公共 API”是一个奇怪的论点,而事实上,ensureCapacity
确实忽略了负值。 Java 8 API 没有改变这种行为,它所做的只是在 ArrayList
处于其初始状态(即使用默认容量初始化但仍为空)时忽略默认容量以下的容量。
也就是说newcount = count + len
的推理在内部使用上是正确的,但不适用于public
方法ensureCapacity()
...【参考方案3】:
看代码:
int newCapacity = oldCapacity + (oldCapacity >> 1);
如果oldCapacity
很大,则会溢出,newCapacity
将是一个负数。像newCapacity < oldCapacity
这样的比较将错误地评估true
,ArrayList
将无法增长。
相反,编写的代码(newCapacity - minCapacity < 0
返回 false)将允许在下一行中进一步评估 newCapacity
的负值,从而通过调用 hugeCapacity
(newCapacity = hugeCapacity(minCapacity);
)重新计算 newCapacity
让ArrayList
成长为MAX_ARRAY_SIZE
。
这就是// overflow-conscious code
评论试图传达的内容,虽然相当间接。
因此,最重要的是,新的比较可防止分配大于预定义 MAX_ARRAY_SIZE
的 ArrayList
,同时允许它在需要时增长到该限制。
【讨论】:
【参考方案4】:这两种形式的行为完全相同,除非表达式a - b
溢出,在这种情况下它们是相反的。如果a
是大负数,b
是大正数,那么(a < b)
显然是真的,但a - b
会溢出变成正数,所以(a - b < 0)
是假的。
如果您熟悉 x86 汇编代码,请考虑 (a < b)
是由 jge
实现的,它在 SF = OF 时围绕 if 语句的主体进行分支。另一方面,(a - b < 0)
的行为类似于jns
,当 SF = 0 时会出现分支。因此,当 OF = 1 时,它们的行为会有所不同。
【讨论】:
以上是关于if (a - b < 0) 和 if (a < b) 之间的区别的主要内容,如果未能解决你的问题,请参考以下文章