Rapide de multiplication/division par 2 pour des flotteurs et des doubles (C/C++)
Dans le logiciel, je suis en train d'écrire, je suis en train de faire des millions de multiplication ou de division par 2 (ou puissances de 2) de mes valeurs. J'aimerais vraiment que ces valeurs int
pour que je puisse accéder à la bitshift opérateurs
int a = 1;
int b = a<<24
Cependant, je ne peux pas, et je dois coller avec du double.
Ma question est : comme il y a une représentation standard des doubles (signe, exposant mantisse), est-il un moyen de jouer avec l'exposant pour obtenir rapidement des multiplications/divisions par une puissance de 2?
Je peut même supposer que le nombre de bits va être fixe (le logiciel fonctionne sur les ordinateurs qui ont toujours 64 bits de long double)
P. S : Et oui, l'algorithme surtout n'ces opérations. C'est le goulot d'étranglement (c'est déjà multithread).
Edit : Ou suis-je totalement dans l'erreur et intelligent compilateurs déjà d'optimiser les choses pour moi?
Des résultats temporaires (avec Qt pour mesurer le temps, overkill, mais je n'ai pas de soins):
#include <QtCore/QCoreApplication>
#include <QtCore/QElapsedTimer>
#include <QtCore/QDebug>
#include <iostream>
#include <math.h>
using namespace std;
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
while(true)
{
QElapsedTimer timer;
timer.start();
int n=100000000;
volatile double d=12.4;
volatile double D;
for(unsigned int i=0; i<n; ++i)
{
//D = d*32; //200 ms
//D = d*(1<<5); //200 ms
D = ldexp (d,5); //6000 ms
}
qDebug() << "The operation took" << timer.elapsed() << "milliseconds";
}
return a.exec();
}
Pistes suggèrent que D = d*(1<<5);
et D = d*32;
exécuter dans le même temps (200 ms) alors que D = ldexp (d,5);
est beaucoup plus lent (6000 ms). Je savoir que c'est un micro de référence, et que tout à coup, ma RAM a explosé parce que Chrome a soudainement demandé de calcul de Pi dans mon dos chaque fois que je lance ldexp()
, donc ce test ne vaut rien. Mais je vais le garder quand même.
Sur l'autre, je vais avoir du mal à faire reinterpret_cast<uint64_t *>
parce qu'il y a un const
violation (qui semble être le volatile
mot-clé interfère)
- Ne présumez pas que c'est le goulot d'étranglement juste parce que c'est multithread. Nous avons eu une application multithread que nous avons trouvé était un goulot d'étranglement dans de nombreux endroits différents de ce que nous attendions? Comment exactement avez-vous profiler?
- Comme toujours, l'application n'est pas profilé assez. Je veux dire, j'ai utilisé CacheGrind, et il semble que je passe la plupart de mon temps dans une fonction qui effectue principalement des multiplications. Il semble. Mais j'ai écrit que c'était le goulot d'étranglement parce que je suis plus intéressé par les idées théoriques derrière la multiplication par 2 que dans "la petite considérations" (bien sûr, je pourrais optimiser mes requêtes SQL, mais honnêtement, je suis assez sûr que ça va être vide de sens, comparativement à la multiplication des trucs, et, surtout, je n'aime pas ^^)
- Rappelez-vous,
1<<5
est une constante, de sorte que le compilateur va optimiserd*32
. - Ouais, je "sais" ^^ Mais il y a eu des discussions sur la façon de camoufler le compilateur avec des trucs bizarres (en gros, certaines opérations se résument à
*32
mais le compilateur ne veut pas "voir" c'). Et il y a juste une ligne =) - En général, l'accès à la mémoire et les requêtes SQL sont beaucoup beaucoup plus lent alors des choses comme des multiplications en C++. Je suis désolé de dire cela, mais de "petite considérations" est peut-être précisément ce que sont la cause de votre goulot d'étranglement.
- Que puis-je dire d'autre que :
I used CacheGrind, and it seems I spend most of my time in a function that does mostly multiplications
? Oui, bien sûr, la fonction rend également plus, et de répartir des données sur la pile, mais je pense avoir une bonne estimation de la situation. - 1) Votre boucle besoins de dérouler. 2) CacheGrind dit que vous êtes pour la plupart dans des calculs de routine? C'est une alarme! En une seule étape le code à l'assemblée, le niveau de langue et assurez-vous qu'il fait rien de plus que ce que vous attendiez. Il ne devrait pas être l'appel de quoi que ce soit. 3) le Multi-threading n'est pas de rendre le code plus rapide. Il a juste étend sur plus de processeurs, au mieux. 4) Si le rendement est ce que vous aimez, apprendre cette technique.
Vous devez vous connecter pour publier un commentaire.
Vous pouvez supposer IEEE 754 mise en forme, les détails de ce qui peut être assez gnarley (esp. lorsque vous arrivez dans subnormals). Dans la plupart des cas, cependant, cela devrait fonctionner:
EDIT: Après avoir fait un certain timing, cette méthode est étrangement plus lent que le double de la méthode sur mon compilateur et de la machine, même dépouillé au minimum le code exécuté:
Dans le DOUBLE_SHIFT complète de 1,6 secondes, avec une boucle interne de
Par rapport à 2,4 secondes, autrement, avec une boucle interne de:
Vraiment inattendue!
EDIT 2: Mystère résolu! L'un des changements pour VC11 est maintenant toujours vectorizes virgule flottante boucles, obligeant /arch:SSE2, si VC10, même avec /arch:SSE2 est encore pire avec la version 3.0 secondes avec une boucle interne de:
VC10 sans /arch:SSE2 (même avec /arch:ESS) est de 5,3 secondes... au 1/100e de la itérations!!, boucle intérieure:
Je savais que le x87 FP pile a été vraiment mauvais, mais 500 fois pire, c'est un peu ridicule. Vous n'aurez probablement pas voir ces sortes de la vitesse de la conversion, c'est à dire la matrice de la fpo à l'ESS ou l'int des hacks, puisque c'est le pire des cas de chargement dans le FP de la pile, faire une op, et le stockage, mais c'est un bon exemple de pourquoi x87 est pas la voie à suivre pour n'importe quoi perf. liés à.
add
est de 50% plus lent quemulpd
, j'ai même essayé de 64bit ajouter en cas de 32bit lire, 64bit étape a été de confondre le prefetcher, c'est toujours à peu près la même vitesse!addl
s c'est 50% plus lent qu'unmulpd
, si - et rappelez-vous que vous utilisez un entier unité d'exécution pour la variable de boucle incrément de trop, de sorte que certains de la prestation vient d'utiliser une autre unité d'exécution.emms
? Re: la vitesse de l'intadd
vs vecteur SSE2: La boucle va être un goulot d'étranglement en magasin-port de débit. C'est pourquoi d'exploitation sur 16B à un temps gagne. Vous pourriez faire un int ajouter avec l'ESS.paddq
oupaddd
(peut fonctionner sur port1/port5, 1 cycle de latence, de 0,5 c recip débit)C'est un de ces très spécifique de l'application des choses. Il peut aider dans certains cas et pas dans d'autres. (Dans la grande majorité des cas, une simple multiplication est encore mieux.)
La "intuitive" façon de faire c'est juste pour extraire les bits d'un entier de 64 bits et ajouter la valeur de décalage directement dans l'exposant. (cela fonctionne aussi longtemps que vous n'avez pas touché NAN ou INF)
Donc quelque chose comme ceci:
Noter que ce code n'est pas C-conforme en quelque sorte, et est indiqué seulement pour illustrer l'idée. Toute tentative de mettre en œuvre ce qui devrait être fait directement à l'assemblée ou intrinsèques SSE.
Cependant, dans plus cas, les frais généraux de déplacer les données de la FP de l'unité à l'entier de l'unité (et de retour) va coûter beaucoup plus que de simplement faire une multiplication d'emblée. C'est particulièrement le cas pour les pré-ESS époque où la valeur doit être stockée de la FPU x87 en mémoire et puis lisez en entier registres.
Dans l'ESS ère, l'Entier de l'ESS et de la FP de l'ESS utiliser le même ISA registres (s'ils ont encore de registre distinct des fichiers). Selon le Agner Brouillard, il y a de 1 à 2 cycle de pénalité pour déplacer des données entre l'Entier de l'ESS et de la FP de l'ESS des unités d'exécution. De sorte que le coût est beaucoup mieux que le x87 époque, mais il est toujours là.
Tout-en-tout, cela dépendra de ce que vous avez sur votre pipeline. Mais dans la plupart des cas, multipliant sera encore plus rapide. J'ai exécuté ce exactement le même problème avant, donc je parle en expérience de première main.
Maintenant avec 256-bit AVX instructions qui prennent uniquement en charge instructions de FP, il y a encore moins d'une incitation à jouer des tours comme ça.
double
n'est pas garanti d'être en 64 bits IEEE 754 en C?Comment sur ldexp?
Une demi-décent compilateur va générer un code optimal sur votre plate-forme.
Mais comme @Clinton points, il vous suffit de l'écrire dans les "évident" devrait faire aussi bien. La multiplication et la division par puissances de deux, c'est un jeu d'enfant pour un compilateur moderne.
Directement munging la représentation à virgule flottante, en plus d'être non-portable, sera presque certainement pas plus vite (et qui risque d'être plus lent).
Et bien sûr, vous ne devriez pas perdre de temps à penser à propos de cette question, à moins que votre outil de profilage vous dit de vous. Mais le genre de personnes qui écoutent ce conseil ne sera jamais en avoir besoin, et ceux qui en ont besoin, il ne sera jamais de l'écouter.
[mise à jour]
OK, alors j'ai essayé ldexp avec g++ 4.5.2. Le
cmath
en-tête inlines comme un appel à__builtin_ldexp
, qui à son tour......émet un appel à la libm
ldexp
fonction. J'aurais pensé que ce builtin serait trivial à optimiser, mais je suppose que les développeurs de GCC n'a jamais eu autour d'elle.Donc, en multipliant par
1 << p
est probablement votre meilleur pari, que vous avez découvert._control87()
,_controlfp()
, etc...). Essayez de changer la précision en virgule flottante commutateurs de compilateur...Le moyen le plus rapide de le faire est probablement:
Ce genre de chose qui peut simplement être fait par l'appel d'une instruction machine pour ajouter
p
à l'exposant. Dire que le compilateur au lieu d'extraire les quelques morceaux avec un masque et de faire quelque chose à la main sera probablement rendre les choses plus lentement, pas plus rapide.Rappelez-vous, C/C++ n'est pas le langage d'assemblage. À l'aide d'un bitshift l'exploitant n'a pas nécessairement de la compilation d'un bitshift de montage d'opération, ne pas utiliser de multiplication nécessairement compiler à la multiplication. Il y a toutes sortes d'étranges et merveilleux passe des choses comme ce que les registres sont utilisés et à quelles instructions peuvent être exécutées simultanément dont je ne suis pas assez intelligent pour le comprendre. Mais votre compilateur, avec beaucoup de l'homme des années de connaissances et d'expérience et beaucoup de puissance de calcul, est beaucoup mieux à faire de ces jugements.
p.s. Gardez à l'esprit, si votre double sont dans un tableau ou une autre plate structure de données, votre compilateur peut être vraiment intelligents et de l'utilisation de l'ESS à des multiples de 2, voire 4 doubles en même temps. Cependant, en faisant beaucoup de décalage de bits va probablement confondre votre compilateur et de prévenir ce type d'optimisation.
fscale
exactement ce que fait, mais seulement pour undouble
d'entrée, pas entier. Il nex * (1<<trunc(y))
, oux_exponent += trunc(y)
. Il n'est pas rapide mais: beaucoup plus lent quefmul
, à l'instar de 20 à 32 cycles d'horloge classique P5 Pentium vs 3 cycles pour unfmul
, et il n'est pas beaucoup mieux sur moderne x86. (agner.org/optimize). Doncfmul
avec une constante de0.5
est bien mieux.paddd xmm0, xmm1
pour un entier ajouter sur FP modèles de bits entre les instructions commemulps xmm0, xmm2
. Bien sûr, cela ne veut pas gérer le cas où l'exposant sort de la plage, où FP multiplier serait de vous donner l'infini, ou de corriger dépassement de capacité d'un subnormale. (Ou conditionnement de la désinformation-exposant de 0 (subnormale) à tous ceux (NaN ou l'infini selon significande.)Ce que d'autres opérations n'cet algorithme requiert? Vous pourriez être en mesure de briser votre flotte en int paires (//signe de la mantisse et de l'ampleur), faites de votre traitement, et de les reconstituer à la fin.
+
,-
,*
, ...)En multipliant par 2 peut être remplacé par un plus:
x *= 2
est équivalent àx += x
.Division par 2 peut être remplacée par une multiplication par 0.5. La Multiplication est généralement beaucoup plus rapide que la division.
x *= 33554432
Bien qu'il y a peu/pas d'intérêt pratique pour le traitement des puissances de deux, spécialement pour les float double types, il y a un cas pour cette pour double-double types. Double-double de la multiplication et de la division de l'est compliqué en général, mais est trivial pour la multiplication et la division par une puissance de deux.
E. g. pour
En fait, j'ai surchargé
<<
et>>
pourdoubledouble
de sorte qu'il est analogue à entiers.En fonction de ce que vous êtes en multipliant, si vous avez des données qui est assez récurrent, une table peut fournir une meilleure performance, au détriment de la mémoire.