Diferencia entre if (a - b <0) y if (a <b)

252

Estaba leyendo el ArrayListcódigo fuente de Java y noté algunas comparaciones en sentencias if.

En Java 7, el método grow(int)usa

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

En Java 6, growno existía. Sin ensureCapacity(int)embargo, el método utiliza

if (newCapacity < minCapacity)
    newCapacity = minCapacity;

¿Cuál fue la razón detrás del cambio? ¿Fue un problema de rendimiento o simplemente un estilo?

Me imagino que comparar con cero es más rápido, pero realizar una resta completa solo para verificar si es negativo me parece un poco exagerado. También en términos de bytecode, esto implicaría dos instrucciones ( ISUBy IF_ICMPGE) en lugar de una ( IFGE).

dejvuth
fuente
35
@Tunaki ¿Cómo es if (newCapacity - minCapacity < 0)mejor que if (newCapacity < minCapacity)en términos de prevención de desbordamiento?
Eran
3
Me pregunto si el desbordamiento de signo mencionado es realmente la razón. La resta parece más un candidato para el desbordamiento. El componente puede decir "sin embargo, esto no se desbordará", quizás ambas variables no sean negativas.
Joop Eggen
12
Para su información, usted cree que hacer una comparación es más rápido que realizar una "resta completa". En mi experiencia, a nivel de código de máquina, generalmente las comparaciones se realizan realizando una resta, tirando el resultado y verificando las banderas resultantes.
David Dubois
66
@David Dubois: el OP no asumió que la comparación es más rápida que la resta, pero esa comparación con cero podría ser más rápida que la comparación de dos valores arbitrarios y también supone correctamente que esto no se cumple cuando está realizando una resta real primero para obtener un valor para comparar con cero. Eso es todo bastante razonable.
Holger

Respuestas:

285

a < by a - b < 0puede significar dos cosas diferentes. Considere el siguiente código:

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");
}

Cuando se ejecuta, esto solo se imprimirá a - b < 0. Lo que sucede es que a < bes claramente falso, pero se a - bdesborda y se vuelve -1negativo.

Ahora, dicho esto, considere que la matriz tiene una longitud muy cercana Integer.MAX_VALUE. El código en ArrayListva así:

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

oldCapacityestá realmente cerca de Integer.MAX_VALUElo newCapacityque (que es oldCapacity + 0.5 * oldCapacity) podría desbordarse y volverse Integer.MIN_VALUE(es decir, negativo). Luego, restando los minCapacity subflujos nuevamente en un número positivo.

Esta verificación asegura que ifno se ejecute. Si el código se escribiera comoif (newCapacity < minCapacity) , sería trueen este caso (ya que newCapacityes negativo), por lo que newCapacityse vería obligado a hacerlo minCapacityindependientemente de oldCapacity.

Este caso de desbordamiento es manejado por el siguiente if. Cuando se newCapacityha desbordado, esto será true: MAX_ARRAY_SIZEse define como Integer.MAX_VALUE - 8y Integer.MIN_VALUE - (Integer.MAX_VALUE - 8) > 0estrue . Por newCapacitylo tanto, se maneja correctamente: el hugeCapacitymétodo devuelve MAX_ARRAY_SIZEo Integer.MAX_VALUE.

NB: esto es lo que dice el // overflow-conscious codecomentario en este método.

Tunaki
fuente
8
Buena demostración sobre la diferencia entre matemáticas y CS
piggybox
36
@piggybox No lo diría. Esto es matemática. Simplemente no es matemática en Z, sino en una versión de los enteros módulo 2 ^ 32 (con las representaciones canónicas elegidas de manera diferente a la habitual). Es un sistema matemático adecuado, no solo "lol computadoras y sus peculiaridades".
harold
2
Hubiera escrito un código que no se desbordara en absoluto.
Aleksandr Dubinsky
Los procesadores IIRC implementan una instrucción menor que en enteros firmados haciendo a - by verificando si el bit superior es a 1. ¿Cómo manejan el desbordamiento?
Ben Leggiero
2
@ BenC.R.Leggiero x86, entre otros, rastrea varias condiciones a través de indicadores de estado en un registro separado para usar con instrucciones condicionales. Este registro tiene bits separados para el signo del resultado, zeroness del resultado y si se produjo un flujo excesivo / insuficiente en la última operación aritmética.
105

Encontré esta explicación :

El martes 9 de marzo de 2010 a las 03:02, Kevin L. Stern escribió:

