Разница между 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).

dejvuth
источник
35
@Tunaki Как if (newCapacity - minCapacity < 0)лучше, чем if (newCapacity < minCapacity)с точки зрения предотвращения переполнения?
Eran
3
Интересно, является ли упомянутое переполнение знака действительно причиной? Вычитание кажется более вероятным кандидатом на переполнение. Компонент может сказать «это, тем не менее, не будет переполнено», возможно, обе переменные неотрицательны.
Joop Eggen
12
К вашему сведению, вы считаете, что сравнение выполняется быстрее, чем «полное вычитание». По моему опыту, на уровне машинного кода обычно сравнения выполняются путем вычитания, отбрасывания результата и проверки полученных флагов.
Дэвид Дюбуа
6
@ Дэвид Дюбуа: ОП не предполагал, что сравнение происходит быстрее, чем вычитание, но сравнение с нулем может быть быстрее, чем сравнение двух произвольных значений, а также правильно предполагает, что это не выполняется, когда вы выполняете фактическое вычитание первым чтобы получить значение для сравнения с нулем. Это все вполне разумно.
Хольгер

Ответы:

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.

Этот случай переполнения обрабатывается следующим if. При 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комментарий в этом методе.

Tunaki
источник
8
Хорошая демонстрация разницы между математикой и CS
piggybox
36
@piggybox Я бы так не сказал. Это математика Это просто не математика в Z, а в версии целых чисел по модулю 2 ^ 32 (канонические представления выбираются не так, как обычно). Это правильная математическая система, а не просто "LOL компьютеры и их причуды".
Гарольд
2
Я бы написал код, который не переполнялся вообще.
Александр Дубинский
Процессоры IIRC реализуют инструкцию меньше чем для целых чисел со знаком, выполняя a - bи проверяя, является ли старший бит a 1. Как они справляются с переполнением?
Бен Легжеро
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;
}

Вебрев восстановлен.

Мартин

В Java 6, если вы используете API как:

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

И newCountпереполнения (это становится отрицательным), if (minCapacity > oldCapacity)вернет false, и вы можете ошибочно предположить, что ArrayListбыл увеличен на len.

Эран
источник
2
Хорошая идея, но она противоречит реализацииensureCapacity ; если minCapacityоно отрицательное, вы никогда не достигнете этой точки - оно так же тихо игнорируется, как и сложная реализация, которая пытается предотвратить. Так что «мы не можем этого сделать» для совместимости с публичным API - странный аргумент, как они уже сделали. Единственные абоненты, полагающиеся на это поведение, являются внутренними.
Хольгер
1
@Holger Если значение minCapacityочень отрицательное (т. Е. Возникло из-за intпереполнения при добавлении текущего размера ArrayList к числу элементов, которые вы хотите добавить), minCapacity - elementData.lengthснова переполниться и стать положительным. Вот как я это понимаю.
Eran
1
@Holger Тем не менее, они изменили его снова в Java 8, на if (minCapacity > minExpand)что я не понимаю.
Eran
Да, эти два addAllметода являются единственным случаем, когда это уместно, так как сумма текущего размера и количества новых элементов может переполниться. Тем не менее, это внутренние вызовы, и аргумент «мы не можем его изменить, потому что ensureCapacityэто публичный API» является странным аргументом, когда фактически ensureCapacityигнорирует отрицательные значения. API-интерфейс Java 8 не изменил это поведение, все, что он делает, это игнорирует возможности ниже емкости по умолчанию, когда он ArrayListнаходится в своем начальном состоянии (то есть инициализирован с емкостью по умолчанию и все еще пуст).
Хольгер
Другими словами, рассуждение о newcount = count + lenтом, что правильно, когда речь идет о внутреннем использовании, однако, оно не относится к publicметоду ensureCapacity()...
Хольгер
19

Глядя на код:

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

Если oldCapacityоно достаточно велико, оно будет переполнено и newCapacityбудет отрицательным числом. Сравнение вроде newCapacity < oldCapacityбудет неправильно оценивать trueи ArrayListрасти не будет.

Вместо этого код в том виде, в котором он написан ( newCapacity - minCapacity < 0возвращает false), позволит newCapacityдополнительно оценить отрицательное значение в следующей строке, что приведет к повторному вычислению с newCapacityпомощью invoking 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.

Doradus
источник