Быстрый пол целочисленного деления со знаком в C / C++

В C может быть сделано разделение на пол, например:

int floor_div(int a, int b) {
    int d = a / b;
    if (a < 0 != b < 0) {  /* negative output (check inputs since 'd' isn't floored) */
        if (d * a != b) {  /* avoid modulo, use multiply instead */
            d -= 1;        /* floor */
        }
    }
    return d;
}

Но, похоже, это можно упростить.

Есть ли более эффективный способ сделать это в C?


Обратите внимание, что это почти противоположность этого вопроса: быстрый потолок целочисленного деления в C / C++

5 ответов

Решение

Этажное деление может быть выполнено с использованием деления и по модулю.

Нет причин избегать вызова по модулю, поскольку современные компиляторы оптимизируют разделение и деление по модулю.

int floor_div(int a, int b) {
    int d = a / b;
    int r = a % b;  /* optimizes into single division. */
    return r ? (d - ((a < 0) ^ (b < 0))) : d;
}

Я думаю, меньше инструкций по сборке в сгенерированном коде и более быстрый путь к результату.

Для машин RISC с огромным количеством регистров этот лучше, так как ветвей вообще нет, и это хорошо для конвейера и кеша.

Для x86 на самом деле это не имеет значения.

int floor_div3(int a, int b) {
    int d = a / b;


    return d * b == a ? d : d - ((a < 0) ^ (b < 0));
}

div() функции в стандарте C

Я думаю, вы должны посмотреть на div() функции от <stdlib.h>, (Они являются стандартными функциями C и определены во всех версиях стандарта, несмотря на ссылку на спецификацию POSIX.)

Стандарт C11 §7.22.6.2 определяет:

div … Функции вычисляют numer / denom а также numer % denom в одной операции.

Обратите внимание, что C11 определяет целочисленное деление в §6.5.5 (и C99 был похож):

Когда целые числа делятся, результат / оператор - это алгебраический фактор, любая дробная часть которого отбрасывается. 105)

105) Это часто называют "усечением до нуля".

но C90 (§6.3.5) был более гибким, но менее полезным:

Когда целые числа делятся и деление является неточным. если оба операнда положительны, результат / оператор является наибольшим целым числом меньше, чем алгебраический фактор и результат % Оператор положительный. Если любой операнд отрицательный, является ли результат / оператор является наибольшим целым числом, меньшим или равным алгебраическому частному, или наименьшим целым числом, большим или равным алгебраическому частному, определяется реализацией, как и знак результата % оператор.

floor_div()

Вычислительный код для запрошенного floor_div() с помощью div() аккуратный и опрятный.

int floor_div(int a, int b)
{
    assert(b != 0);
    div_t r = div(a, b);
    if (r.rem != 0 && ((a < 0) ^ (b < 0)))
        r.quot--;
    return r.quot;
}

Тестовый код

Форматирование печати в приведенном ниже коде довольно точно адаптировано к образцу данных. (Было бы лучше, но более обширно, чтобы использовать %4d а также %-4d на протяжении). Этот код печатает строки длиной 89 символов плюс перевод строки; более общий макет будет печатать строки длиной 109. Ни один из них не избегает горизонтальной полосы прокрутки на SO.

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

static int floor_div(int a, int b)
{
    assert(b != 0);
    div_t r = div(a, b);
    if (r.rem != 0 && ((a < 0) ^ (b < 0)))
        r.quot--;
    return r.quot;
}

static void test_floor_div(int n, int d)
{
    assert(d != 0);
    printf(   "%3d/%-2d = %-3d (%3d)", +n, +d, floor_div(+n, +d), +n / +d);
    printf(";  %3d/%-3d = %-4d (%4d)", +n, -d, floor_div(+n, -d), +n / -d);
    if (n != 0)
    {
        printf(";  %4d/%-2d = %-4d (%4d)", -n, +d, floor_div(-n, +d), -n / +d);
        printf(";  %4d/%-3d = %-3d (%3d)", -n, -d, floor_div(-n, -d), -n / -d);
    }
    putchar('\n');
}

