В Java может ли & быть быстрее, чем &&?

В этом коде:

if (value >= x && value <= y) {

когда value >= x и value <= y равновероятны как истина, так и ложь без определенного шаблона, будет ли использование оператора & быстрее, чем использование &&?

В частности, я думаю о том, как && лениво оценивает правостороннее выражение (т.е. только если LHS истинно), что подразумевает условное выражение, тогда как в Java & в этом контексте гарантируется строгая оценка обоих (логических) подвыражений . Результат значения одинаков в любом случае.

Но в то время как оператор >= или <= будет использовать простую инструкцию сравнения, && должен включать ветвь, и эта ветвь подвержена сбою предсказания ветвления - согласно этому очень известному вопросу: Почему быстрее обрабатывать отсортированный массив, чем несортированный массив?

Таким образом, принуждение выражения к отсутствию ленивых компонентов, безусловно, будет более детерминированным и не будет уязвимым для ошибки прогнозирования. Верно?

Примечания:

  • очевидно, ответ на мой вопрос был бы Нет, если бы код выглядел так: if(value >= x && verySlowFunction()). Я сосредоточусь на «достаточно простых» выражениях RHS.
  • в любом случае там есть условная ветвь (оператор if). Я не могу доказать себе, что это не имеет значения, и что альтернативные формулировки могут быть лучшими примерами, такими как boolean b = value >= x && value <= y;
  • все это попадает в мир ужасных микрооптимизаций. Да, я знаю :-) ... хотя интересно?

Обновление Просто чтобы объяснить, почему мне это интересно: я смотрю на системы, о которых писал Мартин Томпсон на своем блог механической симпатии, после того как он пришел и рассказал об Aeron. Один из ключевых выводов заключается в том, что в нашем оборудовании есть все эти волшебные вещи, и мы, разработчики программного обеспечения, трагически не можем воспользоваться ими. Не волнуйтесь, я не собираюсь использовать s/&&/\&/ по всему моему коду :-) ... но на этом сайте есть ряд вопросов по улучшению прогнозирования ветвлений путем удаления ветвей, и это произошло мне кажется, что условные булевы операторы лежат в основе тестовых условий.

Конечно, @StephenC делает фантастическое замечание о том, что преобразование вашего кода в странные формы может затруднить обнаружение JIT-компиляторами общих оптимизаций - если не сейчас, то в будущем. И что очень известный вопрос, упомянутый выше, особенный, потому что он выдвигает сложность прогнозирования далеко за пределы практической оптимизации.

Я в значительной степени осознаю, что в большинстве (или почти во всех) ситуациях && является самым ясным, простым, быстрым и лучшим решением, хотя я очень благодарен людям, которые разместили ответы. демонстрируя это! Мне действительно интересно узнать, есть ли в чьем-либо опыте случаи, когда ответ на вопрос «Может ли & быть быстрее?» может быть Да...

Обновление 2: (Учитывая рекомендацию о том, что вопрос является слишком широким. Я не хочу вносить серьезные изменения в этот вопрос, поскольку это может поставить под угрозу некоторые из приведенных ниже ответов, которые имеют исключительное качество. !) Возможно, требуется пример из дикой природы; это из Guava Класс LongMath (огромное спасибо @maaartinus за это):

public static boolean isPowerOfTwo(long x) {
    return x > 0 & (x & (x - 1)) == 0;
}

Видишь первого &? И если вы проверите ссылку, то увидите, что метод next называется lessThanBranchFree(...), что намекает на то, что мы находимся на территории избегания ответвлений, а гуава действительно широко используется: каждый сохраненный цикл вызывает заметное понижение уровня моря. Итак, давайте поставим вопрос так: является ли такое использование & (где && было бы более нормальным) настоящей оптимизацией?


person SusanW    schedule 20.09.2016    source источник
comment
Если и есть разница, то наносекунды. Это пахнет преждевременной оптимизацией. Почему это важно? Если вы действительно хотите знать, просто посмотрите на скомпилированный байт-код.   -  person Jim Garrison    schedule 20.09.2016
comment
@JimGarrison Это важно, потому что подобные тесты обычно используются в компараторах (то есть сортировке) и фильтрах, поэтому миллионы выполнений в узком цикле могут быть обычным явлением, а затем ns становится ms. Кроме того, строгая оценка оператора & является малоизвестной особенностью Java с точки зрения альтернативы &&, и за годы программирования на Java я ни разу не решился его использовать. Возможно, я был слишком пренебрежительным!   -  person SusanW    schedule 20.09.2016
comment
& ‹-- проверяет оба операнда && ‹-- прекращает оценку, если первый операнд оценивается как false, поскольку результат будет ложным. Поэтому я иногда нажимаю, && быстрее, чем &   -  person pavlos    schedule 20.09.2016
comment
@pavlos - я думал, что ясно дал понять это в вопросе (см. примечание verySlowFunction()); речь идет о предсказании ветвления - или мне следует уточнить это еще немного? Предложения приветствуются.   -  person SusanW    schedule 20.09.2016
comment
&& не обязательно ведет к ответвлению. В x86 есть условные инструкции. Но предсказатель ветвлений также очень хорош в современных процессорах. Почему бы вам не сравнить обе версии?   -  person rustyx    schedule 20.09.2016
comment
Не ориентируйтесь на JVM, если это уровень, на котором вы оптимизируете.   -  person    schedule 20.09.2016
comment
@Rhymoid абсолютно: меня больше волнует общий вопрос & против &&. Я всегда предполагал, что && полностью лучше, но предсказание ветвлений делает это сомнительным.   -  person SusanW    schedule 20.09.2016
comment
@RustyX Хорошо, условные инструкции - это начало хорошего ответа, поскольку это означает, что предиктор не имеет значения, пока RHS может выполняться условно - а это значит, что нас не волнует качество предиктора, верно?   -  person SusanW    schedule 20.09.2016
comment
FWIW, похоже, что & вместо && имеет реальное использование.   -  person maaartinus    schedule 20.09.2016
comment
@maaartinus Замечательно!! Хорошо - я думаю, мы нашли его! И он использует старый трюк с очисткой нижнего бита a&(a-1), который передавался с древних времен. Интересно, действительно ли это эффективно?   -  person SusanW    schedule 20.09.2016
comment
Компилятор C# будет генерировать код так, как если бы вы написали &, даже если вы написали &&, если его эвристика считает, что это будет победой. Я понятия не имею, делает ли то же самое компилятор Java, но это простая оптимизация, и было бы немного удивительно, если бы они не подумали об этом.   -  person Eric Lippert    schedule 20.09.2016
comment
@SusanW Возможно. Ребята обычно знают, что делают, и все тестируют, OTOH первое условие, вероятно, выполняется для большинства вызовов, а затем условный переход может выполняться параллельно с вычислением второго условия. +++ В случае отсутствия побочных эффектов я бы рассматривал & против && как подсказку для JVM о том, что делать, когда она не видит явного преимущества ни с одной из сторон. Не знаю, так ли это на самом деле.   -  person maaartinus    schedule 21.09.2016
comment
@EricLippert Обратите внимание, что использование & не всегда лучше. JVM существует, если ветку не легко предсказать. Когда я измерил его некоторое время назад, они не поняли его правильно. Существует порог вероятности ветвления, при котором условный переход (&&) выполняется быстрее, но обычно он очень низкий (4% в моем случае).   -  person maaartinus    schedule 21.09.2016


