Когда я писал этот ответ, я смотрел только на титульный вопрос о <против <= в целом, а не конкретный пример постоянного a < 901
VS. a <= 900
. Многие компиляторы всегда уменьшают величину констант путем преобразования между <
и <=
, например, потому что непосредственный операнд x86 имеет более короткую 1-байтовую кодировку для -128..127.
Для ARM и особенно для AArch64 возможность кодирования как непосредственного зависит от возможности поворота узкого поля в любую позицию в слове. Так cmp w0, #0x00f000
что будет закодировано, а cmp w0, #0x00effff
может и не быть. Таким образом, правило «сделай это меньше» для сравнения с константой времени компиляции не всегда применимо к AArch64.
<vs. <= в целом, в том числе для переменных во время выполнения
На языке ассемблера на большинстве машин сравнение для <=
имеет такую же стоимость, что и сравнение <
. Это применимо, независимо от того, ветвитесь ли вы на нем, логизируете его для создания целого числа 0/1 или используете его в качестве предиката для операции выбора без ответвлений (например, CMOV x86). Другие ответы касались только этой части вопроса.
Но этот вопрос касается операторов C ++, входных данных для оптимизатора. Обычно они оба одинаково эффективны; совет из книги звучит совершенно фиктивно, потому что компиляторы всегда могут преобразовать сравнение, которое они реализуют в asm. Но есть по крайней мере одно исключение, когда использование <=
может случайно создать что-то, что компилятор не может оптимизировать.
В качестве условия цикла, есть случаи , когда <=
является качественно отличается от <
, когда он останавливает компилятор от доказательства того, что цикл не является бесконечным. Это может иметь большое значение, отключая автоматическую векторизацию.
Неподписанное переполнение четко определено как перестановка по основанию 2, в отличие от подписанного переполнения (UB). Счетчики циклов со знаком, как правило, защищены от этого, поскольку компиляторы, которые оптимизируют на основе UB со знаком переполнения, не происходят: ++i <= size
всегда в конечном итоге становятся ложными. ( Что должен знать каждый программист на C о неопределенном поведении )
void foo(unsigned size) {
unsigned upper_bound = size - 1; // or any calculation that could produce UINT_MAX
for(unsigned i=0 ; i <= upper_bound ; i++)
...
Компиляторы могут оптимизировать только таким образом, чтобы сохранить (определенное и юридически наблюдаемое) поведение источника C ++ для всех возможных входных значений , кроме тех, которые приводят к неопределенному поведению.
(Простое i <= size
тоже создало бы проблему, но я подумал, что вычисление верхней границы было более реалистичным примером случайного введения возможности бесконечного цикла для ввода, который вас не волнует, но который должен учитывать компилятор.)
В этом случае size=0
приводит к upper_bound=UINT_MAX
и i <= UINT_MAX
всегда верно. Так что этот цикл бесконечен size=0
, и компилятор должен учитывать это, даже если вы, как программист, вероятно, никогда не намереваетесь передать size = 0. Если компилятор может встроить эту функцию в вызывающую функцию, где он может доказать, что size = 0 невозможен, то это здорово, он может оптимизировать так, как мог бы i < size
.
Asm like if(!size) skip the loop;
do{...}while(--size);
- это один обычно эффективный способ оптимизировать for( i<size )
цикл, если фактическое значение i
внутри цикла не требуется ( почему циклы всегда компилируются в стиле "do ... while" (прыжок в хвост)? ).
Но это делает {}, хотя не может быть бесконечным: если введено с size==0
, мы получаем 2 ^ n итераций. ( Итерация по всем целым числам без знака в цикле for C позволяет выразить цикл по всем целым числам без знака, включая ноль, но без флага переноса это непросто, как в asm.)
Учитывая возможность оборачивания счетчика циклов, современные компиляторы часто просто «сдаются» и оптимизируют их не так агрессивно.
Пример: сумма целых чисел от 1 до n
Использование неподписанных i <= n
поражений распознавания идиома clang, которая оптимизирует sum(1 .. n)
петли с замкнутой формой на основе n * (n+1) / 2
формулы Гаусса .
unsigned sum_1_to_n_finite(unsigned n) {
unsigned total = 0;
for (unsigned i = 0 ; i < n+1 ; ++i)
total += i;
return total;
}
x86-64 asm из clang7.0 и gcc8.2 в проводнике компилятора Godbolt
# clang7.0 -O3 closed-form
cmp edi, -1 # n passed in EDI: x86-64 System V calling convention
je .LBB1_1 # if (n == UINT_MAX) return 0; // C++ loop runs 0 times
# else fall through into the closed-form calc
mov ecx, edi # zero-extend n into RCX
lea eax, [rdi - 1] # n-1
imul rax, rcx # n * (n-1) # 64-bit
shr rax # n * (n-1) / 2
add eax, edi # n + (stuff / 2) = n * (n+1) / 2 # truncated to 32-bit
ret # computed without possible overflow of the product before right shifting
.LBB1_1:
xor eax, eax
ret
Но для наивной версии мы просто получаем тупую петлю от лязга.
unsigned sum_1_to_n_naive(unsigned n) {
unsigned total = 0;
for (unsigned i = 0 ; i<=n ; ++i)
total += i;
return total;
}
# clang7.0 -O3
sum_1_to_n(unsigned int):
xor ecx, ecx # i = 0
xor eax, eax # retval = 0
.LBB0_1: # do {
add eax, ecx # retval += i
add ecx, 1 # ++1
cmp ecx, edi
jbe .LBB0_1 # } while( i<n );
ret
GCC в любом случае не использует замкнутую форму, поэтому выбор условия цикла на самом деле не повредит ; он автоматически векторизуется с добавлением целых чисел SIMD, выполняя 4 i
значения параллельно в элементах регистра XMM.
# "naive" inner loop
.L3:
add eax, 1 # do {
paddd xmm0, xmm1 # vect_total_4.6, vect_vec_iv_.5
paddd xmm1, xmm2 # vect_vec_iv_.5, tmp114
cmp edx, eax # bnd.1, ivtmp.14 # bound and induction-variable tmp, I think.
ja .L3 #, # }while( n > i )
"finite" inner loop
# before the loop:
# xmm0 = 0 = totals
# xmm1 = {0,1,2,3} = i
# xmm2 = set1_epi32(4)
.L13: # do {
add eax, 1 # i++
paddd xmm0, xmm1 # total[0..3] += i[0..3]
paddd xmm1, xmm2 # i[0..3] += 4
cmp eax, edx
jne .L13 # }while( i != upper_limit );
then horizontal sum xmm0
and peeled cleanup for the last n%3 iterations, or something.
У этого также есть простой скалярный цикл, который я думаю, что он использует для очень маленького n
, и / или для случая бесконечного цикла.
Кстати, оба этих цикла тратят впустую инструкцию (и моп на процессорах семейства Sandybridge) на издержки цикла. sub eax,1
/ jnz
вместо add eax,1
/ cmp / jcc будет более эффективным. 1 моп вместо 2 (после макро-слияния sub / jcc или cmp / jcc). Код после обоих циклов безоговорочно записывает EAX, поэтому он не использует окончательное значение счетчика цикла.