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 ay m > 0. Sin embargo, se puede generalizar fácilmente a todos ay 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 -DNDEBUGAMD 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 ay 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;
}
rs que calculas son iguales entre sí.
a % bson bmucho 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 bmás grande, y su versión puede ser más rápida en muchas CPU en esas situaciones.