Differenza tra if (a - b <0) e if (a <b)


252

Stavo leggendo il ArrayListcodice sorgente di Java e ho notato alcuni confronti nelle dichiarazioni if.

In Java 7, il metodo grow(int)utilizza

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

In Java 6, grownon esisteva. Il metodo ensureCapacity(int)tuttavia utilizza

if (newCapacity < minCapacity)
    newCapacity = minCapacity;

Qual è stato il motivo del cambiamento? È stato un problema di prestazioni o solo uno stile?

Potrei immaginare che il confronto con lo zero sia più veloce, ma eseguire una sottrazione completa solo per verificare se è negativo mi sembra un po 'eccessivo. Anche in termini di bytecode, ciò implicherebbe due istruzioni ( ISUBe IF_ICMPGE) anziché una ( IFGE).


35
@Tunaki Come è if (newCapacity - minCapacity < 0)meglio che if (newCapacity < minCapacity)in termini di prevenzione del trabocco?
Eran,

3
Mi chiedo se il segno di overflow menzionato sia davvero il motivo. La sottrazione sembra più un candidato per l'overflow. Il componente forse dice "questo non sarà comunque troppo pieno", forse entrambe le variabili non sono negative.
Joop Eggen,

12
Cordiali saluti, credi che fare un confronto sia più veloce che eseguire una "sottrazione completa". Nella mia esperienza, a livello di codice macchina, di solito i confronti vengono eseguiti eseguendo una sottrazione, eliminando il risultato e controllando i flag risultanti.
David Dubois,

6
@David Dubois: l'OP non ha ipotizzato che il confronto sia più veloce della sottrazione, ma quel confronto con zero potrebbe essere più veloce di un confronto di due valori arbitrari e presume inoltre che ciò non valga quando si esegue prima una sottrazione effettiva per ottenere un valore da confrontare con zero. È abbastanza ragionevole.
Holger,

Risposte:


285

a < be a - b < 0può significare due cose diverse. Considera il seguente codice:

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

Quando eseguito, verrà stampato solo a - b < 0. Quello che succede è a < bchiaramente falso, ma a - btrabocca e diventa -1, il che è negativo.

Ora, detto questo, considera che l'array ha una lunghezza molto vicina Integer.MAX_VALUE. Il codice ArrayListentra in questo modo:

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è molto vicino a Integer.MAX_VALUEcosì newCapacity(che è oldCapacity + 0.5 * oldCapacity) potrebbe traboccare e diventare Integer.MIN_VALUE(cioè negativo). Quindi, sottraendo i minCapacity underflow ritorna in un numero positivo.

Questo controllo assicura che ifnon sia eseguito. Se il codice fosse scritto come if (newCapacity < minCapacity), sarebbe truein questo caso (poiché newCapacityè negativo), quindi newCapacitysarebbe costretto a minCapacityprescindere da oldCapacity.

Questo caso di overflow viene gestito dal successivo if. Quando newCapacitysarà traboccato, questo sarà true: MAX_ARRAY_SIZEè definito come Integer.MAX_VALUE - 8ed Integer.MIN_VALUE - (Integer.MAX_VALUE - 8) > 0è true. Il newCapacityè quindi giustamente gestito: hugeCapacitymetodo restituisce MAX_ARRAY_SIZEo Integer.MAX_VALUE.

NB: questo è ciò che // overflow-conscious codedice il commento in questo metodo.


8
Buona demo sulla differenza tra matematica e CS
piggybox

36
@piggybox Non lo direi. Questa è matematica. Non è solo matematica in Z, ma in una versione degli interi modulo 2 ^ 32 (con le rappresentazioni canoniche scelte in modo diverso dal solito). È un vero sistema matematico, non solo "lol computer e le loro stranezze".
Harold,

2
Avrei scritto un codice che non traboccasse affatto.
Aleksandr Dubinsky,

I processori IIRC implementano un'istruzione minore di sugli interi con segno eseguendo a - be controllando se il bit superiore è a 1. Come gestiscono l'overflow?
Ben Leggiero,

2
@ BenC.R.Leggiero x86, tra gli altri, tiene traccia di varie condizioni tramite flag di stato in un registro separato da utilizzare con istruzioni condizionali. Questo registro ha bit separati per il segno del risultato, la zeronessa del risultato e se si è verificato un over / underflow nell'ultima operazione aritmetica.

105

Ho trovato questa spiegazione :

Martedì 9 marzo 2010 alle 03:02, Kevin L. Stern ha scritto:

Ho fatto una ricerca veloce e sembra che Java sia davvero basato sul complemento di due. Tuttavia, mi permetta di sottolineare che, in generale, questo tipo di codice mi preoccupa poiché mi aspetto pienamente che a un certo punto qualcuno verrà e farà esattamente ciò che Dmytro ha suggerito; cioè qualcuno cambierà:

