Noté algo curioso en mi computadora. * La prueba de divisibilidad escrita a mano es significativamente más rápida que la del %
operador. Considere el ejemplo mínimo:
* AMD Ryzen Threadripper 2990WX, GCC 9.2.0
static int divisible_ui_p(unsigned int m, unsigned int a)
{
if (m <= a) {
if (m == a) {
return 1;
}
return 0;
}
m += a;
m >>= __builtin_ctz(m);
return divisible_ui_p(m, a);
}
El ejemplo está limitado por impar a
y m > 0
. Sin embargo, se puede generalizar fácilmente a todos a
y m
. El código simplemente convierte la división en una serie de adiciones.
Ahora considere el programa de prueba compilado con -std=c99 -march=native -O3
:
for (unsigned int a = 1; a < 100000; a += 2) {
for (unsigned int m = 1; m < 100000; m += 1) {
#if 1
volatile int r = divisible_ui_p(m, a);
#else
volatile int r = (m % a == 0);
#endif
}
}
... y los resultados en mi computadora:
| implementation | time [secs] |
|--------------------|-------------|
| divisible_ui_p | 8.52user |
| builtin % operator | 17.61user |
Por lo tanto, más de 2 veces más rápido.
La pregunta: ¿Me puede decir cómo se comporta el código en su máquina? ¿Se pierde la oportunidad de optimización en GCC? ¿Puedes hacer esta prueba aún más rápido?
ACTUALIZACIÓN: según lo solicitado, aquí hay un ejemplo mínimo reproducible:
#include <assert.h>
static int divisible_ui_p(unsigned int m, unsigned int a)
{
if (m <= a) {
if (m == a) {
return 1;
}
return 0;
}
m += a;
m >>= __builtin_ctz(m);
return divisible_ui_p(m, a);
}
int main()
{
for (unsigned int a = 1; a < 100000; a += 2) {
for (unsigned int m = 1; m < 100000; m += 1) {
assert(divisible_ui_p(m, a) == (m % a == 0));
#if 1
volatile int r = divisible_ui_p(m, a);
#else
volatile int r = (m % a == 0);
#endif
}
}
return 0;
}
compilado con gcc -std=c99 -march=native -O3 -DNDEBUG
AMD Ryzen Threadripper 2990WX con
gcc --version
gcc (Gentoo 9.2.0-r2 p3) 9.2.0
ACTUALIZACIÓN2: según lo solicitado, la versión que puede manejar cualquiera a
y m
(si también desea evitar el desbordamiento de enteros, la prueba debe implementarse con un tipo de entero dos veces mayor que los enteros de entrada):
int divisible_ui_p(unsigned int m, unsigned int a)
{
#if 1
/* handles even a */
int alpha = __builtin_ctz(a);
if (alpha) {
if (__builtin_ctz(m) < alpha) {
return 0;
}
a >>= alpha;
}
#endif
while (m > a) {
m += a;
m >>= __builtin_ctz(m);
}
if (m == a) {
return 1;
}
#if 1
/* ensures that 0 is divisible by anything */
if (m == 0) {
return 1;
}
#endif
return 0;
}
r
s que calculas son iguales entre sí.
a % b
son b
mucho más pequeños que a
. A través de la mayoría de las iteraciones en su caso de prueba, son de tamaño similar o b
más grande, y su versión puede ser más rápida en muchas CPU en esas situaciones.