Ответы (7)


Итак, вы хотите знать, как он ведет себя на более низком уровне... Тогда давайте посмотрим на байт-код!

EDIT: в конец добавлен сгенерированный ассемблерный код для AMD64. Поищите интересные заметки.
EDIT 2 (re: OP Update 2): добавлен ассемблерный код для метод isPowerOfTwo Guava.< /эм>

исходный код Java

Я написал эти два быстрых метода:

public boolean AndSC(int x, int value, int y) {
    return value >= x && value <= y;
}

public boolean AndNonSC(int x, int value, int y) {
    return value >= x & value <= y;
}

Как видите, они абсолютно одинаковы, за исключением типа оператора AND.

Байт-код Java

А это сгенерированный байт-код:

  public AndSC(III)Z
   L0
    LINENUMBER 8 L0
    ILOAD 2
    ILOAD 1
    IF_ICMPLT L1
    ILOAD 2
    ILOAD 3
    IF_ICMPGT L1
   L2
    LINENUMBER 9 L2
    ICONST_1
    IRETURN
   L1
    LINENUMBER 11 L1
   FRAME SAME
    ICONST_0
    IRETURN
   L3
    LOCALVARIABLE this Ltest/lsoto/AndTest; L0 L3 0
    LOCALVARIABLE x I L0 L3 1
    LOCALVARIABLE value I L0 L3 2
    LOCALVARIABLE y I L0 L3 3
    MAXSTACK = 2
    MAXLOCALS = 4

  // access flags 0x1
  public AndNonSC(III)Z
   L0
    LINENUMBER 15 L0
    ILOAD 2
    ILOAD 1
    IF_ICMPLT L1
    ICONST_1
    GOTO L2
   L1
   FRAME SAME
    ICONST_0
   L2
   FRAME SAME1 I
    ILOAD 2
    ILOAD 3
    IF_ICMPGT L3
    ICONST_1
    GOTO L4
   L3
   FRAME SAME1 I
    ICONST_0
   L4
   FRAME FULL [test/lsoto/AndTest I I I] [I I]
    IAND
    IFEQ L5
   L6
    LINENUMBER 16 L6
    ICONST_1
    IRETURN
   L5
    LINENUMBER 18 L5
   FRAME SAME
    ICONST_0
    IRETURN
   L7
    LOCALVARIABLE this Ltest/lsoto/AndTest; L0 L7 0
    LOCALVARIABLE x I L0 L7 1
    LOCALVARIABLE value I L0 L7 2
    LOCALVARIABLE y I L0 L7 3
    MAXSTACK = 3
    MAXLOCALS = 4

Как и ожидалось, метод AndSC (&&) создает два условных перехода:

  1. Он загружает value и x в стек и переходит к L1, если value меньше. В противном случае он продолжает работать на следующих строках.
  2. Он загружает в стек value и y и также переходит к L1, если value больше. В противном случае он продолжает работать на следующих строках.
  3. Который оказывается return true на случай, если ни один из двух прыжков не был сделан.
  4. И затем у нас есть строки, отмеченные как L1, которые являются return false.

Однако метод AndNonSC (&) создает три условных перехода!

  1. Он загружает value и x в стек и переходит к L1, если value меньше. Поскольку теперь ему нужно сохранить результат, чтобы сравнить его с другой частью И, поэтому он должен выполнить либо сохранение true, либо сохранение false, он не может выполнить оба действия с одной и той же инструкцией.
  2. Он загружает value и y в стек и переходит к L1, если value больше. Еще раз нужно сохранить true или false, а это две разные строки в зависимости от результата сравнения.
  3. Теперь, когда оба сравнения выполнены, код фактически выполняет операцию И, и если оба они верны, он переходит (в третий раз) и возвращает значение true; или же он продолжает выполнение на следующей строке, чтобы вернуть false.

(Предварительный) Заключение

Хотя у меня не так много опыта работы с байт-кодом Java, и я мог что-то упустить из виду, мне кажется, что & на самом деле будет работать хуже, чем && в каждом случае: он генерирует больше инструкций для выполнения, включая больше условных переходов для прогнозирования и, возможно, неудачи.

Переписывание кода для замены сравнений арифметическими операциями, как предложил кто-то другой, может быть способом сделать & лучшим вариантом, но ценой того, что код станет намного менее понятным.
ИМХО, это не стоит того. хлопот для 99% сценариев (хотя это может быть очень хорошо для 1% циклов, которые необходимо чрезвычайно оптимизировать).

РЕДАКТИРОВАТЬ: сборка AMD64

Как отмечено в комментариях, один и тот же байт-код Java может привести к различному машинному коду в разных системах, поэтому, хотя байт-код Java может дать нам подсказку о том, какая версия AND работает лучше, получение фактического ASM, сгенерированного компилятором, является единственным способом. чтобы действительно выяснить это.
Я распечатал инструкции AMD64 ASM для обоих методов; ниже приведены соответствующие строки (зачищенные точки входа и т. д.).

ПРИМЕЧАНИЕ: все методы скомпилированы с помощью java 1.8.0_91, если не указано иное.

Метод AndSC с параметрами по умолчанию

  # {method} {0x0000000016da0810} 'AndSC' '(III)Z' in 'AndTest'
  ...
  0x0000000002923e3e: cmp    %r8d,%r9d
  0x0000000002923e41: movabs $0x16da0a08,%rax   ;   {metadata(method data for {method} {0x0000000016da0810} 'AndSC' '(III)Z' in 'AndTest')}
  0x0000000002923e4b: movabs $0x108,%rsi
  0x0000000002923e55: jl     0x0000000002923e65
  0x0000000002923e5b: movabs $0x118,%rsi
  0x0000000002923e65: mov    (%rax,%rsi,1),%rbx
  0x0000000002923e69: lea    0x1(%rbx),%rbx
  0x0000000002923e6d: mov    %rbx,(%rax,%rsi,1)
  0x0000000002923e71: jl     0x0000000002923eb0  ;*if_icmplt
                                                ; - AndTest::AndSC@2 (line 22)

  0x0000000002923e77: cmp    %edi,%r9d
  0x0000000002923e7a: movabs $0x16da0a08,%rax   ;   {metadata(method data for {method} {0x0000000016da0810} 'AndSC' '(III)Z' in 'AndTest')}
  0x0000000002923e84: movabs $0x128,%rsi
  0x0000000002923e8e: jg     0x0000000002923e9e
  0x0000000002923e94: movabs $0x138,%rsi
  0x0000000002923e9e: mov    (%rax,%rsi,1),%rdi
  0x0000000002923ea2: lea    0x1(%rdi),%rdi
  0x0000000002923ea6: mov    %rdi,(%rax,%rsi,1)
  0x0000000002923eaa: jle    0x0000000002923ec1  ;*if_icmpgt
                                                ; - AndTest::AndSC@7 (line 22)

  0x0000000002923eb0: mov    $0x0,%eax
  0x0000000002923eb5: add    $0x30,%rsp
  0x0000000002923eb9: pop    %rbp
  0x0000000002923eba: test   %eax,-0x1c73dc0(%rip)        # 0x0000000000cb0100
                                                ;   {poll_return}
  0x0000000002923ec0: retq                      ;*ireturn
                                                ; - AndTest::AndSC@13 (line 25)

  0x0000000002923ec1: mov    $0x1,%eax
  0x0000000002923ec6: add    $0x30,%rsp
  0x0000000002923eca: pop    %rbp
  0x0000000002923ecb: test   %eax,-0x1c73dd1(%rip)        # 0x0000000000cb0100
                                                ;   {poll_return}
  0x0000000002923ed1: retq   