int main(void)
{
    int numerators[] = { 0, 1, 2, 4, 9, 23, 291 };
    enum { NUM_NUMERATORS = sizeof(numerators) / sizeof(numerators[0]) };
    int denominators[] = { 1, 2, 3, 6, 17, 23 };
    enum { NUM_DENOMINATORS = sizeof(denominators) / sizeof(denominators[0]) };

    for (int i = 0; i < NUM_NUMERATORS; i++)
    {
        for (int j = 0; j < NUM_DENOMINATORS; j++)
            test_floor_div(numerators[i], denominators[j]);
        putchar('\n');
    }

    return 0;
}

Тестовый вывод

  0/1  = 0   (  0);    0/-1  = 0    (   0)
  0/2  = 0   (  0);    0/-2  = 0    (   0)
  0/3  = 0   (  0);    0/-3  = 0    (   0)
  0/6  = 0   (  0);    0/-6  = 0    (   0)
  0/17 = 0   (  0);    0/-17 = 0    (   0)
  0/23 = 0   (  0);    0/-23 = 0    (   0)

  1/1  = 1   (  1);    1/-1  = -1   (  -1);    -1/1  = -1   (  -1);    -1/-1  = 1   (  1)
  1/2  = 0   (  0);    1/-2  = -1   (   0);    -1/2  = -1   (   0);    -1/-2  = 0   (  0)
  1/3  = 0   (  0);    1/-3  = -1   (   0);    -1/3  = -1   (   0);    -1/-3  = 0   (  0)
  1/6  = 0   (  0);    1/-6  = -1   (   0);    -1/6  = -1   (   0);    -1/-6  = 0   (  0)
  1/17 = 0   (  0);    1/-17 = -1   (   0);    -1/17 = -1   (   0);    -1/-17 = 0   (  0)
  1/23 = 0   (  0);    1/-23 = -1   (   0);    -1/23 = -1   (   0);    -1/-23 = 0   (  0)

  2/1  = 2   (  2);    2/-1  = -2   (  -2);    -2/1  = -2   (  -2);    -2/-1  = 2   (  2)
  2/2  = 1   (  1);    2/-2  = -1   (  -1);    -2/2  = -1   (  -1);    -2/-2  = 1   (  1)
  2/3  = 0   (  0);    2/-3  = -1   (   0);    -2/3  = -1   (   0);    -2/-3  = 0   (  0)
  2/6  = 0   (  0);    2/-6  = -1   (   0);    -2/6  = -1   (   0);    -2/-6  = 0   (  0)
  2/17 = 0   (  0);    2/-17 = -1   (   0);    -2/17 = -1   (   0);    -2/-17 = 0   (  0)
  2/23 = 0   (  0);    2/-23 = -1   (   0);    -2/23 = -1   (   0);    -2/-23 = 0   (  0)

  4/1  = 4   (  4);    4/-1  = -4   (  -4);    -4/1  = -4   (  -4);    -4/-1  = 4   (  4)
  4/2  = 2   (  2);    4/-2  = -2   (  -2);    -4/2  = -2   (  -2);    -4/-2  = 2   (  2)
  4/3  = 1   (  1);    4/-3  = -2   (  -1);    -4/3  = -2   (  -1);    -4/-3  = 1   (  1)
  4/6  = 0   (  0);    4/-6  = -1   (   0);    -4/6  = -1   (   0);    -4/-6  = 0   (  0)
  4/17 = 0   (  0);    4/-17 = -1   (   0);    -4/17 = -1   (   0);    -4/-17 = 0   (  0)
  4/23 = 0   (  0);    4/-23 = -1   (   0);    -4/23 = -1   (   0);    -4/-23 = 0   (  0)

  9/1  = 9   (  9);    9/-1  = -9   (  -9);    -9/1  = -9   (  -9);    -9/-1  = 9   (  9)
  9/2  = 4   (  4);    9/-2  = -5   (  -4);    -9/2  = -5   (  -4);    -9/-2  = 4   (  4)
  9/3  = 3   (  3);    9/-3  = -3   (  -3);    -9/3  = -3   (  -3);    -9/-3  = 3   (  3)
  9/6  = 1   (  1);    9/-6  = -2   (  -1);    -9/6  = -2   (  -1);    -9/-6  = 1   (  1)
  9/17 = 0   (  0);    9/-17 = -1   (   0);    -9/17 = -1   (   0);    -9/-17 = 0   (  0)
  9/23 = 0   (  0);    9/-23 = -1   (   0);    -9/23 = -1   (   0);    -9/-23 = 0   (  0)

 23/1  = 23  ( 23);   23/-1  = -23  ( -23);   -23/1  = -23  ( -23);   -23/-1  = 23  ( 23)
 23/2  = 11  ( 11);   23/-2  = -12  ( -11);   -23/2  = -12  ( -11);   -23/-2  = 11  ( 11)
 23/3  = 7   (  7);   23/-3  = -8   (  -7);   -23/3  = -8   (  -7);   -23/-3  = 7   (  7)
 23/6  = 3   (  3);   23/-6  = -4   (  -3);   -23/6  = -4   (  -3);   -23/-6  = 3   (  3)
 23/17 = 1   (  1);   23/-17 = -2   (  -1);   -23/17 = -2   (  -1);   -23/-17 = 1   (  1)
 23/23 = 1   (  1);   23/-23 = -1   (  -1);   -23/23 = -1   (  -1);   -23/-23 = 1   (  1)