Hice una búsqueda rápida y parece que Java está basado en el complemento de dos. No obstante, permítame señalar que, en general, este tipo de código me preocupa, ya que espero que en algún momento alguien venga y haga exactamente lo que Dmytro sugirió; es decir, alguien cambiará:

if (a - b > 0)

a

if (a > b)

y toda la nave se hundirá. Personalmente, me gusta evitar las obscuridades, como hacer que el desbordamiento de enteros sea una base esencial para mi algoritmo, a menos que haya una buena razón para hacerlo. En general, preferiría evitar el desbordamiento por completo y hacer que el escenario de desbordamiento sea más explícito:

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

Es un buen punto.

En ArrayListno podemos hacer esto (o al menos no es compatible), porque ensureCapacity es una API pública y efectivamente ya acepta números negativos como solicitudes de una capacidad positiva que no puede satisfacerse.

La API actual se usa así:

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

Si desea evitar el desbordamiento, deberá cambiar a algo menos natural como

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

De todos modos, mantengo el código consciente del desbordamiento, pero agrego más comentarios de advertencia y crea una gran variedad de "esquemas" para que ArrayListel código ahora se vea así:

/**
 * 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 regenerado.

Martín

En Java 6, si usa la API como:

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

Y los newCountdesbordamientos (esto se vuelve negativo), if (minCapacity > oldCapacity)devolverán falso y puede suponer erróneamente que ArrayListse incrementó en len.

Eran
fuente
2
Buena idea, pero está en contradicción con la implementación deensureCapacity ; si minCapacityes negativo, nunca se llega a ese punto, se ignora tan silenciosamente como la complicada implementación pretende evitar. Entonces, "no podemos hacer esto" para la compatibilidad de API pública es un argumento extraño como ya lo hicieron. Las únicas personas que llaman confiando en este comportamiento son las internas.
Holger
1
@Holger If minCapacityes muy negativo (es decir, como resultado del intdesbordamiento al agregar el tamaño actual de ArrayList a la cantidad de elementos que desea agregar), minCapacity - elementData.lengthpara desbordarse nuevamente y volverse positivo. Así lo entiendo.
Eran
1
@Holger Sin embargo, lo cambiaron nuevamente en Java 8, a if (minCapacity > minExpand), lo cual no entiendo.
Eran
Sí, los dos addAllmétodos son el único caso en el que es relevante ya que la suma del tamaño actual y el número de elementos nuevos pueden desbordarse. Sin embargo, estas son llamadas internas y el argumento "no podemos cambiarlo porque ensureCapacityes una API pública" es un argumento extraño cuando, de hecho, ensureCapacityignora los valores negativos. La API de Java 8 no cambió ese comportamiento, todo lo que hace es ignorar las capacidades por debajo de la capacidad predeterminada cuando ArrayListestá en su estado inicial (es decir, inicializado con capacidad predeterminada y aún vacío).
Holger
En otras palabras, el razonamiento newcount = count + lenes correcto cuando se trata del uso interno, sin embargo, no se aplica al publicmétodo ensureCapacity()...
Holger
19

Mirando el código:

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

Si oldCapacityes bastante grande, se desbordará y newCapacityserá un número negativo. Una comparación como newCapacity < oldCapacityse evaluará incorrectamente truey ArrayListno crecerá.

En cambio, el código tal como está escrito ( newCapacity - minCapacity < 0devuelve falso) permitirá que el valor negativo de newCapacityse evalúe más en la siguiente línea, lo que resulta en recalcular newCapacityinvocando hugeCapacity( newCapacity = hugeCapacity(minCapacity);) para permitir ArrayListque crezca MAX_ARRAY_SIZE.

Esto es lo que el // overflow-conscious codecomentario intenta comunicar, aunque de manera bastante oblicua.

Por lo tanto, en resumen, la nueva comparación protege contra la asignación de un valor ArrayListmayor que el predefinido, al MAX_ARRAY_SIZEtiempo que le permite crecer hasta ese límite si es necesario.

Erick G. Hagstrom
fuente
1

Las dos formas se comportan exactamente igual a menos que la expresión se a - bdesborde, en cuyo caso son opuestas. Si aes un gran negativo, y bes un gran positivo, entonces (a < b)es claramente cierto, pero a - bse desbordará para volverse positivo, por lo que (a - b < 0)es falso.

Si está familiarizado con el código de ensamblaje x86, considere que (a < b)lo implementa a jge, que se ramifica alrededor del cuerpo de la instrucción if cuando SF = OF. Por otro lado, (a - b < 0)actuará como a jns, que se ramifica cuando SF = 0. Por lo tanto, estos se comportan de manera diferente precisamente cuando OF = 1.

Doradus
fuente