web-dev-qa-db-fra.com

Différence entre if (a - b <0) et if (a <b)

Je lisais le code source ArrayList de Java et j'ai remarqué certaines comparaisons dans les déclarations if.

En Java 7, la méthode grow(int) uses

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

En Java 6, grow n'existait pas. La méthode ensureCapacity(int) utilise cependant

if (newCapacity < minCapacity)
    newCapacity = minCapacity;

Quelle était la raison derrière le changement? Était-ce un problème de performance ou juste un style? 

J'imagine que comparer avec zéro est plus rapide, mais effectuer une soustraction complète pour vérifier si elle est négative me semble un peu exagéré. Toujours en termes de code binaire, cela impliquerait deux instructions (ISUB et IF_ICMPGE) au lieu d'une (IFGE).

229
dejvuth

a < b et a - b < 0 peuvent signifier deux choses différentes. Considérons le code suivant:

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

Lorsqu'il est exécuté, ceci imprimera seulement a - b < 0. Ce qui se passe, c'est que a < b est clairement faux, mais a - b déborde et devient -1, ce qui est négatif.

Cela dit, considérons que la longueur du tableau est très proche de Integer.MAX_VALUE. Le code dans ArrayList va comme ceci:

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 est vraiment proche de Integer.MAX_VALUE donc newCapacity (qui est oldCapacity + 0.5 * oldCapacity) pourrait déborder et devenir Integer.MIN_VALUE (c’est-à-dire négatif). Ensuite, soustrayez minCapacity underflow retour en un nombre positif.

Cette vérification garantit que la if n'est pas exécutée. Si le code était écrit avec if (newCapacity < minCapacity), il s'agirait de true dans ce cas (puisque newCapacity est négatif), de sorte que newCapacity serait forcé à minCapacity sans tenir compte de oldCapacity.

Ce cas de débordement est traité par le prochain if. Lorsque newCapacity a débordé, ce sera true: MAX_ARRAY_SIZE est défini comme Integer.MAX_VALUE - 8 et Integer.MIN_VALUE - (Integer.MAX_VALUE - 8) > 0 est true. La variable newCapacity est donc correctement gérée: la méthode hugeCapacity renvoie MAX_ARRAY_SIZE ou Integer.MAX_VALUE.

NB: c'est ce que dit le commentaire // overflow-conscious code dans cette méthode.

257
Tunaki

J'ai trouvé cette explication :

Le mardi 9 mars 2010 à 03h02, Kevin L. Stern a écrit: 

J'ai fait une recherche rapide et il apparaît que Java est en effet un complément à deux basé. Néanmoins, permettez-moi de souligner que, de manière générale, ceci Ce type de code m'inquiète car je m'attends vraiment à ce que quelqu'un finisse par le faire viens et fais exactement ce que Dmytro a suggéré; c'est-à-dire que quelqu'un le fera changement:

if (a - b > 0)

à

if (a > b)

et tout le navire va couler. Personnellement, j'aime éviter les obscurités comme faire du débordement d’entier une base essentielle de mon algorithme, sauf si il y a une bonne raison de le faire. Je préférerais en général éviter débordement total et pour rendre le scénario de débordement plus explicite:

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

C'est un bon point.

Dans ArrayList nous ne pouvons pas faire cela (ou du moins ne pas être compatible), car ensureCapacity est une API publique et accepte effectivement déjà nombres négatifs en tant que demandes de capacité positive qui ne peuvent pas être satisfait.

L'API actuelle est utilisée comme ceci:

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

Si vous voulez éviter les débordements, vous devrez changer quelque chose moins naturel comme

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

Quoi qu'il en soit, je garde le code trop sensible au débordement, mais j'ajoute plus de commentaires d'avertissement et "esquisse" la création d'un tableau énorme de telle sorte que Le code de ArrayList ressemble maintenant à:

/**
 * 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 régénéré.

Martin

En Java 6, si vous utilisez l'API en tant que:

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

Et newCount déborde (cela devient négatif), if (minCapacity > oldCapacity) retournera false et vous pouvez supposer à tort que la ArrayList a été augmentée de len.

92
Eran

En regardant le code:

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

Si oldCapacity est assez grand, cela débordera et newCapacity sera un nombre négatif. Une comparaison comme newCapacity < oldCapacity n'évaluera pas correctement true et la variable ArrayList ne pourra pas croître.

Au lieu de cela, le code tel qu'il est écrit (newCapacity - minCapacity < 0 renvoie false) permettra d'évaluer davantage la valeur négative de newCapacity à la ligne suivante, ce qui entraînera le recalcul de newCapacity en appelant hugeCapacity (newCapacity = hugeCapacity(minCapacity);) pour permettre à ArrayList de grandir à MAX_ARRAY_SIZE.

C’est ce que le commentaire // overflow-conscious code tente de communiquer, bien que de manière oblique.

Ainsi, au final, la nouvelle comparaison protège contre l’allocation d’une ArrayList plus grande que le MAX_ARRAY_SIZE prédéfini, tout en lui permettant de croître jusqu’à cette limite si nécessaire.

16
Erick G. Hagstrom

Les deux formes se comportent exactement de la même façon, sauf si l'expression a - b déborde, auquel cas elles sont opposées. Si a est un grand négatif et b est un grand positif, alors (a < b) est clairement vrai, mais a - b débordera pour devenir positif, donc (a - b < 0) est faux.

Si vous connaissez le code d'assemblage x86, considérez que (a < b) est implémenté par une jge, qui se branche autour du corps de l'instruction if lorsque SF = OF. D'autre part, (a - b < 0) agira comme une jns, qui se branche lorsque SF = 0. Par conséquent, ils se comportent différemment, précisément lorsque OF = 1.

0
Doradus