Эффективное внедрение целочисленного / евклидова целочисленного деления - PullRequest
20 голосов
/ 05 ноября 2010

Флористическое деление - это когда результат всегда смещен вниз (к −∞), а не к 0:

division types

Можно ли эффективно реализовать целочисленное или евклидово целочисленное деление в C / C ++?

(очевидное решение - проверить знак дивиденда)

Ответы [ 6 ]

8 голосов
/ 06 ноября 2010

Я написал тестовую программу для сравнения представленных здесь идей:

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

#define N 10000000
#define M 100

int dividends[N], divisors[N], results[N];

__forceinline int floordiv_signcheck(int a, int b)
{
    return (a<0 ? a-(b-1) : a) / b;
}

__forceinline int floordiv_signcheck2(int a, int b)
{
    return (a - (a<0 ? b-1 : 0)) / b;
}

__forceinline int floordiv_signmultiply(int a, int b)
{
    return (a + (a>>(sizeof(a)*8-1))*(b-1)) / b;
}

__forceinline int floordiv_floatingpoint(int a, int b)
{
    // I imagine that the call to floor can be replaced to a cast
    // if you can get FPU rounding control to work (I couldn't).
    return floor((double)a / b);
}

void main()
{
    for (int i=0; i<N; i++)
    {
        dividends[i] = rand();
        do
            divisors[i] = rand();
        while (divisors[i]==0);
    }

    LARGE_INTEGER t0, t1;

    QueryPerformanceCounter(&t0);
    for (int j=0; j<M; j++)
        for (int i=0; i<N; i++)
            results[i] = floordiv_signcheck(dividends[i], divisors[i]);
    QueryPerformanceCounter(&t1);
    printf("signcheck    : %9llu\n", t1.QuadPart-t0.QuadPart);

    QueryPerformanceCounter(&t0);
    for (int j=0; j<M; j++)
        for (int i=0; i<N; i++)
            results[i] = floordiv_signcheck2(dividends[i], divisors[i]);
    QueryPerformanceCounter(&t1);
    printf("signcheck2   : %9llu\n", t1.QuadPart-t0.QuadPart);

    QueryPerformanceCounter(&t0);
    for (int j=0; j<M; j++)
        for (int i=0; i<N; i++)
            results[i] = floordiv_signmultiply(dividends[i], divisors[i]);
    QueryPerformanceCounter(&t1);
    printf("signmultiply : %9llu\n", t1.QuadPart-t0.QuadPart);

    QueryPerformanceCounter(&t0);
    for (int j=0; j<M; j++)
        for (int i=0; i<N; i++)
            results[i] = floordiv_floatingpoint(dividends[i], divisors[i]);
    QueryPerformanceCounter(&t1);
    printf("floatingpoint: %9llu\n", t1.QuadPart-t0.QuadPart);
}

Результаты:

signcheck    :  61458768
signcheck2   :  61284370
signmultiply :  61625076
floatingpoint: 287315364

Итак, по моим результатам, проверка знака является самой быстрой:

(a - (a<0 ? b-1 : 0)) / b
3 голосов
/ 22 апреля 2016

Я возвращаюсь к этому вопросу пять лет спустя, так как это актуально и для меня.Я провел несколько измерений производительности для двух версий pure-C и двух версий inline-ассемблера для x86-64, и результаты могут быть интересными.

Протестированные варианты разделения по этажам:

  1. Реализация, которую я использовал в течение некоторого времени;
  2. Небольшой вариант представленного выше, который использует только одно деление;
  3. Предыдущий, но реализованный вручную в inline-монтаж;и
  4. A CMOV версия, реализованная в сборке.

Ниже приведена моя программа тестирования:

#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>

#ifndef VARIANT
#define VARIANT 3
#endif

#if VARIANT == 0
#define floordiv(a, b) (((a) < 0)?((((a) + 1) / (b)) - 1):((a) / (b)))
#elif VARIANT == 1
#define floordiv(a, b) ((((a) < 0)?((a) - ((b) - 1)):(a)) / (b))
#elif VARIANT == 2
#define floordiv(a, b) ({                                   \
    int result;                                             \
    asm("test %%eax, %%eax; jns 1f; sub %1, %%eax;"         \
        "add $1, %%eax; 1: cltd; idivl %1;"                 \
        : "=a" (result)                                     \
        : "r" (b),                                          \
          "0" (a)                                           \
        : "rdx");                                           \
    result;})
