Коли я писав цю відповідь, я дивився тільки на титульний питання про <проти <= в цілому, а не конкретний приклад постійного a < 901
VS. a <= 900
. Багато укладачі завжди зменшити величину констант шляху перетворення між <
і <=
, наприклад , з - x86 безпосереднього операнда має більш короткий кодування 1-байтовое для -128..127.
Для ARM, і особливо для AArch64, можливість кодування як негайної залежить від того, чи зможе повернути вузьке поле в будь-яку позицію слова. Так cmp w0, #0x00f000
би було кодовано, а cmp w0, #0x00effff
може і не бути. Таким чином, правило make-it-small для порівняння та константа часу компіляції не завжди застосовується для AArch64.
<vs. <= загалом, у тому числі для змінних умов виконання
У мові складання на більшості машин порівняння <=
має таку саму вартість, як і порівняння <
. Це стосується того, чи розгалужуєте ви його, булінізуєте його для створення цілого числа 0/1 або використовуєте його як предикат для операції вибору без гілок (наприклад, CM86 CMOV). Інші відповіді стосувались лише цієї частини питання.
Але це питання стосується операторів 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
, і компілятор повинен поважати це, хоча ви, як програміст, ймовірно, ніколи не збираєтесь передавати розмір = 0. Якщо компілятор може вбудувати цю функцію в абонент, де він може довести, що розмір = 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
поразки розпізнавання ідіоми Кланг, що оптимізує 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 беззастережно, тому він не використовує остаточне значення лічильника циклу.