Яке найшвидше ціле ділення, що підтримує ділення на нуль, незалежно від результату?


109

Підсумок:

Я шукаю найшвидший спосіб розрахунку

(int) x / (int) y

не отримуючи винятку для y==0. Натомість я просто хочу довільний результат.


Фон:

При кодуванні алгоритмів обробки зображень мені часто потрібно ділити на (накопичене) значення альфа. Найпростіший варіант - звичайний код C з цілою арифметикою. Моя проблема полягає в тому, що я зазвичай отримую поділ на нульову помилку для пікселів результатів alpha==0. Однак це саме пікселі, де результат зовсім не має значення: мені не важливо значення кольорів пікселів alpha==0.


Деталі:

Я шукаю щось на кшталт:

result = (y==0)? 0 : x/y;

або

result = x / MAX( y, 1 );

x і y - натуральні числа. Код виконується величезна кількість разів у вкладеному циклі, тому я шукаю спосіб позбутися від умовного розгалуження.

Коли y не перевищує діапазон байтів, я задоволений рішенням

unsigned char kill_zero_table[256] = { 1, 1, 2, 3, 4, 5, 6, 7, [...] 255 };
[...]
result = x / kill_zero_table[y];

Але це, очевидно, не дуже добре для великих діапазонів.

Я думаю, що остаточне питання полягає в тому, що найшвидший біт-подвійний злом зміни 0 на будь-яке інше ціле значення, залишаючи без змін усі інші значення?


Роз'яснення

Я не на 100% впевнений, що розгалуження занадто дороге. Однак використовуються різні компілятори, тому я віддаю перевагу бенчмаркінгу з невеликими оптимізаціями (що справді сумнівно).

Напевно, компілятори чудові, коли справа доходить до подвійного скручування, але я не можу виразити результат "не байдуже" на C, тому компілятор ніколи не зможе використовувати весь спектр оптимізацій.

Код повинен бути повністю сумісним C, основними платформами є Linux 64 біт з gcc & clang та MacOS.


22
Як ви визначили, що гілка if занадто дорога?
djechlin

7
Як ви визначили , що там є філія?
leemes

13
+1 для профілювання, але для сучасного прогнозування філій вам це не знадобиться. Крім того, чому ви кодуєте власні алгоритми обробки зображень?
TC1

8
"Який найшвидший битовий хакер ..." Можливо y += !y? Для обчислення цього не потрібно жодної гілки. Ви можете порівняти x / (y + !y)проти, x / max(y, 1)а може, і y ? (x/y) : 0. Я думаю, що в жодному з них не буде відділення, принаймні з увімкненими оптимізаціями.
leemes

6
Кожен, хто думає, що сучасне передбачення гілок означає, що вам не доведеться цього робити, недостатньо профільований код усунення гілки, який працює на рівні пікселя. Сучасне передбачення галузей прийнятне, якщо альфа- 0секції є величезними та суміжними. Тут є місце для обертання мікро-оптимізаціями, а операції за пікселем - саме це місце.
Якк - Адам Невраумон

Відповіді:


107

Натхнений деякими коментарями, я позбувся гілки на моєму Pentium та gccкомпіляторі за допомогою

int f (int x, int y)
{
        y += y == 0;
        return x/y;
}

Компілятор в основному визнає, що може додатково використовувати прапор умови тесту.

Згідно запиту складання:

.globl f
    .type   f, @function
f:
    pushl   %ebp
    xorl    %eax, %eax
    movl    %esp, %ebp
    movl    12(%ebp), %edx
    testl   %edx, %edx
    sete    %al
    addl    %edx, %eax
    movl    8(%ebp), %edx
    movl    %eax, %ecx
    popl    %ebp
    movl    %edx, %eax
    sarl    $31, %edx
    idivl   %ecx
    ret

Оскільки це виявилося настільки популярним питанням та відповіддю, я детальніше розробимо детальніше. Наведений вище приклад заснований на ідіомі програмування, яку розпізнає компілятор. У наведеному вище випадку в інтегральній арифметиці використовується булевий вираз, а для цього в технічних засобах винайдено використання прапорів стану. У загальному випадку прапори доступні лише на C за допомогою ідіоми. Ось чому так важко зробити портативну цілочисленну бібліотеку з великою точністю в C, не вдаючись до (вбудованої) збірки. Я здогадуюсь, що більшість порядних упорядників зрозуміють вищезгадану ідіому.

