Разница между 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)с точки зрения предотвращения переполнения?
— 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комментарий в этом методе.


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.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.