Метод AndSC с опцией -XX:PrintAssemblyOptions=intel

  # {method} {0x00000000170a0810} 'AndSC' '(III)Z' in 'AndTest'
  ...
  0x0000000002c26e2c: cmp    r9d,r8d
  0x0000000002c26e2f: jl     0x0000000002c26e36  ;*if_icmplt
  0x0000000002c26e31: cmp    r9d,edi
  0x0000000002c26e34: jle    0x0000000002c26e44  ;*iconst_0
  0x0000000002c26e36: xor    eax,eax            ;*synchronization entry
  0x0000000002c26e38: add    rsp,0x10
  0x0000000002c26e3c: pop    rbp
  0x0000000002c26e3d: test   DWORD PTR [rip+0xffffffffffce91bd],eax        # 0x0000000002910000
  0x0000000002c26e43: ret    
  0x0000000002c26e44: mov    eax,0x1
  0x0000000002c26e49: jmp    0x0000000002c26e38

Метод AndNonSC с параметрами по умолчанию

  # {method} {0x0000000016da0908} 'AndNonSC' '(III)Z' in 'AndTest'
  ...
  0x0000000002923a78: cmp    %r8d,%r9d
  0x0000000002923a7b: mov    $0x0,%eax
  0x0000000002923a80: jl     0x0000000002923a8b
  0x0000000002923a86: mov    $0x1,%eax
  0x0000000002923a8b: cmp    %edi,%r9d
  0x0000000002923a8e: mov    $0x0,%esi
  0x0000000002923a93: jg     0x0000000002923a9e
  0x0000000002923a99: mov    $0x1,%esi
  0x0000000002923a9e: and    %rsi,%rax
  0x0000000002923aa1: cmp    $0x0,%eax
  0x0000000002923aa4: je     0x0000000002923abb  ;*ifeq
                                                ; - AndTest::AndNonSC@21 (line 29)

  0x0000000002923aaa: mov    $0x1,%eax
  0x0000000002923aaf: add    $0x30,%rsp
  0x0000000002923ab3: pop    %rbp
  0x0000000002923ab4: test   %eax,-0x1c739ba(%rip)        # 0x0000000000cb0100
                                                ;   {poll_return}
  0x0000000002923aba: retq                      ;*ireturn
                                                ; - AndTest::AndNonSC@25 (line 30)

  0x0000000002923abb: mov    $0x0,%eax
  0x0000000002923ac0: add    $0x30,%rsp
  0x0000000002923ac4: pop    %rbp
  0x0000000002923ac5: test   %eax,-0x1c739cb(%rip)        # 0x0000000000cb0100
                                                ;   {poll_return}
  0x0000000002923acb: retq   

Метод AndNonSC с опцией -XX:PrintAssemblyOptions=intel

  # {method} {0x00000000170a0908} 'AndNonSC' '(III)Z' in 'AndTest'
  ...
  0x0000000002c270b5: cmp    r9d,r8d
  0x0000000002c270b8: jl     0x0000000002c270df  ;*if_icmplt
  0x0000000002c270ba: mov    r8d,0x1            ;*iload_2
  0x0000000002c270c0: cmp    r9d,edi
  0x0000000002c270c3: cmovg  r11d,r10d
  0x0000000002c270c7: and    r8d,r11d
  0x0000000002c270ca: test   r8d,r8d
  0x0000000002c270cd: setne  al
  0x0000000002c270d0: movzx  eax,al
  0x0000000002c270d3: add    rsp,0x10
  0x0000000002c270d7: pop    rbp
  0x0000000002c270d8: test   DWORD PTR [rip+0xffffffffffce8f22],eax        # 0x0000000002910000
  0x0000000002c270de: ret    
  0x0000000002c270df: xor    r8d,r8d
  0x0000000002c270e2: jmp    0x0000000002c270c0
  • Прежде всего, сгенерированный код ASM различается в зависимости от того, выбираем ли мы синтаксис AT&T по умолчанию или синтаксис Intel.
  • With AT&T syntax:
    • The ASM code is actually longer for the AndSC method, with every bytecode IF_ICMP* translated to two assembly jump instructions, for a total of 4 conditional jumps.
    • Между тем, для метода AndNonSC компилятор генерирует более простой код, в котором каждый байт-код IF_ICMP* транслируется только в одну инструкцию перехода сборки, сохраняя исходное количество 3 условных переходов.
  • With Intel syntax:
    • The ASM code for AndSC is shorter, with just 2 conditional jumps (not counting the non-conditional jmp at the end). Actually it's just two CMP, two JL/E and a XOR/MOV depending on the result.
    • Код ASM для AndNonSC теперь длиннее, чем AndSC! Однако он имеет только 1 условный переход (для первого сравнения), используя регистры для прямого сравнения первого результата со вторым, без дополнительных переходов.

Заключение после анализа кода ASM

  • На уровне машинного языка AMD64 оператор &, по-видимому, генерирует код ASM с меньшим количеством условных переходов, что может быть лучше для высоких показателей ошибок прогнозирования (например, случайные values).
  • С другой стороны, оператор &&, кажется, генерирует код ASM с меньшим количеством инструкций (во всяком случае, с опцией -XX:PrintAssemblyOptions=intel), что может быть лучше для очень длинных циклов с удобными для прогнозирования входными данными, где меньшее количество Циклы ЦП для каждого сравнения могут иметь значение в долгосрочной перспективе.

Как я уже говорил в некоторых комментариях, это будет сильно различаться между системами, поэтому, если мы говорим об оптимизации прогнозирования ветвлений, единственным реальным ответом будет: это зависит от вашей реализации JVM, вашего компилятора, ваш процессор и ваши входные данные.


Приложение: метод Гуавы isPowerOfTwo

