Какое самое быстрое целочисленное деление поддерживает деление на ноль, независимо от результата?

Резюме:

Я ищу самый быстрый способ вычисления

(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.

Ответ 1

Вдохновленный некоторыми комментариями, я избавился от ветки на моем процессоре 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

версия Philipp с 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 полностью реализуется с помощью предикатного выполнения.

Ответ 2

Вот некоторые конкретные номера в 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 не имеет недостатка по сравнению с другими проверками, и она сохраняет преимущества, поскольку нули становятся более распространенными.

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

Ответ 3

Не зная платформы, нет способа узнать наиболее эффективный метод, однако в общей системе это может приближаться к оптимальному (используя синтаксис ассемблера Intel):

(предположим, что divisor находится в ecx, а дивиденд находится в eax)

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

Четыре неразветвленных однотактных инструкций плюс разделить. Фактор будет в eax, а остаток будет в конце edx в конце. (Это показывает, почему вы не хотите отправлять компилятор для выполнения задания).

Ответ 4

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

Это самый быстрый подход, если деление на нулевые ошибки крайне редко: вы платите только за деления на ноль, а не за допустимые деления, нормальный путь выполнения вообще не изменяется.

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