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

252

Je lisais le ArrayListcode source de Java et j'ai remarqué des comparaisons dans les instructions if.

Dans Java 7, la méthode grow(int)utilise

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

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

if (newCapacity < minCapacity)
    newCapacity = minCapacity;

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

Je pourrais imaginer que comparer avec zéro est plus rapide, mais effectuer une soustraction complète juste pour vérifier s'il est négatif me semble un peu exagéré. Toujours en termes de bytecode, cela impliquerait deux instructions ( ISUBet IF_ICMPGE) au lieu d'une ( IFGE).

dejvuth
la source
35
@Tunaki Quoi de if (newCapacity - minCapacity < 0)mieux qu'en if (newCapacity < minCapacity)termes de prévention des débordements?
Eran
3
Je me demande si le débordement de signe mentionné est bien la raison. La soustraction semble plutôt être un candidat au débordement. Le composant peut-être dire "cela ne débordera pas", les deux variables étant peut-être non négatives.
Joop Eggen
12
Pour info, vous pensez que faire une comparaison est plus rapide que d'effectuer une "soustraction complète". D'après mon expérience, au niveau du code machine, les comparaisons sont généralement effectuées en effectuant une soustraction, en jetant le résultat et en vérifiant les indicateurs résultants.
David Dubois du
6
@David Dubois: l'OP n'a pas supposé que la comparaison est plus rapide que la soustraction, mais cette comparaison avec zéro pourrait être plus rapide qu'une comparaison de deux valeurs arbitraires et suppose également correctement que cela ne tient pas lorsque vous effectuez d'abord une soustraction réelle afin d'obtenir une valeur à comparer avec zéro. C'est tout à fait raisonnable.
Holger

Réponses:

285

a < bet a - b < 0peut signifier deux choses différentes. Considérez 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");
}

Lors de l'exécution, cela ne fera qu'imprimer a - b < 0. Ce qui se passe, c'est a < bclairement faux, mais a - bdéborde et devient -1, ce qui est négatif.

Maintenant, cela dit, considérez que le tableau a une longueur qui est vraiment proche de Integer.MAX_VALUE. Le code ArrayListva 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);

oldCapacityest vraiment proche de Integer.MAX_VALUEsi newCapacity( ce qui est oldCapacity + 0.5 * oldCapacity) risque de déborder et de devenir Integer.MIN_VALUE(c. -à- négatif). Ensuite, la soustraction des minCapacity sous-flux revient en un nombre positif.

Cette vérification garantit que le ifn'est pas exécuté. Si le code était écrit en tant que if (newCapacity < minCapacity), ce serait truedans ce cas (car il newCapacityest négatif) donc le newCapacityserait forcé à minCapacityindépendamment de oldCapacity.

Ce cas de débordement est géré par le prochain if. Quand newCapacitya débordé, ce sera true: MAX_ARRAY_SIZEest défini comme Integer.MAX_VALUE - 8et Integer.MIN_VALUE - (Integer.MAX_VALUE - 8) > 0est true. Le newCapacityest donc correctement géré: la hugeCapacityméthode retourne MAX_ARRAY_SIZEou Integer.MAX_VALUE.

NB: c'est ce que dit le // overflow-conscious codecommentaire de cette méthode.

Tunaki
la source
8
Bonne démo sur la différence entre les mathématiques et CS
piggybox
36
@piggybox Je ne le dirais pas. Ce sont des maths. Ce n'est tout simplement pas des mathématiques en Z, mais dans une version des entiers modulo 2 ^ 32 (avec les représentations canoniques choisies différemment que d'habitude). C'est un bon système mathématique, pas seulement "des ordinateurs lol et leurs bizarreries".
harold
2
J'aurais écrit du code qui ne débordait pas du tout.
Aleksandr Dubinsky
Les processeurs IIRC implémentent une instruction inférieure à sur les entiers signés en faisant a - bet en vérifiant si le bit supérieur est a 1. Comment gèrent-ils le débordement?
Ben Leggiero
2
@ BenC.R.Leggiero x86, entre autres, assure le suivi de diverses conditions via des indicateurs d'état dans un registre distinct à utiliser avec des instructions conditionnelles. Ce registre a des bits séparés pour le signe du résultat, la zérosité du résultat et si un dépassement / dépassement de capacité s'est produit lors de la dernière opération arithmétique.
105

J'ai trouvé cette explication :

