2010-11-04 14 views
20

Floored de manera eficiente la implementación de solado es cuando el resultado siempre se redondea hacia abajo (hacia -∞), no hacia 0:/euclidiana número entero división de la división

division types

¿Es posible aplicar de manera eficiente división entera con suelo o euclidiana en C/C++?

(la solución obvia es comprobar la señal del dividendo)

Respuesta

2

Volveré a esta cuestión cinco años después, ya que esto también es relevante para mí. Hice algunas mediciones de rendimiento en dos versiones de C puro y dos versiones de ensamblaje en línea para x86-64, y los resultados pueden ser interesantes.

Las variantes ensayadas de división con suelo son:

  1. La implementación He estado usando desde hace algún tiempo;
  2. La pequeña variante de la presentada arriba que solo usa una división;
  3. La anterior, pero implementada a mano en ensamblaje en línea; y
  4. A CMOV versión implementada en el ensamblaje.

La siguiente es mi programa de referencia:

#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); 
} 

Compilé esto con gcc -march=native -Ofast usando GCC 4.9.2, y los resultados, en mi Core i5-2400, fueron los siguientes. Los resultados son bastante reproducibles de ejecución a ejecución: siempre aterrizan en el mismo orden, al menos.

  • variante 0: 7.21 segundos
  • Variante 1: 7,26 segundos
  • Variante 2: 6,73 segundos
  • Variante 3: 4,32 segundos

Así que la aplicación CMOV sopla los otros fuera de el agua, al menos. Lo que me sorprende es que la variante 2 supera su versión de C puro (variante 1) por un margen bastante amplio. Pensé que el compilador debería poder emitir un código al menos tan eficiente como el mío.

Aquí hay algunas otras plataformas, para la comparación:

AMD Athlon 64 X2 4200+, GCC 4.7.2:

  • Variant 0: 26.33 segundos
  • Variante 1: 25,38 segundos
  • Variante 2: 25.19 segundos
  • Variante 3: 22.39 segundos
v3

Xeon E3-1271, GCC 4.9.2:

  • Variant 0: 5.95 segundos
  • Variante 1: 5,62 segundos
  • Variante 2: 5,40 segundos
  • Variante 3: 3,44 segundos
+0

Tenga en cuenta que las variantes 0 y 1 (no verificaron las demás) solo dan los resultados numéricamente correctos para la división del piso cuando el divisor es positivo. (Y en ese caso, los resultados también coinciden con la división euclidiana.) Si, por ejemplo, a, b = 5, -3, estas fórmulas dicen que el cociente es -1, pero el resultado para la división del piso debe ser -2. (La división euclidiana para 5, -3 es -1, pero estas fórmulas dan una respuesta diferente a la del piso y la división euclidiana cuando ambos operandos son negativos.) – dubiousjim

+0

¿Ha comparado versiones donde el divisor es una constante positiva (pero el dividendo es calculado en tiempo de ejecución)? Para los dividendos del poder de dos, el operador de cambio a la derecha sería casi tan rápido como cualquier alternativa, y probablemente más legible que cualquier cosa que sea tan eficiente. Para otros dividendos, el código óptimo para la división suelo/euclidiana debería ser más rápido que el código para truncado, pero no conozco ninguna forma confiable de convencer a los compiladores para generar un buen código. – supercat

+0

@supercat: Para los divisores constantes en tiempo de compilación, el compilador podrá generar un mejor código ya sea que se trate de un poder-de-dos o no, usando [este truco] (https://stackoverflow.com/questions/30790184/perform) -integer-division-using-multiplication). Entonces, sí, las divisiones con divisores constantes en tiempo de compilación serán ciertamente más rápidas que cualquier algoritmo que maneje divisores dinámicos. – Dolda2000

0

Desde IEEE-754 especifica vuelta hacia -inf como uno de los modos de redondeo requeridas Imagino que la respuesta a su pregunta es sí. Pero quizás pueda explicar si desea saber cómo se implementaría el procedimiento si se estuviera escribiendo el compilador, o si se sabe cómo usar un compilador particular para realizar la operación.

+0

¡Ups, quise decir división entera! Voy a enmendar la pregunta. No estoy seguro de qué tienen que ver los compiladores de escritura con la pregunta, ya que lo he comprobado y C/C++ hace una división truncada. –

+0

Se está volviendo complicado –

+0

@CyberShadow: así que vierte el divisor y el dividendo a números de coma flotante de algún tipo, usa el piso y vuelve el resultado a un número entero. ¿O buscas otro tipo de respuesta? –

1

¿Es posible implementar eficientemente la división de enteros con piso o euclidiano en C/C++?

Sí.

(la solución obvia es comprobar la señal del dividendo)

estoy completamente de acuerdo, y podría encontrar difícil de creer que existe una alternativa que es significativamente más rápido.

2

Podría ser más eficiente crear algo libre de ramas para corregir el resultado en función del signo, ya que las ramas son caras.

Consulte la página 20ff de Chapter 2 en Hacker's Delight sobre cómo acceder al letrero.

+0

Lo único que se me ocurre es la multiplicación del bit de signo con el divisor. –

+0

Puede representar ciertas condiciones por 0 o 1 y agregarlas al resultado. – starblue

8

He escrito un programa de prueba de referencia para las ideas presentadas aquí:

#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); 
} 

Resultados:

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

Así, de acuerdo con mis resultados, comprobando el signo es el más rápido:

(a - (a<0 ? b-1 : 0))/b 
+0

Su tiempo para todos menos el primero incluye el 'printf' para la respuesta anterior. 'printf' es un poco lento, no sé si es lo suficientemente lento como para afectar los resultados o no. –

+0

Es posible que también se vea afectado por la toma de decisiones en línea tomadas por el compilador, vale la pena mirar el ensamblado generado. Pruebe 'double' en lugar de' flotar' ya que también pueden haber conversiones allí. –

+0

Gracias por las sugerencias, he actualizado el programa. El uso de 'double' provocó que floatingpoint se ejecute un poco más rápido, pero no por mucho. De lo contrario, el resultado no cambió mucho. –

1

Solo una nota: la instrucción x86 sar realiza una gran división cuando se trata de poderes de dos.