Здесь разработчики Guava придумали изящный способ вычисления, является ли данное число степенью двойки:

public static boolean isPowerOfTwo(long x) {
    return x > 0 & (x & (x - 1)) == 0;
}

Цитирую ОП:

является ли использование & (где && более уместным) настоящей оптимизацией?

Чтобы узнать, так ли это, я добавил в свой тестовый класс два похожих метода:

public boolean isPowerOfTwoAND(long x) {
    return x > 0 & (x & (x - 1)) == 0;
}

public boolean isPowerOfTwoANDAND(long x) {
    return x > 0 && (x & (x - 1)) == 0;
}

Код Intel ASM для версии Guava

  # {method} {0x0000000017580af0} 'isPowerOfTwoAND' '(J)Z' in 'AndTest'
  # this:     rdx:rdx   = 'AndTest'
  # parm0:    r8:r8     = long
  ...
  0x0000000003103bbe: movabs rax,0x0
  0x0000000003103bc8: cmp    rax,r8
  0x0000000003103bcb: movabs rax,0x175811f0     ;   {metadata(method data for {method} {0x0000000017580af0} 'isPowerOfTwoAND' '(J)Z' in 'AndTest')}
  0x0000000003103bd5: movabs rsi,0x108
  0x0000000003103bdf: jge    0x0000000003103bef
  0x0000000003103be5: movabs rsi,0x118
  0x0000000003103bef: mov    rdi,QWORD PTR [rax+rsi*1]
  0x0000000003103bf3: lea    rdi,[rdi+0x1]
  0x0000000003103bf7: mov    QWORD PTR [rax+rsi*1],rdi
  0x0000000003103bfb: jge    0x0000000003103c1b  ;*lcmp
  0x0000000003103c01: movabs rax,0x175811f0     ;   {metadata(method data for {method} {0x0000000017580af0} 'isPowerOfTwoAND' '(J)Z' in 'AndTest')}
  0x0000000003103c0b: inc    DWORD PTR [rax+0x128]
  0x0000000003103c11: mov    eax,0x1
  0x0000000003103c16: jmp    0x0000000003103c20  ;*goto
  0x0000000003103c1b: mov    eax,0x0            ;*lload_1
  0x0000000003103c20: mov    rsi,r8
  0x0000000003103c23: movabs r10,0x1
  0x0000000003103c2d: sub    rsi,r10
  0x0000000003103c30: and    rsi,r8
  0x0000000003103c33: movabs rdi,0x0
  0x0000000003103c3d: cmp    rsi,rdi
  0x0000000003103c40: movabs rsi,0x175811f0     ;   {metadata(method data for {method} {0x0000000017580af0} 'isPowerOfTwoAND' '(J)Z' in 'AndTest')}
  0x0000000003103c4a: movabs rdi,0x140
  0x0000000003103c54: jne    0x0000000003103c64
  0x0000000003103c5a: movabs rdi,0x150
  0x0000000003103c64: mov    rbx,QWORD PTR [rsi+rdi*1]
  0x0000000003103c68: lea    rbx,[rbx+0x1]
  0x0000000003103c6c: mov    QWORD PTR [rsi+rdi*1],rbx
  0x0000000003103c70: jne    0x0000000003103c90  ;*lcmp
  0x0000000003103c76: movabs rsi,0x175811f0     ;   {metadata(method data for {method} {0x0000000017580af0} 'isPowerOfTwoAND' '(J)Z' in 'AndTest')}
  0x0000000003103c80: inc    DWORD PTR [rsi+0x160]
  0x0000000003103c86: mov    esi,0x1
  0x0000000003103c8b: jmp    0x0000000003103c95  ;*goto
  0x0000000003103c90: mov    esi,0x0            ;*iand
  0x0000000003103c95: and    rsi,rax
  0x0000000003103c98: and    esi,0x1
  0x0000000003103c9b: mov    rax,rsi
  0x0000000003103c9e: add    rsp,0x50
  0x0000000003103ca2: pop    rbp
  0x0000000003103ca3: test   DWORD PTR [rip+0xfffffffffe44c457],eax        # 0x0000000001550100
  0x0000000003103ca9: ret    

Asm-код Intel для версии &&

  # {method} {0x0000000017580bd0} 'isPowerOfTwoANDAND' '(J)Z' in 'AndTest'
  # this:     rdx:rdx   = 'AndTest'
  # parm0:    r8:r8     = long
  ...
  0x0000000003103438: movabs rax,0x0
  0x0000000003103442: cmp    rax,r8
  0x0000000003103445: jge    0x0000000003103471  ;*lcmp
  0x000000000310344b: mov    rax,r8
  0x000000000310344e: movabs r10,0x1
  0x0000000003103458: sub    rax,r10
  0x000000000310345b: and    rax,r8
  0x000000000310345e: movabs rsi,0x0
  0x0000000003103468: cmp    rax,rsi
  0x000000000310346b: je     0x000000000310347b  ;*lcmp
  0x0000000003103471: mov    eax,0x0
  0x0000000003103476: jmp    0x0000000003103480  ;*ireturn
  0x000000000310347b: mov    eax,0x1            ;*goto
  0x0000000003103480: and    eax,0x1
  0x0000000003103483: add    rsp,0x40
  0x0000000003103487: pop    rbp
  0x0000000003103488: test   DWORD PTR [rip+0xfffffffffe44cc72],eax        # 0x0000000001550100
  0x000000000310348e: ret    

В этом конкретном примере JIT-компилятор генерирует намного меньше ассемблерного кода для версии &&, чем для версии & Guava (и после вчерашних результатов я был искренне удивлен этим).
По сравнению с Guava, версия && переводит на 25% меньше байт-кода для JIT-компиляции, на 50% меньше ассемблерных инструкций и только два условных перехода (версия & имеет четыре из них).

Таким образом, все указывает на то, что метод & Гуавы менее эффективен, чем более естественная версия &&.

... Or is it?

Как отмечалось ранее, я запускаю приведенные выше примеры с Java 8:

C:\....>java -version
java version "1.8.0_91"
Java(TM) SE Runtime Environment (build 1.8.0_91-b14)
Java HotSpot(TM) 64-Bit Server VM (build 25.91-b14, mixed mode)

Но что если я перейду на Java 7?