291/1  = 291 (291);  291/-1  = -291 (-291);  -291/1  = -291 (-291);  -291/-1  = 291 (291)
291/2  = 145 (145);  291/-2  = -146 (-145);  -291/2  = -146 (-145);  -291/-2  = 145 (145)
291/3  = 97  ( 97);  291/-3  = -97  ( -97);  -291/3  = -97  ( -97);  -291/-3  = 97  ( 97)
291/6  = 48  ( 48);  291/-6  = -49  ( -48);  -291/6  = -49  ( -48);  -291/-6  = 48  ( 48)
291/17 = 17  ( 17);  291/-17 = -18  ( -17);  -291/17 = -18  ( -17);  -291/-17 = 17  ( 17)
291/23 = 12  ( 12);  291/-23 = -13  ( -12);  -291/23 = -13  ( -12);  -291/-23 = 12  ( 12)

Остаток от "деления по полу" либо равен 0, либо имеет тот же знак, что и делитель.

(the proof)  
a: dividend  b: divisor
q: quotient  r: remainder

q = floor(a/b)
a = q * b + r  
r = a - q * b = (a/b - q) * b  
                ~~~~~~~~~
                    ^ this factor in [0, 1)

И, к счастью, результат / а также % в C/C++ стандартизирован как "усеченный до нуля" после C99/C++11. (до этого библиотечная функция div в С и std::div в C++ играл те же роли).

Давайте сравним "деление по полу" и "усеченное деление", сосредоточив внимание на диапазоне остатка:

       "floor"     "truncate"
b>0    [0, b-1]    [-b+1, b-1]
b<0    [b+1, 0]    [b+1, -b-1]

Для удобства обсуждения:

  • пусть a, b = дивиденд и делитель;
  • пусть q, r = частное и остаток от "деления по полу";
  • пусть q0, r0 = частное и остаток от "усеченного деления".

Предположим, что b>0, и, к сожалению, r0 находится в [-b+1, -1]. Однако мы можем получить r довольно легко: r = r0+b, и r гарантированно находится в [1, b-1], что находится внутри диапазона "floor". То же самое верно для случая b<0.

Теперь, когда мы можем исправить остаток, мы также можем исправить частное. Правило простое: мы добавляем b к r 0, затем мы должны вычесть 1 из q0.

В завершение, реализация "разделения пола" в C++11:

void floor_div(int& q, int& r, int a, int b)
{
    int q0 = a / b;
    int r0 = a % b;
    if (b > 0){
        q = r0 >= 0 ? q0 : q0 - 1;
        r = r0 >= 0 ? r0 : r0 + b;
    }
    else {
        q = r0 <= 0 ? q0 : q0 - 1;
        r = r0 <= 0 ? r0 : r0 + b;
    }
}

По сравнению со знаменитым (a < 0) ^ (b < 0) Метод, этот метод имеет преимущество: если делитель является константой времени компиляции, для исправления результатов требуется только одно сравнение.

Безответственная версия:

      int floor_div(int x, int y)
{
    return x / y - (x % y != 0) * ((x < 0) ^ (y < 0));
}

https://godbolt.org/z/9n3637brs

Я вижу, что для других ответов компилятор генерирует условные переходы, а это означает более медленный код.

Провел некоторые тесты производительности, и результаты интересны:

Проверка кода, сгенерированного clang, показывает, что clang смог оптимизировать удаленные ветки (s и тернарный оператор)

Итак, это показывает: сначала измеряйте при оптимизации :).

Другие вопросы по тегам