Різниця між if (a - b <0) та if (a <b)


252

Я читав ArrayListвихідний код Java і помітив деякі порівняння у 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).


35
@Tunaki Як if (newCapacity - minCapacity < 0)краще, ніж if (newCapacity < minCapacity)в плані запобігання переливу?
Еран

3
Цікаво, чи справді згаданий знак переповнення справді є причиною. Віднімання здається більше кандидатом на перелив. Компонент, можливо, говорить "це все-таки не переповниться", можливо, обидві змінні невід'ємні.
Joop Eggen

12
FYI, ти вважаєш, що робити порівняння швидше, ніж виконувати "повне віднімання". З мого досвіду, на рівні машинного коду порівняння зазвичай проводяться шляхом віднімання, викидання результату та перевірки отриманих прапорів.
Девід Дюбуа

6
@David Dubois: ОП не вважав, що порівняння відбувається швидше, ніж віднімання, але порівняння з нулем може бути швидшим порівняння двох довільних значень, а також правильно припускає, що це не має значення, коли ви вперше виконуєте фактичне віднімання. щоб отримати значення для порівняння з нулем. Це все цілком розумно.
Хольгер

Відповіді:


285

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 підтіків повертається в додатне число.

Ця перевірка гарантує, що ifвона не виконується. Якби код був написаний так if (newCapacity < minCapacity), він був би trueв цьому випадку (оскільки newCapacityє негативним), тому newCapacityвін буде змушений робити minCapacityнезалежно від oldCapacity.

Цей випадок переповнення обробляється наступним, якщо. Коли 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.

NB: Про це говорить // overflow-conscious codeкоментар у цьому методі.


8
Хороша демонстрація різниці між математикою та CS
скарбничка

36
@piggybox Я б не сказав цього. Це математика. Це просто не математика в Z, а у версії цілого числа за модулем 2 ^ 32 (з канонічними поданнями, обраними інакше, ніж зазвичай). Це належна математична система, а не лише "lol комп'ютери та їхні диваки".
harold

2
Я б написав код, який зовсім не переповнювався.
Олександр Дубінський

Процесори IIRC реалізують меншу, ніж інструкцію щодо підписаних цілих чисел, виконуючи a - bта перевіряючи, чи верхній біт є a 1. Як вони справляються з переливом?
Ben Leggiero

2
@ BenC.R.Leggiero x86, серед інших, відстежує різні умови за допомогою прапорів статусу в окремому реєстрі для використання з умовними інструкціями. У цьому регістрі є окремі біти для ознаки результату, нульовості результату та того, чи відбулося переповнення / підтікання в останній арифметичній операції.

105

Я знайшов таке пояснення :

У вівторок, 9 березня 2010 року о 03:02, Кевін Л. Стерн написав:

Я швидко здійснив пошук, і, здається, що Java справді заснована на двох. Тим не менш, дозвольте мені зазначити, що в цілому цей тип коду хвилює мене, оскільки я повністю сподіваюся, що в якийсь момент хтось підійде і зробить саме те, що запропонував Дмитро; тобто хтось змінить:

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;

У будь-якому випадку, я зберігаю переповнюваний код, але додаю більше попереджувальних коментарів та створюю величезний масив "накреслюючи" так, що 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)повернеться помилковим, і ви можете помилково припустити, що значення ArrayListбуло збільшене на len.


2
Хороша ідея, але вона суперечить реалізаціїensureCapacity ; якщо minCapacityце негативно, ви ніколи не дістанетесь до цього пункту - це так само мовчки ігнорується, як і складне втілення в життя запобігає. Тож “ми не можемо цього зробити” для сумісності з громадськими API - це дивний аргумент, як це вже було. Єдині, хто покликав цю поведінку, - це внутрішні.
Холгер

1
@Holger Якщо minCapacityдуже негативний (тобто виник внаслідок intпереповнення при додаванні поточного розміру ArrayList до кількості елементів, які ви хотіли додати), minCapacity - elementData.lengthзнову переповнюйте та станьте позитивними. Ось як я це розумію.
Еран

1
@Holger Однак вони знову змінили її в Java 8, на if (minCapacity > minExpand)яку я не розумію.
Еран

Так, два addAllспособи - єдиний випадок, коли він доречний, оскільки сума поточного розміру та кількості нових елементів може переповнюватись. Тим не менш, це внутрішні виклики, і аргумент «ми не можемо змінити його, оскільки ensureCapacityце публічний API» - це дивний аргумент, коли насправді ensureCapacityвін ігнорує негативні значення. API Java 8 не змінив такої поведінки. Все, що вона робить, - це ігнорування потужностей, що не перевищують ємність за замовчуванням, коли вона ArrayListзнаходиться у початковому стані (тобто ініціалізована за замовчуванням та ще порожня).
Холгер

Іншими словами, міркування про newcount = count + lenце правильне, коли мова йде про внутрішнє використання, однак воно не застосовується до publicметоду ensureCapacity()
Holger

19

Дивлячись на код:

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

Якщо oldCapacityвін досить великий, це переповниться, і newCapacityце буде від’ємне число. Порівняння на зразок newCapacity < oldCapacityбуде неправильно оцінено, trueі ArrayListволя не зможе зростати.

Натомість код, записаний ( newCapacity - minCapacity < 0повертає помилкове), дозволить newCapacityдодатково оцінити негативне значення у наступному рядку, в результаті чого перерахунок newCapacityза допомогою виклику hugeCapacity( newCapacity = hugeCapacity(minCapacity);) дозволить ArrayListзростати до MAX_ARRAY_SIZE.

Це те, що // overflow-conscious codeкоментар намагається донести, хоча досить косо.

Отже, підсумок , нове порівняння захищає від виділення ArrayListбільшого за попередньо визначене MAX_ARRAY_SIZE, дозволяючи йому зростати аж до цієї межі, якщо потрібно.


1

Дві форми поводяться абсолютно однаково, якщо вираз не a - bпереповнює, і в цьому випадку вони протилежні. Якщо aвеликий негатив і bвеликий позитив, то (a < b)це явно вірно, але a - bпереповниться, щоб стати позитивним, так (a - b < 0)це помилково.

Якщо ви знайомі з кодом асемблери x86, врахуйте, що (a < b)реалізований a jge, який розгалужується навколо тіла оператора if, коли SF = OF. З іншого боку, він (a - b < 0)буде діяти як a jns, який розгалужується, коли SF = 0. Отже, вони поводяться по-різному точно, коли OF = 1.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.