Le mar 9 mars 2010 à 3 h 02, Kevin L. Stern a écrit:

J'ai fait une recherche rapide et il semble que Java soit en effet basé sur le complément à deux. Néanmoins, permettez-moi de souligner que, en général, ce type de code m'inquiète car je m'attends à ce qu'à un moment donné quelqu'un vienne et fasse exactement ce que Dmytro a suggéré; c'est-à-dire que quelqu'un va changer:

if (a - b > 0)

à

if (a > b)

et le navire entier coulera. Personnellement, j'aime éviter les obscurités telles que faire du débordement d'entier une base essentielle pour mon algorithme, sauf s'il y a une bonne raison de le faire. Je préférerais en général éviter tout débordement et 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.

En ArrayListnous ne pouvons pas le faire (ou du moins pas de manière compatible), car il ensureCapacitys'agit d'une API publique et accepte effectivement déjà les nombres négatifs comme des demandes d'une capacité positive qui ne peuvent pas être satisfaites.

L'API actuelle est utilisée comme ceci:

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

Si vous voulez éviter le débordement, vous devrez changer pour quelque chose de moins naturel comme

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

Quoi qu'il en soit, je garde le code conscient des débordements, mais j'ajoute plus de commentaires d'avertissement et je «souligne» la création de tableaux énormes pour que ArrayListle code 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 s'est régénéré.

Martin

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

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

Et les newCountdébordements (cela devient négatif), if (minCapacity > oldCapacity)retourneront faux et vous pouvez supposer à tort que le a ArrayListété augmenté de len.

Eran
la source
2
Belle idée mais elle est contredite par la mise en œuvre deensureCapacity ; s'il minCapacityest négatif, vous n'y arrivez jamais - il est tout aussi ignoré silencieusement que l'implémentation compliquée prétend l'empêcher. Donc "nous ne pouvons pas faire cela" pour la compatibilité des API publiques est un argument étrange comme ils l'ont déjà fait. Les seuls appelants s'appuyant sur ce comportement sont les internes.
Holger
1
@Holger If minCapacityest très négatif (c'est-à-dire résultant d'un intdébordement lors de l'ajout de la taille actuelle de la liste de tableaux au nombre d'éléments que vous souhaitez ajouter), minCapacity - elementData.lengthpour déborder à nouveau et devenir positif. Voilà comment je le comprends.
Eran
1
@Holger Cependant, ils l'ont à nouveau changé en Java 8 if (minCapacity > minExpand), ce que je ne comprends pas.
Eran
Oui, les deux addAllméthodes sont le seul cas où elles sont pertinentes car la somme de la taille actuelle et du nombre de nouveaux éléments peut déborder. Néanmoins, ce sont des appels internes et l'argument «nous ne pouvons pas le changer parce que ensureCapacityc'est une API publique» est un argument étrange alors qu'en fait, ensureCapacityil ignore les valeurs négatives. L'API Java 8 n'a pas changé ce comportement, elle ne fait qu'ignorer les capacités inférieures à la capacité par défaut lorsque le ArrayListest dans son état initial (c'est-à-dire initialisé avec la capacité par défaut et toujours vide).
Holger
En d'autres termes, le raisonnement sur newcount = count + lenest correct en ce qui concerne l'utilisation interne, cependant, il ne s'applique pas à la publicméthode ensureCapacity()...
Holger
19

En regardant le code:

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

Si oldCapacityest assez grand, cela débordera et newCapacitysera un nombre négatif. Une comparaison comme celle- newCapacity < oldCapacityci n'évaluera pas correctement trueet ArrayListne parviendra pas à croître.

Au lieu de cela, le code tel qu'il est écrit ( newCapacity - minCapacity < 0retourne false) permettra à la valeur négative de newCapacityd'être évaluée plus avant dans la ligne suivante, ce qui entraînera un recalcul newCapacityen appelant hugeCapacity( newCapacity = hugeCapacity(minCapacity);) pour permettre la ArrayListcroissance de MAX_ARRAY_SIZE.

C'est ce que le // overflow-conscious codecommentaire essaie de communiquer, mais de manière plutôt oblique.

Donc, en fin de compte, la nouvelle comparaison protège contre l'attribution d'un ArrayList plus grand que le prédéfini MAX_ARRAY_SIZEtout en lui permettant de croître jusqu'à cette limite si nécessaire.

Erick G. Hagstrom
la source
1

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

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

Doradus
la source