Найшвидший спосіб визначити, чи є ціле число між двома цілими числами (включно) з відомими наборами значень


389

Чи є швидший спосіб x >= start && x <= endперевірити, чи є ціле число між двома цілими числами?

ОНОВЛЕННЯ : Моя конкретна платформа - iOS. Це частина функції розмиття поля, яка обмежує пікселі колом у заданому квадраті.

ОНОВЛЕННЯ : Після спробу прийнятої відповіді я отримав порядок прискорення масштабу в одному рядку коду над тим, як робити це звичайним x >= start && x <= endчином.

ОНОВЛЕННЯ : Ось код після і перед асемблером від XCode:

НОВИЙ ШЛЯХ

// diff = (end - start) + 1
#define POINT_IN_RANGE_AND_INCREMENT(p, range) ((p++ - range.start) < range.diff)

Ltmp1313:
 ldr    r0, [sp, #176] @ 4-byte Reload
 ldr    r1, [sp, #164] @ 4-byte Reload
 ldr    r0, [r0]
 ldr    r1, [r1]
 sub.w  r0, r9, r0
 cmp    r0, r1
 blo    LBB44_30

СТАРИЙ ШЛЯХ

#define POINT_IN_RANGE_AND_INCREMENT(p, range) (p <= range.end && p++ >= range.start)

Ltmp1301:
 ldr    r1, [sp, #172] @ 4-byte Reload
 ldr    r1, [r1]
 cmp    r0, r1
 bls    LBB44_32
 mov    r6, r0
 b      LBB44_33
LBB44_32:
 ldr    r1, [sp, #188] @ 4-byte Reload
 adds   r6, r0, #1
Ltmp1302:
 ldr    r1, [r1]
 cmp    r0, r1
 bhs    LBB44_36

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


28
Чому ви стурбовані тим, що це недостатньо швидко для вас?
Метт Бал

90
Кого це хвилює, цікаве питання. Це просто виклик заради виклику.
Девід каже, що повернеться до Моніки

46
@SLaks Отже, ми повинні просто ігнорувати всі подібні запитання наосліп і просто говорити "нехай оптимізатор це зробить?"
Девід каже, що повернемо Моніку

87
не має значення, чому задають питання. Це дійсне питання, навіть якщо відповідь ні
tay10r

41
Це вузьке місце у функції одного з моїх додатків
jjxtra

Відповіді:


527

Існує стара хитрість зробити це лише одним порівнянням / гілкою. Чи дійсно це покращить швидкість, можливо, це може бути сумнівом, і навіть якщо це станеться, це, мабуть, замало, щоб помітити чи дбати, але коли ви тільки починаєте з двох порівнянь, шанси на величезне поліпшення досить далекі. Код виглядає так:

// use a < for an inclusive lower bound and exclusive upper bound
// use <= for an inclusive lower bound and inclusive upper bound
// alternatively, if the upper bound is inclusive and you can pre-calculate
//  upper-lower, simply add + 1 to upper-lower and use the < operator.
    if ((unsigned)(number-lower) <= (upper-lower))
        in_range(number);

З типовим сучасним комп’ютером (тобто все, що використовує двійки), перетворення на безпідписаний насправді є nop - лише зміною способу перегляду одних і тих же бітів.

Зауважте, що в типовому випадку ви можете попередньо обчислити upper-lowerпоза (передбачуваний) цикл, щоб зазвичай не вносити жодного значного часу. Поряд зі зменшенням кількості інструкцій із галузей, це також (загалом) покращує прогнозування галузей. У цьому випадку береться та сама гілка, чи то число, що знаходиться нижче нижнього кінця або вище верхнього кінця діапазону.

Щодо того, як це працює, основна ідея досить проста: негативне число, якщо розглядати його як непідписане число, буде більшим за все, що почалося як позитивне число.

На практиці цей метод переводить numberі інтервал до точки початку і перевіряє, чи numberзнаходиться в інтервалі [0, D], де D = upper - lower. Якщо numberнижче нижньої межі: мінус , а якщо вище верхньої межі: більше, ніжD .


8
@ TomásBadan: Вони обидва будуть одним циклом на будь-якій розумній машині. Що дорого - це галузь.
Олівер Чарльворт

3
Додаткове розгалуження проводиться за рахунок короткого замикання? Якщо це так, чи призведе lower <= x & x <= upper(замість lower <= x && x <= upper) до кращих показників?
Маркус Майр

6
@ AK4749, jxh: Настільки ж класно, як цей самородок, я не вагаюся, тому що, на жаль, немає нічого, що дозволяє припустити, що це швидше на практиці (поки хтось не порівняє отриману інформацію про асемблер та профілювання). Наскільки нам відомо, компілятор ОП може надати код ОП з єдиним кодом гілки ...
Олівер Чарльворт

152
ОГО!!! Це призвело до покращення масштабу в моєму додатку для цього конкретного рядка коду. Заздалегідь обчислюючи верхню та нижню частину мого профілювання, пішло від 25% часу цієї функції до менш ніж 2%! Пляшка зараз - це операції додавання та віднімання, але я думаю, що це може бути зараз досить добре :)
jjxtra

28
Ага, тепер @PsychoDad оновив питання, зрозуміло, чому це швидше. Реальний код має побічний ефект в порівнянні, тому компілятор не може оптимізувати коротке замикання геть.
Олівер Чарльворт

17

Рідко можна зробити значні оптимізації для кодування в такому невеликому масштабі. Великі підвищення продуктивності приходять від спостереження та зміни коду з вищого рівня. Можливо, ви зможете повністю усунути необхідність випробування діапазону або зробити лише O (n) з них замість O (n ^ 2). Можливо, ви зможете повторно замовити тести так, щоб одна сторона нерівності завжди малася на увазі. Навіть якщо алгоритм ідеальний, виграш швидше прийде, коли ви побачите, як цей код проводить тест діапазону в 10 мільйонів разів, і ви знайдете спосіб їх пакетного використання та використання SSE для того, щоб робити багато тестів паралельно.


16
Незважаючи на голосування, я стою проти своєї відповіді: Згенерована збірка (див. Посилання pastebin у коментарі до прийнятої відповіді) є досить жахливою для чогось у внутрішньому циклі функції обробки пікселів. Прийнята відповідь - це акуратний трюк, але його драматичний ефект є далеко не тим, що розумно очікувати для усунення частки гілки за ітерацію. Якийсь вторинний ефект є домінуючим, і я все ще сподіваюся, що спроба оптимізувати весь процес у рамках цього одного тесту залишить у пилу переваги розумного порівняння діапазону.
Бен Джексон

17

Це залежить від того, скільки разів ви хочете виконати тест над одними і тими ж даними.

Якщо ви виконуєте тест одноразово, ймовірно, немає значущого способу прискорити алгоритм.

Якщо ви робите це для дуже обмеженого набору значень, то ви можете створити таблицю пошуку. Виконання індексації може бути дорожчим, але якщо ви можете помістити всю таблицю в кеш, то ви можете видалити всі розгалуження з коду, що повинно прискорити роботу.

Для ваших даних таблиця пошуку складе 128 ^ 3 = 2,097,152. Якщо ви можете керувати однією з трьох змінних, щоб ви врахували всі випадки, коли start = Nсвого часу розмір робочого набору зменшується до 128^2 = 16432байтів, що має добре відповідати в більшості сучасних кеш-пам'яток.

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


Таким чином, ви б зберігали якийсь пошук із заданим значенням, початком і кінцем, і він містив би BOOL, який повідомляє вам, чи був він між ними?
jjxtra

Правильно. Було б перегляд таблиці 3D: bool between[start][end][x]. Якщо ви знаєте, як виглядатиме ваш шаблон доступу (наприклад, x монотонно зростає), ви можете спроектувати таблицю для збереження локальності, навіть якщо вся таблиця не вміщується в пам'яті.
Ендрю Прок

Я побачу, чи можу я обійтись, щоб спробувати цей метод і побачити, як це відбувається. Я планую робити це з бітним вектором на рядок, де буде встановлено біт, якщо точка знаходиться в колі. Думаєте, що це буде швидше, ніж байт чи int32 проти біт-маскування?
jjxtra

2

Ця відповідь полягає у звіті про тестування, проведене за допомогою прийнятої відповіді. Я провів тест закритого діапазону на великому векторі відсортованого випадкового цілого числа, і на мій подив, основний метод (низький <= число & & число <= високий) насправді швидший, ніж прийнятий відповідь вище! Тест проводився на HP Pavilion g6 (AMD A6-3400APU з 6 ГБ оперативної пам’яті. Ось основний код, який використовується для тестування:

int num = rand();  // num to compare in consecutive ranges.
chrono::time_point<chrono::system_clock> start, end;
auto start = chrono::system_clock::now();

int inBetween1{ 0 };
for (int i = 1; i < MaxNum; ++i)
{
    if (randVec[i - 1] <= num && num <= randVec[i])
        ++inBetween1;
}
auto end = chrono::system_clock::now();
chrono::duration<double> elapsed_s1 = end - start;

порівняно з наступним, що є прийнятою відповіддю вище:

int inBetween2{ 0 };
for (int i = 1; i < MaxNum; ++i)
{
    if (static_cast<unsigned>(num - randVec[i - 1]) <= (randVec[i] - randVec[i - 1]))
        ++inBetween2;
}

Зверніть увагу, що randVec - це відсортований вектор. Для будь-якого розміру MaxNum перший метод б'є другий на моїй машині!


1
Мої дані не відсортовані, і мої тести на процесорі iPhone arm. Ваші результати з різними даними та процесором можуть відрізнятися.
jjxtra

відсортовано в моєму тесті було лише для того, щоб переконатися, що верхня межа не менше нижньої межі.
rezeli

1
Впорядковані числа означають, що передбачення гілок буде дуже надійним, і всі гілки будуть правильними, за винятком кількох у точках переключення. Перевага безрозгалужуваного коду полягає в тому, що він позбудеться подібних викривлень щодо непередбачуваних даних.
Андреас Клебінгер

0

Для будь-якої перевірки змінних діапазонів:

if (x >= minx && x <= maxx) ...

Швидше використовувати бітову операцію:

if ( ((x - minx) | (maxx - x)) >= 0) ...

Це дозволить скоротити дві гілки в одну.

Якщо ви дбаєте про безпечний тип:

if ((int32_t)(((uint32_t)x - (uint32_t)minx) | ((uint32_t)maxx - (uint32_t)x)) > = 0) ...

Ви можете комбінувати більше перевірки змінної дальності разом:

if (( (x - minx) | (maxx - x) | (y - miny) | (maxy - y) ) >= 0) ...

Це зменшить 4 гілки на 1.

Це в 3,4 рази швидше, ніж старий в gcc:

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


-4

Чи не можливо просто виконати побітну операцію на ціле число?

Оскільки він повинен бути між 0 і 128, якщо 8-й біт встановлений (2 ^ 7), він становить 128 або більше. Крайній випадок буде болем, оскільки ви хочете всебічне порівняння.


3
Він хоче знати, якщо x <= end, де end <= 128. Ні x <= 128.
Бен Войгт

1
Це твердження " Оскільки воно повинно бути між 0 і 128, якщо 8-й біт встановлено (2 ^ 7), це 128 або більше ", є помилковим. Розгляньте 256.
Happy Green Kid Naps

1
Так, мабуть, я не вважав це достатньо. Вибачте.
крижана вода
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.