Notre bibliothèque C++ utilise actuellement time_t pour stocker les valeurs de temps. Je commence à avoir besoin d'une précision inférieure à une seconde à certains endroits, un type de données plus important sera donc nécessaire de toute façon. En outre, il pourrait être utile de contourner le problème de l'an 2038 à certains endroits. Je pense donc à passer complètement à une seule classe Time avec une valeur int64_t sous-jacente, pour remplacer la valeur time_t à tous les endroits.
Maintenant, je me pose des questions sur l'impact sur les performances d'un tel changement lors de l'exécution de ce code sur un système d'exploitation 32 bits ou un processeur 32 bits. IIUC le compilateur générera du code pour effectuer une arithmétique 64 bits à l'aide de registres 32 bits. Mais si cela est trop lent, je devrai peut-être utiliser une méthode plus différenciée pour traiter les valeurs de temps, ce qui pourrait rendre le logiciel plus difficile à maintenir.
Ce qui m'intéresse:
Je suis principalement intéressé par g ++ 4.1 et 4.4 sur Linux 2.6 (RHEL5, RHEL6) sur les systèmes Intel Core 2; mais il serait également agréable de connaître la situation pour d'autres systèmes (comme Sparc Solaris + Solaris CC, Windows + MSVC).
quels facteurs influencent la performance de ces opérations? Probablement le compilateur et la version du compilateur; mais le système d'exploitation ou la marque/le modèle du processeur influencent-ils cela également?
Surtout l'architecture du processeur (et le modèle - veuillez lire le modèle où je mentionne l'architecture du processeur dans cette section). Le compilateur peut avoir une certaine influence, mais la plupart des compilateurs s'en sortent assez bien, donc l'architecture du processeur aura une plus grande influence que le compilateur.
Le système d'exploitation n'aura aucune influence (à part "si vous changez le système d'exploitation, vous devez utiliser un type de compilateur différent qui change ce que fait le compilateur" dans certains cas - mais c'est probablement un petit effet).
Un système 32 bits normal utilisera-t-il les registres 64 bits des processeurs modernes?
Ce n'est pas possible. Si le système est en mode 32 bits, il agira comme un système 32 bits, les 32 bits supplémentaires des registres sont complètement invisibles, tout comme il le serait si le système était en fait un "vrai système 32 bits" .
quelles opérations seront particulièrement lentes lors d'une émulation sur 32 bits? Ou qui n'aura presque aucun ralentissement?
L'addition et la soustraction sont pires car elles doivent être effectuées en séquence de deux opérations, et la deuxième opération nécessite que la première soit terminée - ce n'est pas le cas si le compilateur produit simplement deux opérations d'ajout sur des données indépendantes.
La mulitplication sera bien pire si les paramètres d'entrée sont en fait 64 bits - donc 2 ^ 35 * 83 est pire que 2 ^ 31 * 2 ^ 31, par exemple. Cela est dû au fait que le processeur peut assez bien produire une multiplication 32 x 32 bits en un résultat 64 bits - environ 5 à 10 cycles d'horloge. Mais une multiplication 64 x 64 bits nécessite un peu de code supplémentaire, donc cela prendra plus de temps.
La division est un problème similaire à la multiplication - mais ici, il est OK de prendre une entrée 64 bits d'un côté, de la diviser par une valeur 32 bits et d'obtenir une valeur 32 bits. Comme il est difficile de prédire quand cela fonctionnera, la division 64 bits est probablement presque toujours lente.
Les données prendront également deux fois plus d'espace cache, ce qui peut avoir un impact sur les résultats. Et comme conséquence similaire, l'attribution générale et la transmission de données prendront deux fois plus longtemps que le minimum, car il y a deux fois plus de données à exploiter.
Le compilateur devra également utiliser plus de registres.
existe-t-il des résultats de référence existants pour utiliser int64_t/uint64_t sur les systèmes 32 bits?
Probablement, mais je n'en connais aucun. Et même s'il y en a, cela ne serait que quelque peu significatif pour vous, car la combinaison des opérations est extrêmement critique pour la vitesse des opérations.
Si les performances sont un élément important de votre application, comparez VOTRE code (ou une partie représentative de celui-ci). Cela n'a pas vraiment d'importance si Benchmark X donne des résultats 5%, 25% ou 103% plus lents, si votre code est complètement différent plus lentement ou plus rapidement dans les mêmes circonstances.
quelqu'un a-t-il sa propre expérience de cet impact sur les performances?
J'ai recompilé du code qui utilise des entiers 64 bits pour l'architecture 64 bits, et j'ai constaté que les performances s'amélioraient considérablement - jusqu'à 25% sur certains bits de code.
Changer votre système d'exploitation pour une version 64 bits du même système d'exploitation, serait peut-être utile?
Éditer:
Parce que j'aime découvrir quelle est la différence dans ce genre de choses, j'ai écrit un peu de code, et avec un modèle primitif (j'apprends toujours ce bit - les modèles ne sont pas exactement mon sujet le plus chaud, je dois dire - donnez-moi bitfiddling et pointeur arithmétique, et je vais (généralement) bien faire les choses ...)
Voici le code que j'ai écrit, en essayant de répliquer quelques fonctions courantes:
#include <iostream>
#include <cstdint>
#include <ctime>
using namespace std;
static __inline__ uint64_t rdtsc(void)
{
unsigned hi, lo;
__asm__ __volatile__ ("rdtsc" : "=a"(lo), "=d"(hi));
return ( (uint64_t)lo)|( ((uint64_t)hi)<<32 );
}
template<typename T>
static T add_numbers(const T *v, const int size)
{
T sum = 0;
for(int i = 0; i < size; i++)
sum += v[i];
return sum;
}
template<typename T, const int size>
static T add_matrix(const T v[size][size])
{
T sum[size] = {};
for(int i = 0; i < size; i++)
{
for(int j = 0; j < size; j++)
sum[i] += v[i][j];
}
T tsum=0;
for(int i = 0; i < size; i++)
tsum += sum[i];
return tsum;
}
template<typename T>
static T add_mul_numbers(const T *v, const T mul, const int size)
{
T sum = 0;
for(int i = 0; i < size; i++)
sum += v[i] * mul;
return sum;
}
template<typename T>
static T add_div_numbers(const T *v, const T mul, const int size)
{
T sum = 0;
for(int i = 0; i < size; i++)
sum += v[i] / mul;
return sum;
}
template<typename T>
void fill_array(T *v, const int size)
{
for(int i = 0; i < size; i++)
v[i] = i;
}
template<typename T, const int size>
void fill_array(T v[size][size])
{
for(int i = 0; i < size; i++)
for(int j = 0; j < size; j++)
v[i][j] = i + size * j;
}
uint32_t bench_add_numbers(const uint32_t v[], const int size)
{
uint32_t res = add_numbers(v, size);
return res;
}
uint64_t bench_add_numbers(const uint64_t v[], const int size)
{
uint64_t res = add_numbers(v, size);
return res;
}
uint32_t bench_add_mul_numbers(const uint32_t v[], const int size)
{
const uint32_t c = 7;
uint32_t res = add_mul_numbers(v, c, size);
return res;
}
uint64_t bench_add_mul_numbers(const uint64_t v[], const int size)
{
const uint64_t c = 7;
uint64_t res = add_mul_numbers(v, c, size);
return res;
}
uint32_t bench_add_div_numbers(const uint32_t v[], const int size)
{
const uint32_t c = 7;
uint32_t res = add_div_numbers(v, c, size);
return res;
}
uint64_t bench_add_div_numbers(const uint64_t v[], const int size)
{
const uint64_t c = 7;
uint64_t res = add_div_numbers(v, c, size);
return res;
}
template<const int size>
uint32_t bench_matrix(const uint32_t v[size][size])
{
uint32_t res = add_matrix(v);
return res;
}
template<const int size>
uint64_t bench_matrix(const uint64_t v[size][size])
{
uint64_t res = add_matrix(v);
return res;
}
template<typename T>
void runbench(T (*func)(const T *v, const int size), const char *name, T *v, const int size)
{
fill_array(v, size);
uint64_t long t = rdtsc();
T res = func(v, size);
t = rdtsc() - t;
cout << "result = " << res << endl;
cout << name << " time in clocks " << dec << t << endl;
}
template<typename T, const int size>
void runbench2(T (*func)(const T v[size][size]), const char *name, T v[size][size])
{
fill_array(v);
uint64_t long t = rdtsc();
T res = func(v);
t = rdtsc() - t;
cout << "result = " << res << endl;
cout << name << " time in clocks " << dec << t << endl;
}
int main()
{
// spin up CPU to full speed...
time_t t = time(NULL);
while(t == time(NULL)) ;
const int vsize=10000;
uint32_t v32[vsize];
uint64_t v64[vsize];
uint32_t m32[100][100];
uint64_t m64[100][100];
runbench(bench_add_numbers, "Add 32", v32, vsize);
runbench(bench_add_numbers, "Add 64", v64, vsize);
runbench(bench_add_mul_numbers, "Add Mul 32", v32, vsize);
runbench(bench_add_mul_numbers, "Add Mul 64", v64, vsize);
runbench(bench_add_div_numbers, "Add Div 32", v32, vsize);
runbench(bench_add_div_numbers, "Add Div 64", v64, vsize);
runbench2(bench_matrix, "Matrix 32", m32);
runbench2(bench_matrix, "Matrix 64", m64);
}
Compilé avec:
g++ -Wall -m32 -O3 -o 32vs64 32vs64.cpp -std=c++0x
Et les résultats sont les suivants: Remarque: voir les résultats de 2016 ci-dessous - ces résultats sont légèrement optimistes en raison de la différence d'utilisation des instructions SSE en mode 64 bits, mais non SSE utilisation en mode 32 bits.
result = 49995000
Add 32 time in clocks 20784
result = 49995000
Add 64 time in clocks 30358
result = 349965000
Add Mul 32 time in clocks 30182
result = 349965000
Add Mul 64 time in clocks 79081
result = 7137858
Add Div 32 time in clocks 60167
result = 7137858
Add Div 64 time in clocks 457116
result = 49995000
Matrix 32 time in clocks 22831
result = 49995000
Matrix 64 time in clocks 23823
Comme vous pouvez le voir, l'addition et la multiplication ne sont pas bien pires. La division devient vraiment mauvaise. Fait intéressant, l'ajout de matrice ne fait pas beaucoup de différence.
Et est-ce plus rapide sur 64 bits, j'entends certains d'entre vous demander: en utilisant les mêmes options du compilateur, juste -m64 au lieu de -m32 - yupp, beaucoup plus rapide:
result = 49995000
Add 32 time in clocks 8366
result = 49995000
Add 64 time in clocks 16188
result = 349965000
Add Mul 32 time in clocks 15943
result = 349965000
Add Mul 64 time in clocks 35828
result = 7137858
Add Div 32 time in clocks 50176
result = 7137858
Add Div 64 time in clocks 50472
result = 49995000
Matrix 32 time in clocks 12294
result = 49995000
Matrix 64 time in clocks 14733
Edit, update for 2016: quatre variantes, avec et sans SSE, en mode 32 et 64 bits du compilateur.
J'utilise généralement clang ++ comme compilateur habituel ces jours-ci. J'ai essayé de compiler avec g ++ (mais ce serait toujours une version différente de celle ci-dessus, car j'ai mis à jour ma machine - et j'ai aussi un processeur différent). Étant donné que g ++ n'a pas réussi à compiler la version no-sse en 64 bits, je n'ai pas vu l'intérêt de cela. (g ++ donne quand même des résultats similaires)
Comme un petit tableau:
Test name | no-sse 32 | no-sse 64 | sse 32 | sse 64 |
----------------------------------------------------------
Add uint32_t | 20837 | 10221 | 3701 | 3017 |
----------------------------------------------------------
Add uint64_t | 18633 | 11270 | 9328 | 9180 |
----------------------------------------------------------
Add Mul 32 | 26785 | 18342 | 11510 | 11562 |
----------------------------------------------------------
Add Mul 64 | 44701 | 17693 | 29213 | 16159 |
----------------------------------------------------------
Add Div 32 | 44570 | 47695 | 17713 | 17523 |
----------------------------------------------------------
Add Div 64 | 405258 | 52875 | 405150 | 47043 |
----------------------------------------------------------
Matrix 32 | 41470 | 15811 | 21542 | 8622 |
----------------------------------------------------------
Matrix 64 | 22184 | 15168 | 13757 | 12448 |
Résultats complets avec options de compilation.
$ clang++ -m32 -mno-sse 32vs64.cpp --std=c++11 -O2
$ ./a.out
result = 49995000
Add 32 time in clocks 20837
result = 49995000
Add 64 time in clocks 18633
result = 349965000
Add Mul 32 time in clocks 26785
result = 349965000
Add Mul 64 time in clocks 44701
result = 7137858
Add Div 32 time in clocks 44570
result = 7137858
Add Div 64 time in clocks 405258
result = 49995000
Matrix 32 time in clocks 41470
result = 49995000
Matrix 64 time in clocks 22184
$ clang++ -m32 -msse 32vs64.cpp --std=c++11 -O2
$ ./a.out
result = 49995000
Add 32 time in clocks 3701
result = 49995000
Add 64 time in clocks 9328
result = 349965000
Add Mul 32 time in clocks 11510
result = 349965000
Add Mul 64 time in clocks 29213
result = 7137858
Add Div 32 time in clocks 17713
result = 7137858
Add Div 64 time in clocks 405150
result = 49995000
Matrix 32 time in clocks 21542
result = 49995000
Matrix 64 time in clocks 13757
$ clang++ -m64 -msse 32vs64.cpp --std=c++11 -O2
$ ./a.out
result = 49995000
Add 32 time in clocks 3017
result = 49995000
Add 64 time in clocks 9180
result = 349965000
Add Mul 32 time in clocks 11562
result = 349965000
Add Mul 64 time in clocks 16159
result = 7137858
Add Div 32 time in clocks 17523
result = 7137858
Add Div 64 time in clocks 47043
result = 49995000
Matrix 32 time in clocks 8622
result = 49995000
Matrix 64 time in clocks 12448
$ clang++ -m64 -mno-sse 32vs64.cpp --std=c++11 -O2
$ ./a.out
result = 49995000
Add 32 time in clocks 10221
result = 49995000
Add 64 time in clocks 11270
result = 349965000
Add Mul 32 time in clocks 18342
result = 349965000
Add Mul 64 time in clocks 17693
result = 7137858
Add Div 32 time in clocks 47695
result = 7137858
Add Div 64 time in clocks 52875
result = 49995000
Matrix 32 time in clocks 15811
result = 49995000
Matrix 64 time in clocks 15168
Plus que vous ne l'avez jamais voulu savoir sur les mathématiques 64 bits en mode 32 bits ...
Lorsque vous utilisez des nombres 64 bits en mode 32 bits (même sur un processeur 64 bits si un code est compilé pour 32 bits), ils sont stockés sous la forme de deux nombres 32 bits distincts, l'un stockant les bits supérieurs d'un nombre et un autre stockant des bits inférieurs. L'impact de cela dépend d'une instruction. (tl; dr - en général, faire des calculs 64 bits sur un processeur 32 bits est en théorie 2 fois plus lent, tant que vous ne divisez pas/modulo, mais en pratique, la différence sera plus petite (1,3x serait mon devinez), car généralement les programmes ne font pas seulement des calculs sur des entiers 64 bits, et aussi à cause du pipelining, la différence peut être beaucoup plus petite dans votre programme).
De nombreuses architectures prennent en charge ce qu'on appelle carry flag . Il est défini lorsque le résultat de l'addition déborde ou que le résultat de la soustraction ne dépasse pas. Le comportement de ces bits peut être montré avec une longue addition et une longue soustraction. C dans cet exemple montre soit un bit supérieur au bit représentable le plus élevé (pendant le fonctionnement), soit un indicateur de report (après le fonctionnement).
C 7 6 5 4 3 2 1 0 C 7 6 5 4 3 2 1 0
0 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0
+ 0 0 0 0 0 0 0 1 - 0 0 0 0 0 0 0 1
= 1 0 0 0 0 0 0 0 0 = 0 1 1 1 1 1 1 1 1
Pourquoi le drapeau de transport est-il pertinent? Eh bien, il se trouve que les CPU ont généralement deux opérations d'addition et de soustraction distinctes. Dans x86, les opérations d'addition sont appelées add
et adc
. add
signifie addition, tandis que adc
signifie addition avec report. La différence entre ceux-ci est que adc
considère un bit de retenue, et s'il est défini, il en ajoute un au résultat.
De même, la soustraction avec report soustrait 1 du résultat si le bit de report n'est pas défini.
Ce comportement permet d'implémenter facilement l'addition et la soustraction de taille arbitraire sur des entiers. Le résultat de l'addition de x et y (en supposant que ce sont 8- bit) n'est jamais plus grand que 0x1FE
. Si vous ajoutez 1
, vous obtenez 0x1FF
. 9 bits suffisent donc pour représenter les résultats de toute addition de 8 bits. Si vous commencez l'addition avec add
, puis ajoutez tous les bits au-delà de ceux initiaux avec adc
, vous pouvez effectuer l'ajout sur n'importe quelle taille de données que vous aimez.
L'ajout de deux valeurs 64 bits sur un processeur 32 bits est le suivant.
Analogiquement pour la soustraction.
Cela donne 2 instructions, cependant, en raison de instruction pipelinining , cela peut être plus lent que cela, car un calcul dépend de l'autre pour terminer, donc si le CPU n'a rien d'autre à faire que 64 ajout de bits, le processeur peut attendre que le premier ajout soit effectué.
Il arrive ainsi sur x86 que imul
et mul
puissent être utilisés de telle sorte que le débordement soit stocké dans edx S'inscrire. Par conséquent, multiplier deux valeurs 32 bits pour obtenir une valeur 64 bits est vraiment facile. Une telle multiplication est une instruction, mais pour l'utiliser, l'une des valeurs de multiplication doit être stockée dans eax .
Quoi qu'il en soit, pour un cas plus général de multiplication de deux valeurs 64 bits, elles peuvent être calculées à l'aide d'une formule suivante (supposer la fonction r supprime les bits au-delà de 32 morceaux).
Tout d'abord, il est facile de remarquer que les 32 bits inférieurs d'un résultat seront la multiplication de 32 bits inférieurs de variables multipliées. Cela est dû à la relation de congruence.
a 1 ≡ b 1 (mod n )
a 2 ≡ b 2 (mod n )
a 1 a 2 ≡ b 1 b 2 (mod n )
Par conséquent, la tâche se limite à déterminer uniquement les 32 bits supérieurs. Pour calculer les 32 bits supérieurs d'un résultat, les valeurs suivantes doivent être additionnées.
Cela donne environ 5 instructions, mais en raison du nombre relativement limité de registres dans x86 (en ignorant les extensions d'une architecture), ils ne peuvent pas trop tirer parti du pipelining. Activez SSE si vous souhaitez améliorer la vitesse de multiplication, car cela augmente le nombre de registres.
Je ne sais pas comment cela fonctionne, mais c'est beaucoup plus complexe que l'addition, la soustraction ou même la multiplication. Cependant, il est susceptible d'être dix fois plus lent que la division sur un processeur 64 bits. Consultez "L'art de la programmation informatique, Volume 2: Algorithmes séminariques", page 257 pour plus de détails si vous pouvez le comprendre (je ne peux pas d'une manière que je pourrais l'expliquer, malheureusement).
Si vous divisez par une puissance de 2, veuillez vous référer à la section décalage, car c'est essentiellement ce que le compilateur peut optimiser la division (plus l'ajout du bit le plus significatif avant le décalage pour les nombres signés).
Étant donné que ces opérations sont des opérations sur un seul bit, rien de spécial ne se produit ici, une opération au niveau du bit est effectuée deux fois.
Fait intéressant, x86 a en fait une instruction pour effectuer un décalage à gauche de 64 bits appelé shld
, qui au lieu de remplacer les bits de valeur les moins significatifs par des zéros, il les remplace par les bits les plus significatifs d'un registre différent. De même, c'est le cas pour le décalage à droite avec l'instruction shrd
. Cela ferait facilement du décalage 64 bits une opération à deux instructions.
Cependant, ce n'est qu'un cas de changements constants. Lorsqu'un décalage n'est pas constant, les choses deviennent plus délicates, car l'architecture x86 ne prend en charge le décalage qu'avec 0-31 comme valeur. Tout ce qui est au-delà de cela est selon la documentation officielle non défini, et en pratique, au niveau du bit et le fonctionnement avec 0x1F est effectué sur une valeur. Par conséquent, lorsqu'une valeur de décalage est supérieure à 31, l'un des stockages de valeurs est entièrement effacé (pour le décalage vers la gauche, c'est-à-dire les octets inférieurs, pour le décalage vers la droite, c'est les octets supérieurs). L'autre obtient la valeur qui était dans le registre qui a été effacée, puis l'opération de décalage est effectuée. En conséquence, cela dépend du prédicteur de branche pour faire de bonnes prédictions, et est un peu plus lent car une valeur doit être vérifiée.
__builtin_popcount (inférieur) + __builtin_popcount (supérieur)
Je suis trop paresseux pour terminer la réponse à ce stade. Quelqu'un les utilise-t-il même?
L'addition, la soustraction, la multiplication ou, et, ou xor, le décalage vers la gauche génèrent exactement le même code. Le décalage vers la droite n'utilise qu'un code légèrement différent (décalage arithmétique vs décalage logique), mais structurellement, c'est la même chose. Il est cependant probable que la division génère un code différent, et la division signée sera probablement plus lente que la division non signée.
Des repères? Ils sont pour la plupart dénués de sens, car le pipelining des instructions entraîne généralement une accélération des choses lorsque vous ne répétez pas constamment la même opération. N'hésitez pas à considérer la division comme lente, mais rien d'autre ne l'est vraiment, et lorsque vous sortez des repères, vous remarquerez peut-être qu'en raison du pipelining, effectuer des opérations 64 bits sur un processeur 32 bits n'est pas lent du tout.
Benchmarkez votre propre application, ne faites pas confiance aux micro-benchmarks qui ne font pas ce que fait votre application. Les processeurs modernes sont assez délicats, donc les benchmarks indépendants peuvent et mentiront .
Votre question semble assez bizarre dans son environnement. Vous utilisez time_t qui utilise jusqu'à 32 bits. Vous avez besoin d'informations supplémentaires, ce qui signifie plus de bits. Vous êtes donc obligé d'utiliser quelque chose de plus grand que int32. Peu importe la performance, non? Les choix iront entre utiliser simplement 40 bits ou passer à int64. À moins que des millions d'instances ne doivent en être stockées, ce dernier est un choix judicieux.
Comme d'autres l'ont souligné, la seule façon de connaître la vraie performance est de la mesurer avec le profileur (dans certains échantillons grossiers, une simple horloge fera l'affaire). alors allez-y et mesurez. Il ne doit pas être difficile de remplacer globalement votre utilisation de time_t par un typedef et de le redéfinir en 64 bits et de corriger les quelques instances où le temps réel_t était attendu.
Mon pari serait sur une "différence incommensurable" à moins que vos instances actuelles de time_t n'occupent au moins quelques Mo de mémoire. sur les plateformes actuelles de type Intel, les cœurs passent la plupart du temps à attendre que la mémoire externe entre dans le cache. Un seul échec de cache bloque pendant des centaines de cycles. Ce qui rend le calcul des différences de 1 tick sur les instructions impossible. Vos performances réelles peuvent chuter à cause de choses telles que votre structure actuelle ne correspond qu'à une ligne de cache et la plus grande en a besoin de deux. Et si vous n'avez jamais mesuré vos performances actuelles, vous découvrirez peut-être que vous pouvez obtenir une accélération extrême de certaines fonctions simplement en ajoutant un ordre d'alignement ou d'échange de certains membres dans une structure. Ou emballez (1) la structure au lieu d'utiliser la disposition par défaut ...
L'addition/soustraction devient essentiellement deux cycles chacun, la multiplication et la division dépendent du CPU réel. L'impact général sur les performances sera plutôt faible.
Notez qu'Intel Core 2 prend en charge EM64T.