Ще одним способом уникнення гілок, як також зазначалося в деяких з вищезазначених коментарів, є передбачуване виконання. Тому я взяв перший код і мій код philipp і провів його через компілятор від ARM та компілятор GCC для архітектури ARM, який має передбачуване виконання. Обидва компілятори уникають гілки в обох зразках коду:

Версія Philipp з компілятором ARM:

f PROC
        CMP      r1,#0
        BNE      __aeabi_idivmod
        MOVEQ    r0,#0
        BX       lr

Версія Філіпа з GCC:

f:
        subs    r3, r1, #0
        str     lr, [sp, #-4]!
        moveq   r0, r3
        ldreq   pc, [sp], #4
        bl      __divsi3
        ldr     pc, [sp], #4

Мій код із компілятором ARM:

f PROC
        RSBS     r2,r1,#1
        MOVCC    r2,#0
        ADD      r1,r1,r2
        B        __aeabi_idivmod

Мій код із GCC:

f:
        str     lr, [sp, #-4]!
        cmp     r1, #0
        addeq   r1, r1, #1
        bl      __divsi3
        ldr     pc, [sp], #4

Усі версії все ще потребують розгалуження до підрозділу поділу, оскільки ця версія ARM не має апаратного забезпечення для поділу, але тест для y == 0цього повністю реалізований за допомогою передбачуваного виконання.


Чи можете ви показати нам отриманий код асемблера? Або як ти визначив, що немає відділення?
Haatschii

1
Дивовижно. Можна зробити constexprі уникнути непотрібних template<typename T, typename U> constexpr auto fdiv( T t, U u ) -> decltype(t/(u+!u)) { return t/(u+!u); }255(lhs)/(rhs+!rhs) & -!rhs
ролей

1
@leemes , але я мав в виду , |НЕ &. Ooops - ( (lhs)/(rhs+!rhs) ) | -!rhsслід встановити значення, 0xFFFFFFFякщо rhsє 0, і lhs/rhsякщо rhs!=0.
Якк - Адам Невраумон

1
Це було дуже розумно.
Теодорос Чатзіґянакікіс

1
Чудова відповідь! Я зазвичай вдаюсь до зборів для подібних речей, але це завжди жахливо підтримувати (не кажучи вже про менш портативний;)).
Лев

20

Ось конкретні цифри для Windows, що використовують GCC 4.7.2:

#include <stdio.h>
#include <stdlib.h>

int main()
{
  unsigned int result = 0;
  for (int n = -500000000; n != 500000000; n++)
  {
    int d = -1;
    for (int i = 0; i != ITERATIONS; i++)
      d &= rand();

#if CHECK == 0
    if (d == 0) result++;
#elif CHECK == 1
    result += n / d;
#elif CHECK == 2
    result += n / (d + !d);
#elif CHECK == 3
    result += d == 0 ? 0 : n / d;
#elif CHECK == 4
    result += d == 0 ? 1 : n / d;
#elif CHECK == 5
    if (d != 0) result += n / d;
#endif
  }
  printf("%u\n", result);
}

Зауважте, що я навмисно не дзвоню srand(), так що rand()завжди повертається абсолютно однакові результати. Зауважимо також, що -DCHECK=0лише підраховує нулі, так що очевидно, як часто з'являлися.

Тепер, компілюючи та розміщуючи його, можна різними способами:

$ for it in 0 1 2 3 4 5; do for ch in 0 1 2 3 4 5; do gcc test.cc -o test -O -DITERATIONS=$it -DCHECK=$ch && { time=`time ./test`; echo "Iterations $it, check $ch: exit status $?, output $time"; }; done; done

показує вихід, який можна підсумувати в таблиці:

Iterations  | 0        | 1        | 2        | 3         | 4         | 5
-------------+-------------------------------------------------------------------
Zeroes       | 0        | 1        | 133173   | 1593376   | 135245875 | 373728555
Check 1      | 0m0.612s | -        | -        | -         | -         | -
Check 2      | 0m0.612s | 0m6.527s | 0m9.718s | 0m13.464s | 0m18.422s | 0m22.871s
Check 3      | 0m0.616s | 0m5.601s | 0m8.954s | 0m13.211s | 0m19.579s | 0m25.389s
Check 4      | 0m0.611s | 0m5.570s | 0m9.030s | 0m13.544s | 0m19.393s | 0m25.081s
Check 5      | 0m0.612s | 0m5.627s | 0m9.322s | 0m14.218s | 0m19.576s | 0m25.443s

Якщо нулі рідкісні, -DCHECK=2версія працює погано. Оскільки нулі починають з’являтися більше, -DCHECK=2справа починає виконуватись значно краще. Від інших варіантів різниці різниці не буває.

Бо -O3, однак, це вже інша історія:

Iterations  | 0        | 1        | 2        | 3         | 4         | 5
-------------+-------------------------------------------------------------------
Zeroes       | 0        | 1        | 133173   | 1593376   | 135245875 | 373728555
Check 1      | 0m0.646s | -        | -        | -         | -         | -
Check 2      | 0m0.654s | 0m5.670s | 0m9.905s | 0m14.238s | 0m17.520s | 0m22.101s
Check 3      | 0m0.647s | 0m5.611s | 0m9.085s | 0m13.626s | 0m18.679s | 0m25.513s
Check 4      | 0m0.649s | 0m5.381s | 0m9.117s | 0m13.692s | 0m18.878s | 0m25.354s
Check 5      | 0m0.649s | 0m6.178s | 0m9.032s | 0m13.783s | 0m18.593s | 0m25.377s

Там чек 2 не має недоліків порівняно з іншими чеками, і він зберігає переваги, оскільки нулі стають все більш поширеними.

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


4
Зробіть 50% записів d=0випадковим чином, замість того, щоб робити це майже завжди d!=0, і ви побачите більше помилок у прогнозуванні галузей. Прогнозування гілок чудово, якщо за однією гілкою майже завжди слідкують, або якщо наступна гілка чи інша справді незграбна ...
Як - Адам Невраумон

@Yakk dІтерація - це внутрішня петля, тому d == 0випадки розподіляються рівномірно. І чи d == 0реалістично зробити 50% справ ?

2
чи реально зробити 0.002%справи d==0? Вони розповсюджуються по всіх кожні 65000 ітерацій, у яких ви потрапили у свій d==0випадок. Хоча , 50%можливо , не часто трапляється, 10%або 1%може легко статися, або навіть 90%чи 99%. Тест, що відображається, справді тестує "якщо ви ніколи не спускаєтесь з гілки, чи не передбачає гілка безрезультатне видалення гілки?", На що відповідь "так, але це не цікаво".
Якк - Адам Невраумон

1
Ні, тому що відмінності будуть ефективно непомітні через шум.
Джо

3
Розподіл нулів не стосується розподілу, виявленого в ситуації запитувача. Зображення, що містять суміш 0 альфа та інших, мають отвори або неправильної форми, але (як правило) це не шум. Припустити, що ви нічого не знаєте про дані (і вважаєте це шумом) - помилка. Це програма в реальному світі з фактичними зображеннями, які можуть мати 0 альфа. А оскільки ряд пікселів, ймовірно, має або всі a = 0, або всі a> 0, скористатися передбачуванням гілок може бути найшвидшим, особливо коли a = 0 відбувається багато і (повільний) поділ (15+ циклів) !) уникають.
DDS

13

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

(припустимо, дільник є, ecxа дивіденд - eax)

mov ebx, ecx
neg ebx
sbb ebx, ebx
add ecx, ebx
div eax, ecx

Чотири нерозгалужені інструкції на один цикл плюс розділення. Коефіцієнт з'явиться, eaxа решта буде в edxкінці. (Цей вид показує, чому ви не хочете відправляти компілятора для виконання чоловічої роботи).



1
це не робить поділу, воно просто забруднює дільник, так що поділ на нуль неможливо
Тайлер Дерден,

@Jens Timmerman Вибачте, я написав це, перш ніж додати заяву div. Я оновив текст.
Тайлер Дерден

1

Відповідно до цього посилання , ви можете просто заблокувати сигнал SIGFPE sigaction()(я сам не пробував, але я вважаю, що він повинен працювати).

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

Однак ОС буде брати участь у кожному виключенні, яке ігнорується, що коштує дорого. Я думаю, у вас повинно бути принаймні тисяча добрих поділів на нуль, які ви ігноруєте. Якщо винятки частіші за них, ви, ймовірно, платите більше, ігноруючи винятки, ніж перевіряючи кожне значення перед поділом.

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