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 a
et m > 0
. Cependant, il peut être facilement généralisé à tous a
et 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 -DNDEBUG
sur 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 a
et 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;
}
r
s que vous calculez sont effectivement égaux l'un à l'autre.
a % b
sont b
beaucoup plus petites que a
. Dans la plupart des itérations de votre scénario de test, elles sont de taille similaire ou b
plus grande, et votre version peut être plus rapide sur de nombreux processeurs dans ces situations.