if (a - b > 0)

per

if (a > b)

e l'intera nave affonderà. Personalmente, mi piace evitare oscurità come rendere il trabocco di numeri interi una base essenziale per il mio algoritmo, a meno che non ci siano buone ragioni per farlo. In generale, preferirei evitare del tutto l'overflow e rendere più esplicito lo scenario di overflow:

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

È un buon punto.

In ArrayListnon possiamo farlo (o almeno non in modo compatibile), perché ensureCapacityè un'API pubblica e accetta effettivamente numeri negativi come richieste per una capacità positiva che non può essere soddisfatta.

L'API corrente viene utilizzata in questo modo:

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

Se si desidera evitare il trabocco, è necessario passare a qualcosa di meno naturale

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

Ad ogni modo, sto mantenendo il codice attento all'overflow, ma aggiungendo altri commenti di avvertimento e "creando" un'enorme creazione di array in modo che ArrayListil codice ora assomigli a:

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

balestruccio

In Java 6, se si utilizza l'API come:

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

E newCounttrabocca (questo diventa negativo), if (minCapacity > oldCapacity)restituirà falso e potresti erroneamente supporre che sia ArrayListstato aumentato di len.


2
Bella idea ma è contraddetta dalla realizzazione diensureCapacity ; se minCapacityè negativo, non arriverai mai a quel punto - è silenziosamente ignorato come la complicata implementazione pretende di prevenire. Quindi "non possiamo farlo" per la compatibilità delle API pubbliche è un argomento strano come già hanno fatto. Gli unici chiamanti che fanno affidamento su questo comportamento sono quelli interni.
Holger,

1
@Holger Se minCapacityè molto negativo (ovvero è il risultato di un intoverflow quando si aggiunge la dimensione corrente di ArrayList al numero di elementi che si desidera aggiungere), minCapacity - elementData.lengthper overflow di nuovo e diventare positivo. Ecco come lo capisco.
Eran,

1
@Holger Tuttavia, l'hanno cambiato di nuovo in Java 8, a if (minCapacity > minExpand), che non capisco.
Eran,

Sì, i due addAllmetodi sono l'unico caso in cui è rilevante in quanto la somma della dimensione corrente e il numero di nuovi elementi possono traboccare. Tuttavia, si tratta di chiamate interne e l'argomento "non possiamo cambiarlo perché ensureCapacityè un'API pubblica" è uno strano argomento quando in realtà ensureCapacityignora i valori negativi. L'API Java 8 non ha modificato questo comportamento, tutto ciò che fa è ignorare le capacità al di sotto della capacità predefinita quando ArrayListè nello stato iniziale (ovvero inizializzato con capacità predefinita e ancora vuoto).
Holger,

In altre parole, il ragionamento newcount = count + lenè corretto quando si tratta dell'uso interno, tuttavia, non si applica al publicmetodo ensureCapacity()...
Holger,

19

Guardando il codice:

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

Se oldCapacityè abbastanza grande, questo traboccerà e newCapacitysarà un numero negativo. Un confronto come newCapacity < oldCapacityverrà valutato in modo errato truee ArrayListnon riuscirà a crescere.

Al contrario, il codice scritto ( newCapacity - minCapacity < 0restituisce false) consentirà di newCapacityvalutare ulteriormente il valore negativo di nella riga successiva, risultando nel ricalcolo newCapacityinvocando hugeCapacity( newCapacity = hugeCapacity(minCapacity);) per consentire la ArrayListcrescita MAX_ARRAY_SIZE.

Questo è ciò che il // overflow-conscious codecommento sta cercando di comunicare, anche se in modo piuttosto obliquo.

Quindi, in conclusione, il nuovo confronto protegge dall'allocazione di un valore ArrayListsuperiore a quello predefinito MAX_ARRAY_SIZE, consentendo al contempo di crescere fino a quel limite, se necessario.


1

Le due forme si comportano esattamente allo stesso modo a meno che l'espressione non a - btrabocchi, nel qual caso sono opposte. Se aè un grande negativo ed bè un grande positivo, allora (a < b)è chiaramente vero, ma a - btraboccerà per diventare positivo, quindi (a - b < 0)è falso.

Se hai familiarità con il codice assembly x86, considera che (a < b)è implementato da a jge, che si ramifica attorno al corpo dell'istruzione if quando SF = OF. D'altra parte, (a - b < 0)agirà come un jns, che si ramifica quando SF = 0. Quindi, questi si comportano in modo diverso precisamente quando OF = 1.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.