C:\....>c:\jdk1.7.0_79\bin\java -version
java version "1.7.0_79"
Java(TM) SE Runtime Environment (build 1.7.0_79-b15)
Java HotSpot(TM) 64-Bit Server VM (build 24.79-b02, mixed mode)
C:\....>c:\jdk1.7.0_79\bin\java -XX:+UnlockDiagnosticVMOptions -XX:CompileCommand=print,*AndTest.isPowerOfTwoAND -XX:PrintAssemblyOptions=intel AndTestMain
  .....
  0x0000000002512bac: xor    r10d,r10d
  0x0000000002512baf: mov    r11d,0x1
  0x0000000002512bb5: test   r8,r8
  0x0000000002512bb8: jle    0x0000000002512bde  ;*ifle
  0x0000000002512bba: mov    eax,0x1            ;*lload_1
  0x0000000002512bbf: mov    r9,r8
  0x0000000002512bc2: dec    r9
  0x0000000002512bc5: and    r9,r8
  0x0000000002512bc8: test   r9,r9
  0x0000000002512bcb: cmovne r11d,r10d
  0x0000000002512bcf: and    eax,r11d           ;*iand
  0x0000000002512bd2: add    rsp,0x10
  0x0000000002512bd6: pop    rbp
  0x0000000002512bd7: test   DWORD PTR [rip+0xffffffffffc0d423],eax        # 0x0000000002120000
  0x0000000002512bdd: ret    
  0x0000000002512bde: xor    eax,eax
  0x0000000002512be0: jmp    0x0000000002512bbf
  .....

Сюрприз! Ассемблерный код, сгенерированный для метода & JIT-компилятором в Java 7, теперь имеет только один условный переход и стал намного короче! В то время как метод && (поверьте мне в этом, я не хочу загромождать концовку!) остается примерно таким же, с его двумя условными переходами и парой меньше инструкций, топ.
Выглядит как будто инженеры Гуавы знали, что они делали, в конце концов! (если они пытались оптимизировать время выполнения Java 7, то есть ;-)

Итак, вернемся к последнему вопросу OP:

Является ли использование & (где && более уместным) настоящей оптимизацией?

И ИМХО ответ тот же, даже для этого (очень!) конкретного сценария: это зависит от вашей реализации JVM, вашего компилятора, вашего процессора и ваших входных данных.

person walen    schedule 20.09.2016
comment
@apangin Да, я думал об этом, когда читал этот ответ. Это действительно хороший анализ (спасибо @Walen!), но я не думаю, что он говорит нам, что произойдет на уровне конвейеров ЦП. Или я ошибаюсь? У меня сложилось впечатление, что сгенерированный машинный код (скажем, Hotspot) может разорвать все эти ветки на части, но я понимаю, что говорю по впечатлению, а не по факту. - person SusanW; 20.09.2016
comment
Что ж, байт-код Java ближе всего к ASM, прежде чем углубляться в специфику каждой ОС и ЦП. Конечно, IBM javac может выводить код, отличный от официального Oracle или OpenJDK... И, конечно, машинный код на машине X86, вероятно, будет отличаться от системы PowerPC AIX или процессоров Snapdragon, используемых во многих смартфонах — на каждой платформе. будет иметь свои собственные компиляторы и оптимизации. Но в таком простом случае я сомневаюсь, что различия между одним процессором и другим будут иметь большее значение, чем наличие условных переходов байт-кода 2 против 3. - person walen; 20.09.2016
comment
Хотя это может быть ближе всего к ASM, это недостаточно близко, чтобы вы могли делать какие-либо логические выводы. Проще говоря, после JIT-компиляции кода JVM не выполняет байт-коды. - person Stephen C; 20.09.2016
comment
Генерирует ли он 2/3 прыжков или 2/3 ветвей? - person Riley; 20.09.2016
comment
@Riley, это длинный пост, и я сделал много правок ;-) о чем именно вы говорите? - person walen; 20.09.2016
comment
@walen Ты прояснил это. Первоначально вы сказали переход вместо условного перехода (который на самом деле является ветвью). Есть только одно место для прыжка, так что тут ничего нельзя предсказать. Поэтому не могло быть ошибочного предсказания. - person Riley; 20.09.2016
comment
@Riley рад, что понял :-) До редактирования ASM единственными переходами были байт-кодированные IF_ICMP*, которые всегда являются условными переходами, поэтому я просто написал переходы. После редактирования ассемблерный код имеет как простой JMP, так и условный JLE/JG/и т.д. Я пытался быть более конкретным, но я пропустил часть байт-кода, спасибо!, - person walen; 20.09.2016
comment
@walen Я немного педантичен, но инструкции перехода (JMP в x86) не являются условными. Прыжок — это код операции перехода и адрес (относительный или абсолютный). Условный переход на самом деле является инструкцией ветвления. - person Riley; 20.09.2016
comment
@Riley, да, да, но я могу понять, так что нет проблем :) Позвольте мне процитировать официальный Руководство разработчика программного обеспечения для архитектур Intel® 64 и IA-32: < b>5.1.7 Инструкции по передаче управления Инструкции по передаче управления обеспечивают переход, условный переход, цикл, а также операции вызова и возврата для управления ходом программы. - person walen; 20.09.2016
comment
@walen Спасибо за разъяснения. Я почти исключительно работал с MIPS, где переход и переход - это очень разные инструкции, и я (ошибочно) предположил, что с моим (очень небольшим) исследованием x86, это было так же. - person Riley; 20.09.2016
comment
Вы также получаете разные ASM в зависимости (среди прочего) от того, сколько раз был вызван этот метод. HotSpot — это сложно. - person OrangeDog; 20.09.2016
comment
@OrangeDog справедливая точка зрения. Предположим, что его называют много! Разумно ли говорить, что с такими базовыми методами не так много различий между возможными генерациями кода? Не похоже, что загружаются другие классы, которые могут, например, стимулировать различные встраивания. - person SusanW; 20.09.2016
comment
@SusanW опубликованный вами ASM не является результатом, когда его часто вызывают. Когда это происходит, это запускает фактическую оптимизацию, которая может даже привести к тому, что они будут выдавать идентичные ASM. - person OrangeDog; 20.09.2016
comment
@walen Мне нравится твой ответ, спасибо! Но я добавил update2, чтобы устранить слишком широкий маркер, процитировав пример Guava и переформулировав вопрос в этих конкретных терминах. Я очень старался сделать так, чтобы вся ваша тяжелая работа по-прежнему была подходящим ответом - не могли бы вы просмотреть и исправить все, что, по вашему мнению, было потерянным? - person SusanW; 21.09.2016
comment
@SusanW Вчера я видел пример гуавы, выглядит интересно! Попробую сегодня посмотреть на сборку (часовой пояс CEST) :) - person walen; 21.09.2016
comment
@SusanW Я отредактировал свой ответ, чтобы учесть ваше последнее обновление (Update2). Похоже, версия Guava оптимизирована для Java 7, но не для Java 8, так что все еще зависит от вашей среды #savedyouaclick :) - person walen; 21.09.2016
comment
@OrangeDog Эти методы вызываются внутри цикла с миллионом итераций for (int i = 0; i < 1000000; i++). Я не знаю, много ли JIT считает, но я надеюсь, что да! В любом случае, я приму это во внимание в следующий раз, так как это хороший момент. - person walen; 21.09.2016
comment
Ну, я думаю, что это фантастический ответ. Возможно, в Java8 есть какая-то тонкость, которая может заставить его применять дальнейшие оптимизации на основе магии HotSpot или чего-то еще. В таком случае может родиться новый вопрос... между тем, хороший! Большое спасибо! - person SusanW; 22.09.2016

