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:
- La implementación He estado usando desde hace algún tiempo;
- La pequeña variante de la presentada arriba que solo usa una división;
- La anterior, pero implementada a mano en ensamblaje en línea; y
- 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
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
¿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
@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