#elif VARIANT == 3
#define floordiv(a, b) ({                                           \
    int result;                                                     \
    asm("mov %%eax, %%edx; sub %1, %%edx; add $1, %%edx;"           \
        "test %%eax, %%eax; cmovs %%edx, %%eax; cltd;"              \
        "idivl %1;"                                                 \
        : "=a" (result)                                             \
        : "r" (b),                                                  \
          "0" (a)                                                   \
        : "rdx");                                                   \
    result;})
#endif

double ntime(void)
{
    struct timeval tv;

    gettimeofday(&tv, NULL);
    return(tv.tv_sec + (((double)tv.tv_usec) / 1000000.0));
}

void timediv(int n, int *p, int *q, int *r)
{
    int i;

    for(i = 0; i < n; i++)
        r[i] = floordiv(p[i], q[i]);
}

int main(int argc, char **argv)
{
    int n, i, *q, *p, *r;
    double st;

    n = 10000000;
    p = malloc(sizeof(*p) * n);
    q = malloc(sizeof(*q) * n);
    r = malloc(sizeof(*r) * n);
    for(i = 0; i < n; i++) {
        p[i] = (rand() % 1000000) - 500000;
        q[i] = (rand() % 1000000) + 1;
    }

    st = ntime();
    for(i = 0; i < 100; i++)
        timediv(n, p, q, r);
    printf("%g\n", ntime() - st);
    return(0);
}

Я скомпилировал ее с gcc -march=native -Ofast с помощью GCC4.9.2, и результаты на моем Core i5-2400 были следующими.Результаты довольно воспроизводимы от запуска к бегу - они всегда приземляются в одном и том же порядке, по крайней мере.

  • Вариант 0: 7,21 секунды
  • Вариант 1: 7,26 секунды
  • Вариант 2: 6,73 секунды
  • Вариант 3: 4,32 секунды

Таким образом, реализация CMOV, по крайней мере, выбрасывает остальных из воды.Что удивляет меня, так это то, что вариант 2 превосходит свою версию на чистом C (вариант 1) с довольно большим отрывом.Я бы подумал, что компилятор должен иметь возможность генерировать код по крайней мере так же эффективно, как мой.

Вот несколько других платформ для сравнения:

AMD Athlon 64 X2 4200+, GCC 4.7.2:

  • вариант 0: 26,33 секунды
  • вариант 1: 25,38 секунды
  • вариант 2: 25,19 секунды
  • вариант 3: 22,39 секунды

Xeon E3-1271 v3, GCC 4.9.2:

  • Вариант 0: 5,95 секунды
  • Вариант 1: 5,62 секунды
  • Вариант 2: 5,40 секунды
  • Вариант 3: 3,44 секунды

В качестве последнего замечания я, возможно, должен предостеречь от слишком серьезного восприятия очевидного преимущества в производительности версии CMOV,потому что в реальном мире ветвление в других версиях, вероятно, не будет настолько случайным, как в этом тесте, и если предиктор ветвления может выполнить разумную работу, версии ветвления могут оказаться лучше.Однако реалии этого будут во многом зависеть от данных, которые используются на практике, и поэтому, вероятно, бессмысленно пытаться выполнить какой-либо общий тест.

2 голосов
/ 05 ноября 2010

Может быть эффективнее создать что-то бесплатное, чтобы скорректировать результат на основе знака, так как ветви дорогие.

См. Страницу 20ff Глава 2 в Восхищение Хакера о том, как получить доступ к знаку.

1 голос
/ 14 января 2012

Замечание: инструкция x86 sar выполняет деление по этажам, когда дело доходит до двух степеней.

1 голос
/ 05 ноября 2010

Можно ли эффективно реализовать целочисленное или евклидово целочисленное деление в C / C ++?

Да.

(очевидным решением является проверказнак дивидендов)

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

0 голосов
/ 05 ноября 2010

Поскольку IEEE-754 указывает округление в сторону -inf в качестве одного из необходимых режимов округления, я полагаю, что ответ на ваш вопрос - да.Но, возможно, вы можете объяснить, хотите ли вы знать, как можно реализовать процедуру, если бы вы писали компилятор, или знать, как использовать определенный компилятор для выполнения операции?

...