Для таких вопросов вы должны запустить микробенчмарк. Для этого теста я использовал JMH.

Бенчмарки реализованы как

// boolean logical AND
bh.consume(value >= x & y <= value);

а также

// conditional AND
bh.consume(value >= x && y <= value);

а также

// bitwise OR, as suggested by Joop Eggen
bh.consume(((value - x) | (y - value)) >= 0)

Со значениями для value, x and y в соответствии с названием теста.

Результат (пять итераций прогрева и десять итераций измерения) для сравнительного анализа пропускной способности:

Benchmark                                 Mode  Cnt    Score    Error   Units
Benchmark.isBooleanANDBelowRange          thrpt   10  386.086 ▒ 17.383  ops/us
Benchmark.isBooleanANDInRange             thrpt   10  387.240 ▒  7.657  ops/us
Benchmark.isBooleanANDOverRange           thrpt   10  381.847 ▒ 15.295  ops/us
Benchmark.isBitwiseORBelowRange           thrpt   10  384.877 ▒ 11.766  ops/us
Benchmark.isBitwiseORInRange              thrpt   10  380.743 ▒ 15.042  ops/us
Benchmark.isBitwiseOROverRange            thrpt   10  383.524 ▒ 16.911  ops/us
Benchmark.isConditionalANDBelowRange      thrpt   10  385.190 ▒ 19.600  ops/us
Benchmark.isConditionalANDInRange         thrpt   10  384.094 ▒ 15.417  ops/us
Benchmark.isConditionalANDOverRange       thrpt   10  380.913 ▒  5.537  ops/us

Результат не сильно отличается от самой оценки. Пока этот фрагмент кода не влияет на производительность, я бы не стал пытаться его оптимизировать. В зависимости от места в коде компилятор точки доступа может принять решение о некоторой оптимизации. Что, вероятно, не покрывается вышеуказанными тестами.

некоторые ссылки:

логическое И — результат значение равно true, если оба значения операнда равны true; в противном случае результатом будет false
условное И - похож на &, но оценивает свой правый операнд, только если значение его левого операнда равно true
побитовое ИЛИ — результирующее значение представляет собой побитовое включающее ИЛИ значений операнда.

person SubOptimal    schedule 20.09.2016
comment
Определенно хорошее замечание о JMH!! Проверяют ли эти тесты случайные вариации в успешном и неудачном тестировании, особенно в отношении двух подвыражений? В противном случае мы не наблюдаем эффекта предсказания-ошибки... - person SusanW; 20.09.2016
comment
На данный момент это лучший тест, но он также несовершенен :) Черная дыра занимает гораздо больше времени, чем && или &, поэтому вы в основном измеряете производительность черной дыры :) попробуйте что-то вроде потребления (a & b & c 7 d & f &г....&г); - person Svetlin Zarev; 20.09.2016
comment
Когда мы говорим о дополнительных пожеланиях: ((value - x) | (y - value)) >= 0 было бы моим желанием: побитовое или - person Joop Eggen; 20.09.2016
comment
@SusanW & генерирует три операции IF с байт-кодом, а && генерирует только две (см. ответ для подробностей). С достаточно длинным списком случайно сгенерированных значений должно быть более очевидным, что версия с коротким замыканием имеет меньше ошибок прогнозирования, хотя бы потому, что она должна предсказывать только два из них... @SvetlinZarev Это уже видно в вашем тесте (с & ошибки 17-21-17 и && 13-16-13), но, может быть, вы могли бы переделать их с учетом этого? - person walen; 20.09.2016
comment
@SusanW Кстати, именно ошибка JMH помогла обнаружить, что HotSpot сокращает оценку &. Итак, отвечая на исходный вопрос - нет, JVM все равно генерирует условную ветку для &. - person apangin; 20.09.2016
comment
@SubOptimal Хорошая идея написать тест, подтверждающий теорию. Однако чистые результаты тестов ничего не говорят без надлежащего анализа. Если вы включите сгенерированный ассемблерный код, объясняющий результаты тестов, это будет ответ. - person apangin; 20.09.2016
comment
@apangin Я абсолютно согласен с вами, и это уже упоминалось в моем ответе. Бенчмарк был сделан в основном для того, чтобы объяснить, что без специального теста в нем меньше смысла. - person SubOptimal; 20.09.2016
comment
Придирки: согласно Спецификации языка Java §4.2.5; булевыми логическими операторами являются &, ^ и |. && и || — это операторы условного И и условного ИЛИ. - person Johnbot; 20.09.2016
comment
@JoopEggen Я добавил побитовое или. - person SubOptimal; 20.09.2016
comment
Спасибо. Разочаровывает, хотя, конечно, не плохо. Еще кое-что для C. - person Joop Eggen; 20.09.2016
comment
@apangin об ошибке JMH: означает ли это, что HotSpot пропустил бы вызов метода RHS в boolean b = a>b & methodWithSideEffects()? - person SusanW; 20.09.2016
comment
@SusanW @SubOptimal Я отредактировал свой ответ, включив в него реальный код ASM, сгенерированный JIT. И похоже, что & может быть лучше в некоторых случаях! Комментарии приветствуются :-) - person walen; 20.09.2016
comment
@walen Спасибо за этот дополнительный вклад. - person SubOptimal; 20.09.2016
comment
@SusanW Нет, methodWithSideEffects() не будет пропущено, иначе это будет нарушением спецификации. Однако в этом случае можно оптимизировать метод без побочных эффектов. - person apangin; 20.09.2016
comment
Уже существует много путаницы в отношении значения логических операторов, не являющихся ярлыками. Не могли бы вы изменить этот пост, чтобы не называть их побитовыми? В вашем тесте нет побитовых вычислений. - person JimmyJames; 20.09.2016
comment
@JimmyJames Спасибо за подсказку. Я изменил название и добавил несколько ссылок на JLS. - person SubOptimal; 21.09.2016
comment
генерируются ли значения случайным образом, чтобы показать влияние неверных предсказаний ветвления? - person the8472; 08.10.2016
comment
@ the8472 Нет. Поскольку это не было частью первоначального вопроса, и эталонный тест JMH не был создан для него. - person SubOptimal; 13.10.2016

Я подойду к этому с другой точки зрения.

