J'ai remarqué une chose curieuse sur mon ordinateur. * Le test de divisibilité manuscrit est beaucoup plus rapide que l' %opérateur. Prenons l'exemple minimal:
* 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);
}
L'exemple est limité par impair aet m > 0. Cependant, il peut être facilement généralisé à tous aet m. Le code convertit simplement la division en une série d'ajouts.
Considérons maintenant le programme de test compilé avec -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
}
}
... et les résultats sur mon ordinateur:
| implementation | time [secs] |
|--------------------|-------------|
| divisible_ui_p | 8.52user |
| builtin % operator | 17.61user |
Donc plus de 2 fois plus rapide.
La question: pouvez-vous me dire comment se comporte le code sur votre machine? Est-ce une opportunité d'optimisation manquée dans GCC? Pouvez-vous faire ce test encore plus rapidement?
MISE À JOUR: Comme demandé, voici un exemple reproductible minimal:
#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;
}
compilé avec gcc -std=c99 -march=native -O3 -DNDEBUGsur AMD Ryzen Threadripper 2990WX avec
gcc --version
gcc (Gentoo 9.2.0-r2 p3) 9.2.0
UPDATE2: Comme demandé, la version qui peut gérer tout aet m(si vous voulez également éviter le débordement d'entier, le test doit être implémenté avec un type entier deux fois plus long que les entiers d'entrée):
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 vous calculez sont effectivement égaux l'un à l'autre.
a % bsont bbeaucoup plus petites que a. Dans la plupart des itérations de votre scénario de test, elles sont de taille similaire ou bplus grande, et votre version peut être plus rapide sur de nombreux processeurs dans ces situations.