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
sontb
beaucoup plus petites quea
. Dans la plupart des itérations de votre scénario de test, elles sont de taille similaire oub
plus grande, et votre version peut être plus rapide sur de nombreux processeurs dans ces situations.Réponses:
Ce que vous faites s'appelle la réduction de la force: remplacer une opération coûteuse par une série d'opérations bon marché.
L'instruction mod sur de nombreux processeurs est lente, car historiquement elle n'a pas été testée dans plusieurs benchmarks courants et les concepteurs ont donc optimisé d'autres instructions à la place. Cet algorithme fonctionnera moins bien s'il doit effectuer plusieurs itérations, et
%
fonctionnera mieux sur un processeur où il n'a besoin que de deux cycles d'horloge.Enfin, sachez qu'il existe de nombreux raccourcis pour prendre le reste de la division par des constantes spécifiques. (Bien que les compilateurs s'en occupent généralement pour vous.)
la source
div
/idiv
qui ont obtenu un certain amour dans Intel Penryn, Broadwell et IceLake (diviseurs matériels radix supérieurs)x = i * const
chaque itération, vous effectuezx += const
chaque itération. Je ne pense pas que remplacer une seule multiplication par une boucle shift / add serait appelé réduction de force. en.wikipedia.org/wiki/… dit que le terme peut peut-être être utilisé de cette façon, mais avec une note "Ce matériel est contesté. Il est mieux décrit comme l'optimisation de judas et l'affectation d'instructions."Je répondrai moi-même à ma question. Il semble que je suis devenu victime de la prédiction de branche. La taille mutuelle des opérandes ne semble pas avoir d'importance, seulement leur ordre.
Considérez l'implémentation suivante
et les tableaux
qui sont / ne sont pas mélangés à l'aide de la fonction de lecture aléatoire .
Sans mélange, les résultats sont toujours
Cependant, une fois que je mélange ces tableaux, les résultats sont différents
la source