Рассмотрим эти два фрагмента кода,

  if (value >= x && value <= y) {

а также

  if (value >= x & value <= y) {

Если предположить, что value, x, y имеют примитивный тип, то эти два (частичных) оператора дадут одинаковый результат для всех возможных входных значений. (Если задействованы типы-оболочки, то они не совсем эквивалентны из-за неявного теста null для y, который может дать сбой в версии &, а не в версии &&.)

Если компилятор JIT работает хорошо, его оптимизатор сможет сделать вывод, что эти два оператора делают одно и то же:

  • Если один предсказуемо быстрее другого, то он должен иметь возможность использовать более быструю версию... в коде, скомпилированном JIT.

  • Если нет, то не имеет значения, какая версия используется на уровне исходного кода.

  • Поскольку JIT-компилятор собирает статистику пути перед компиляцией, он потенциально может иметь больше информации о характеристиках выполнения, чем программист(!).

  • Если JIT-компилятор текущего поколения (на любой данной платформе) недостаточно хорошо оптимизируется, чтобы справиться с этим, вполне может справиться следующее поколение... в зависимости от того, указывают ли эмпирические данные на то, что это целесообразно Шаблон для оптимизации.

  • Действительно, если вы пишете свой Java-код таким образом, который оптимизируется для этого, есть шанс, что, выбрав более «непонятную» версию кода, вы можете запретить текущая или будущая способность JIT-компилятора оптимизировать.

Короче говоря, я не думаю, что вам следует делать такую ​​микрооптимизацию на уровне исходного кода. А если принять этот аргумент1 и довести его до логического завершения, то вопрос о том, какая версия быстрее... спорный2.

1 – я не утверждаю, что это доказательство.

2 – Если вы не принадлежите к небольшому сообществу людей, которые пишут JIT-компиляторы для Java...


«Очень известный вопрос» интересен в двух отношениях:

  • С одной стороны, это пример того, как тип оптимизации, необходимый для изменения ситуации, выходит далеко за рамки возможностей JIT-компилятора.

  • С другой стороны, сортировать массив не обязательно правильно... просто потому, что отсортированный массив можно обрабатывать быстрее. Стоимость сортировки массива вполне может быть (намного) больше, чем экономия.

person Stephen C    schedule 20.09.2016
comment
Ваше замечание о запрете будущих оптимизаций очень хорошо сделано! - преднамеренная постановка '&' в условие будет равносильна тому, что вы не сможете четко выразить намерения, чтобы обмануть систему, и когда вы солжете своему компьютеру, он отомстит .... - person SusanW; 20.09.2016
comment
Какой из них быстрее, зависит от данных. Это то, чего JIT не может знать. Или JIT-компиляторы JVM могут профилировать такую ​​вещь? В таком случае это было бы вполне осуществимо. - person usr; 20.09.2016
comment
да. JIT может сделать это. И JIT-компиляторы HotSpot делают это на этапе, предшествующем интерпретации байт-кодов... до компиляции. - person Stephen C; 20.09.2016
comment
Если x и y являются либо постоянными, либо предсказуемыми значениями, оптимизированный код скорее будет выглядеть как value-x ≤ͧ y-x, где ≤ͧ — это unsigned long сравнение, а y-x — константа, хотя даже если x и y не являются предсказуемыми, этот единственный вариант сравнения может использоваться, если два переходы считаются более затратными, чем жадно выполняемое сравнение (числовое сравнение равноценно операции минус). Так что думать о & и && действительно бессмысленно. - person Holger; 20.09.2016
comment
Будущие оптимизации - люблю этот аспект. Подумайте, как a+b+c превратились в использование StringBuffers, даже когда, возможно, они не имели большого значения. Затем, когда появились StringBuilders, теперь у людей есть эти большие неуклюжие потокобезопасные StringBuffers, где такие накладные расходы были ненужными. Теперь a+b+c настраивается на StringBuilders при компиляции, но любые явные StringBuffers, очевидно, все еще существуют из-за усердной сверхоптимизации. - person corsiKa; 20.09.2016
comment
Думаю, я привык к .NET JIT, который вообще не профилирует и не очень оптимизирует. Очень грустно. - person usr; 20.09.2016
comment
@corsiKa: к счастью, HotSpot неплохо устраняет ненужную синхронизацию чисто локального объекта. Проблемы возникают, когда люди настолько «умны», что хранят экземпляры StringBuffer где-то для повторного использования. Это имеет неприятные последствия во многих отношениях. - person Holger; 21.09.2016

Использование & или && по-прежнему требует оценки условия, поэтому маловероятно, что это сэкономит время обработки — оно может даже увеличить его, учитывая, что вы оцениваете оба выражения, когда вам нужно оценить только одно.

Используя & вместо &&, чтобы сэкономить наносекунду, если это в некоторых очень редких ситуациях бессмысленно, вы уже потратили больше времени на обдумывание разницы, чем вы бы сэкономили, используя & вместо &&.

Изменить

Мне стало любопытно, и я решил провести несколько бенчмарков.

Я сделал этот класс:

public class Main {

    static int x = 22, y = 48;

    public static void main(String[] args) {
        runWithOneAnd(30);
        runWithTwoAnds(30);
    }

    static void runWithOneAnd(int value){
        if(value >= x & value <= y){

        }
    }

    static void runWithTwoAnds(int value){
        if(value >= x && value <= y){

        }
    }
}

и провел несколько тестов профилирования с помощью NetBeans. Я не использовал никаких операторов печати, чтобы сэкономить время обработки, просто знаю, что оба оцениваются как true.

Первый тест:

Первый тест профилирования

Второй тест:

Второй тест профилирования

Третий тест:

Третий профилирующий тест

Как видно из тестов профилирования, использование только одного & на самом деле занимает в 2-3 раза больше времени, чем использование двух &&. Это кажется несколько странным, поскольку я ожидал лучшей производительности только от одного &.

Я не уверен на 100%, почему. В обоих случаях должны быть оценены оба выражения, потому что оба они истинны. Я подозреваю, что JVM делает какую-то специальную оптимизацию за кулисами, чтобы ускорить ее.

Мораль истории: соглашение — это хорошо, а преждевременная оптимизация — это плохо.


Изменить 2

Я переделал код теста с учетом комментариев @SvetlinZarev и некоторых других улучшений. Вот измененный код теста:

public class Main {

    static int x = 22, y = 48;

    public static void main(String[] args) {
        oneAndBothTrue();
        oneAndOneTrue();
        oneAndBothFalse();
        twoAndsBothTrue();
        twoAndsOneTrue();
        twoAndsBothFalse();
        System.out.println(b);
    }

    static void oneAndBothTrue() {
        int value = 30;
        for (int i = 0; i < 2000; i++) {
            if (value >= x & value <= y) {
                doSomething();
            }
        }
    }

    static void oneAndOneTrue() {
        int value = 60;
        for (int i = 0; i < 4000; i++) {
            if (value >= x & value <= y) {
                doSomething();
            }
        }
    }

    static void oneAndBothFalse() {
        int value = 100;
        for (int i = 0; i < 4000; i++) {
            if (value >= x & value <= y) {
                doSomething();
            }
        }
    }

    static void twoAndsBothTrue() {
        int value = 30;
        for (int i = 0; i < 4000; i++) {
            if (value >= x & value <= y) {
                doSomething();
            }
        }
    }

    static void twoAndsOneTrue() {
        int value = 60;
        for (int i = 0; i < 4000; i++) {
            if (value >= x & value <= y) {
                doSomething();
            }
        }
    }

    static void twoAndsBothFalse() {
        int value = 100;
        for (int i = 0; i < 4000; i++) {
            if (value >= x & value <= y) {
                doSomething();
            }
        }
    }

    //I wanted to avoid print statements here as they can
    //affect the benchmark results. 
    static StringBuilder b = new StringBuilder();
    static int times = 0;

    static void doSomething(){
        times++;
        b.append("I have run ").append(times).append(" times \n");
    }
}

А вот и тесты производительности:

Тест 1:

введите здесь описание изображения

Тест 2:

введите здесь описание изображения

Тест 3:

введите здесь описание изображения

При этом учитываются разные значения и разные условия.

Использование одного & требует больше времени для выполнения, когда выполняются оба условия, примерно на 60% или на 2 миллисекунды больше времени. Когда одно или оба условия неверны, тогда одно & работает быстрее, но только примерно на 0,30-0,50 миллисекунды быстрее. Таким образом, & в большинстве случаев будет работать быстрее, чем &&, но разница в производительности по-прежнему незначительна.

person Luke Melaia    schedule 20.09.2016
comment
Ваш микротест полностью ошибочен. JIT оптимизирует эти пустые циклы for, не говоря уже о том, что одно выполнение метода, как в вашем коде, никогда не может дать каких-либо значимых результатов. - person Svetlin Zarev; 20.09.2016
comment
Спасибо, что указали на это, я повторю тесты с учетом этого. - person Luke Melaia; 20.09.2016
comment
Единственный правильный способ микробенчмаркинга — использовать такой инструмент, как JMH. - person Svetlin Zarev; 20.09.2016
comment
Если вы не работаете на действительно старой машине, ваши циклы не выполняются достаточно раз, чтобы получить какие-либо значимые результаты. Кроме того, порядок, когда вы называете вещи, может иметь огромное значение. Наконец, если вы продолжаете добавлять к StringBuilder, в конечном итоге потребуется выделить много памяти, а это займет много времени. - person JimmyJames; 20.09.2016
comment
«Оба Ложь» недействительна. Эти методы со 100 проверяют то же самое, что и 60. Вы не можете быть одновременно ниже диапазона и выше диапазона, поэтому BothFalse недостижим. - person Sinc; 26.09.2016
comment
Что вам действительно нужно, так это проверка значения = 10 вместо 100. Тогда проверка 60 становится SecondFalse, а 10 — FirstFalse. Последний тест должен выполняться быстрее, чем первый, с &&s, но все же может быть сравним с &s. - person Sinc; 26.09.2016

То, что вам нужно, выглядит примерно так:

x <= value & value <= y
value - x >= 0 & y - value >= 0
((value - x) | (y - value)) >= 0  // integer bit-or

Интересно, почти хочется посмотреть на байт-код. Но трудно сказать. Я бы хотел, чтобы это был вопрос категории C.

person Joop Eggen    schedule 20.09.2016

Мне тоже был любопытен ответ, поэтому я написал для этого следующий (простой) тест:

private static final int max = 80000;
private static final int size = 100000;
private static final int x = 1500;
private static final int y = 15000;
private Random random;

@Before
public void setUp() {
    this.random = new Random();
}

@After
public void tearDown() {
    random = null;
}

@Test
public void testSingleOperand() {
    int counter = 0;
    int[] numbers = new int[size];
    for (int j = 0; j < size; j++) {
        numbers[j] = random.nextInt(max);
    }

    long start = System.nanoTime(); //start measuring after an array has been filled
    for (int i = 0; i < numbers.length; i++) {
        if (numbers[i] >= x & numbers[i] <= y) {
            counter++;
        }
    }
    long end = System.nanoTime();
    System.out.println("Duration of single operand: " + (end - start));
}

@Test
public void testDoubleOperand() {
    int counter = 0;
    int[] numbers = new int[size];
    for (int j = 0; j < size; j++) {
        numbers[j] = random.nextInt(max);
    }

    long start = System.nanoTime(); //start measuring after an array has been filled
    for (int i = 0; i < numbers.length; i++) {
        if (numbers[i] >= x & numbers[i] <= y) {
            counter++;
        }
    }
    long end = System.nanoTime();
    System.out.println("Duration of double operand: " + (end - start));
}

В результате сравнение с && всегда выигрывает с точки зрения скорости, будучи примерно на 1,5/2 миллисекунды быстрее, чем &.

EDIT: Как отметил @SvetlinZarev, я также измерял время, которое потребовалось Random для получения целого числа. Изменил его, чтобы использовать предварительно заполненный массив случайных чисел, из-за чего продолжительность теста с одним операндом сильно колебалась; различия между несколькими прогонами составляли до 6-7 мс.

person Oromë    schedule 20.09.2016
comment
Хорошо, интересно: я вижу, что первое условие в основном будет успешным (generated >= x), что означает, что предиктор обычно все делает правильно (если он работает так, как я думаю). Я собираюсь попробовать поиграть со значениями «x» и «y» — думаю, x=40000 и y=60000 будут интересны (50% успеха в каждом тесте). - person SusanW; 20.09.2016
comment
С этими значениями && по-прежнему превосходит &. На этот раз средняя разница между ними тоже оказалась выше, никогда не опускаясь ниже 2 мс, а иногда даже превышая 3 мс. - person Oromë; 20.09.2016
comment
вы измеряете random.nextInt(), так как это занимает гораздо больше времени, чем простое && или &. Ваши тесты ошибочны - person Svetlin Zarev; 20.09.2016
comment
@SvetlinZarev хорошая мысль - и если там есть какие-то условные обозначения, они их захлестнут. Лучше сначала создать массив случайных чисел, а затем повторять его в цикле по времени? Крысы, хочу попробовать, но я на работе. - person SusanW; 20.09.2016
comment
Просто используйте JMH. у него есть хорошие методы @Setup для настройки и создания хороших отчетов. - person Svetlin Zarev; 20.09.2016
comment
@SvetlinZarev Хорошая мысль о случайном комментарии; Я изменил его, чтобы использовать массив, заполненный случайными целыми числами, с тем же конечным результатом, что && быстрее, чем &. - person Oromë; 20.09.2016
comment
@ Оромэ, тебе все еще не хватает разминки :) - person Svetlin Zarev; 20.09.2016
comment
@SvetlinZarev Сколько нужно разминки? Я запускал его вручную около 20 раз. - person Oromë; 20.09.2016

person    schedule
comment
Извините, это не соответствует сути вопроса! Посмотрите на первое примечание в вопросе - я довольно четко об этом сказал. Очевидно, что если можно сэкономить значительное время, не выполняя последующие условия, то хорошо, мы все об этом знаем. Но для этого требуется ветвь, и современные конвейеры инструкций процессора иногда делают предположения о направлении, в котором пойдет ветвь, что оказывается а) неправильным и б) довольно дорогим. Пожалуйста, прочитайте верхний ответ на (очень известный) вопрос, на который я ссылался, а затем решите, хотите ли вы сохранить этот ответ. - person SusanW; 29.09.2016