web-dev-qa-db-fra.com

La multiplication d'entiers se fait-elle vraiment à la même vitesse que l'ajout sur un processeur moderne?

J'entends cette déclaration assez souvent, que la multiplication sur du matériel moderne est tellement optimisée qu'elle est en fait à la même vitesse que l'addition. Est-ce vrai?

Je ne peux jamais obtenir de confirmation faisant autorité. Ma propre recherche n'ajoute que des questions. Les tests de vitesse montrent généralement des données qui me déroutent. Voici un exemple:

#include <stdio.h>
#include <sys/time.h>

unsigned int time1000() {
    timeval val;
    gettimeofday(&val, 0);
    val.tv_sec &= 0xffff;
    return val.tv_sec * 1000 + val.tv_usec / 1000;
}

int main() {
    unsigned int sum = 1, T = time1000();
    for (int i = 1; i < 100000000; i++) {
        sum += i + (i+1); sum++;
    }
    printf("%u %u\n", time1000() - T, sum);
    sum = 1;
    T = time1000();
    for (int i = 1; i < 100000000; i++) {
        sum += i * (i+1); sum++;
    }
    printf("%u %u\n", time1000() - T, sum);
}

Le code ci-dessus peut montrer que la multiplication est plus rapide:

clang++ benchmark.cpp -o benchmark
./benchmark
746 1974919423
708 3830355456

Mais avec d'autres compilateurs, d'autres arguments du compilateur, des boucles internes différemment écrites, les résultats peuvent varier et je ne peux même pas obtenir une approximation.

37
exebook

La multiplication de deux n - les nombres binaires peuvent en fait se faire en profondeur de circuit O (log n), tout comme l'addition.

L'addition dans O (log n) se fait en divisant le nombre en deux et (récursivement) en ajoutant les deux parties en parallèle , où la moitié supérieure est résolue pour les deux cas "0-carry" et "1-carry". Une fois la moitié inférieure ajoutée, le report est examiné et sa valeur est utilisée pour choisir entre le boîtier 0 et 1.

La multiplication en profondeur O (log n) est également effectuée par parallélisation , où chaque somme de 3 nombres est réduite à une somme de seulement 2 nombres en parallèle, et les sommes sont faites d'une manière comme ci-dessus.
Je ne l'expliquerai pas ici, mais vous pouvez trouver du matériel de lecture sur l'addition et la multiplication rapides en recherchant "carry-lookahead" et l'ajout "carry-save" .

Donc, d'un point de vue théorique, comme les circuits sont évidemment intrinsèquement parallèles (contrairement aux logiciels), la seule raison pour laquelle la multiplication serait asymptotiquement plus lente est le facteur constant à l'avant, et non la complexité asymptotique.

23
Mehrdad

Non, ce n'est pas la même vitesse. Qui t'as dit ça?

Les tableaux d'instructions d'Agner Fog montrent que lors de l'utilisation de registres entiers 32 bits, l'ADD/SUB de Haswell prend 0,25–1 cycle (selon la façon dont vos instructions sont bien canalisées) tandis que MUL prend 2–4 cycles. La virgule flottante est l'inverse: ADDSS/SUBSS prennent 1 à 3 cycles tandis que MULSS prend 0,5 à 5 cycles.

23
Cory Nelson

C'est une réponse encore plus complexe que la simple multiplication contre l'addition. En réalité, la réponse ne sera probablement jamais oui. La multiplication, par voie électronique, est un circuit beaucoup plus compliqué. La plupart des raisons pour lesquelles, c'est que la multiplication est l'acte d'une étape de multiplication suivie d'une étape d'addition, rappelez-vous ce que c'était que de multiplier des nombres décimaux avant d'utiliser une calculatrice.

L'autre chose à retenir est que la multiplication prendra plus ou moins de temps selon l'architecture du processeur sur lequel vous l'exécutez. Cela peut ou non être simplement spécifique à l'entreprise. Alors qu'un AMD sera très probablement différent d'un Intel, même un Intel i7 peut être différent d'un core 2 (au sein de la même génération), et certainement différent entre les générations (en particulier le plus en arrière).

Dans toute TECHNICITÉ, si les multiplications étaient la seule chose que vous faisiez (sans bouclage, comptage, etc.), les multiplications seraient de 2 à (comme on le voit sur les architectures PPC) 35 fois plus lentement. Il s'agit plus d'un exercice de compréhension de votre architecture et de votre électronique.

En plus: Il convient de noter qu'un processeur POURRAIT être construit pour lequel TOUTES les opérations, y compris une multiplication, prennent une seule horloge. Ce que ce processeur devrait faire est de se débarrasser de tous les pipelining et de ralentir l'horloge afin que la latence HW de tout circuit OP soit inférieure ou égale à la latence FOURNIE par la synchronisation d'horloge.

Faire cela éliminerait les gains de performances inhérents que nous pouvons obtenir lors de l'ajout de pipelining dans un processeur. Le pipelining est l'idée de prendre une tâche et de la décomposer en sous-tâches plus petites qui peuvent être effectuées beaucoup plus rapidement. En stockant et en transmettant les résultats de chaque sous-tâche entre les sous-tâches, nous pouvons maintenant exécuter une fréquence d'horloge plus rapide qui ne doit permettre que la plus longue latence des sous-tâches, et non à partir de la tâche globale dans son ensemble.

Image du temps à travers une multiplication:

| ------------------------------------------------- Non canalisé

| --Étape 1-- | --Étape 2-- | --Étape 3-- | --Étape 4-- | --Étape 5-- | Pipeliné

Dans le schéma ci-dessus, le circuit non canalisé prend 50 unités de temps. Dans la version en pipeline, nous avons divisé les 50 unités en 5 étapes, chacune prenant 10 unités de temps, avec une étape de stockage entre les deux. Il est EXTRÊMEMENT important de noter que dans l'exemple en pipeline, chacune des étapes peut fonctionner complètement seule et en parallèle. Pour qu'une opération soit terminée, elle doit se déplacer dans les 5 étapes dans l'ordre, mais une autre de la même opération avec des opérandes peut être à l'étape 2 car l'une est à l'étape 1, 3, 4 et 5.

Cela étant dit, cette approche en pipeline nous permet de remplir en permanence l'opérateur à chaque cycle d'horloge et d'obtenir un résultat sur chaque cycle d'horloge SI nous sommes en mesure de commander nos opérations de manière à pouvoir effectuer toutes les opérations avant de passer à une autre opération, et tout ce que nous prenons comme un coup de synchronisation est la quantité originale d'horloges nécessaires pour obtenir la première opération hors du pipeline.

Mystique soulève un autre bon point. Il est également important de considérer l'architecture d'un point de vue plus systémique. Il est vrai que les nouvelles architectures Haswell ont été conçues pour améliorer les performances de multiplication en virgule flottante au sein du processeur. Pour cette raison, en tant que niveau système, il a été conçu pour permettre à plusieurs multiplications de se produire en simultanéité par rapport à un ajout qui ne peut se produire qu'une fois par horloge système.

--- (Tout cela peut se résumer comme suit:

  1. Chaque architecture est différente d'un point de vue matériel de niveau inférieur ainsi que d'un point de vue système
  2. FONCTIONNELLEMENT, une multiplication prendra toujours plus de temps qu'un ajout car elle combine une véritable multiplication avec une véritable étape d'addition.
  3. Comprenez l'architecture sur laquelle vous essayez d'exécuter votre code et trouvez le bon équilibre entre la lisibilité et les meilleures performances de cette architecture.
10
trumpetlicks

Intel depuis Haswell a

  • add performances de débit 4/horloge, latence 1 cycle. (N'importe quelle taille d'opérande)
  • imul performances de 1/horloge, latence de 3 cycles. (N'importe quelle taille d'opérande)

Ryzen est similaire. La famille des bulldozers a un débit entier beaucoup plus faible et une multiplication non entièrement pipelinée, y compris un ralentissement supplémentaire pour la multiplication de la taille de l'opérande 64 bits. Voir https://agner.org/optimize/ et d'autres liens dans https://stackoverflow.com/tags/x86/info

Mais un bon compilateur pourrait vectoriser automatiquement vos boucles. (Le débit de multiplication et la latence de l'entier SIMD sont tous deux pires que l'ajout de l'entier SIMD). Ou tout simplement se propager constamment à travers eux pour simplement imprimer la réponse! Clang connaît vraiment la formule de Gauss de forme fermée pour sum(i=0..n) et peut reconnaître certaines boucles qui font cela.


Vous avez oublié d'activer l'optimisation afin que les deux boucles goulot d'étranglement sur l'ALU + latence de stockage/rechargement de garder sum en mémoire entre chacun des sum += independent stuff et sum++. Voir Pourquoi clang produit-il un asm inefficace avec -O0 (pour cette simple somme à virgule flottante)? pour en savoir plus sur la gravité de l'asm résultant, et pourquoi c'est le cas. clang++ par défaut à -O0 (mode débogage: conserve les variables en mémoire où un débogueur peut les modifier entre toutes les instructions C++).

La latence de transfert de magasin sur un x86 moderne comme la famille Sandybridge (y compris Haswell et Skylake) est d'environ 3 à 5 cycles, selon le moment du rechargement. Donc, avec une latence à 1 cycle ALU add là aussi, vous regardez environ deux étapes de latence à 6 cycles dans le chemin critique de cette boucle. (Beaucoup pour cacher tous les magasins/rechargements et calculs basés sur i et la mise à jour du compteur de boucles).

Voir aussi L'ajout d'une affectation redondante accélère le code lors de la compilation sans optimisation pour une autre référence sans optimisation. Dans celui-ci, la latence de transfert de magasin est en fait réduite en ayant un travail plus indépendant dans la boucle, retardant la tentative de rechargement.


Les processeurs x86 modernes ont un débit multiplié par 1/horloge, donc même avec l'optimisation, vous ne verriez pas de goulot d'étranglement de débit. Ou sur la famille Bulldozer, pas entièrement canalisé avec un débit de 1 par 2 heures.

Il est plus probable que vous goulot d'étranglement sur le travail frontal pour obtenir tout le travail publié à chaque cycle.

Bien que lea permette un copier-ajouter très efficace, et faire i + i + 1 avec une seule instruction. Bien qu'un très bon compilateur verrait que la boucle n'utilise que 2*i et optimiser pour incrémenter de 2, c'est-à-dire une réduction de la force pour effectuer des ajouts répétés de 2 au lieu de devoir se déplacer à l'intérieur de la boucle.

Et bien sûr, avec l'optimisation, l'extra sum++ peut simplement se replier dans le sum += stuffstuff inclut déjà une constante. Ce n'est pas le cas avec la multiplication.

3
Peter Cordes

Une multiplication nécessite une étape finale d'un ajout, au minimum, de la même taille du nombre; cela prendra donc plus de temps qu'un ajout. En décimal:

    123
    112
   ----
   +246  ----
   123      | matrix generation  
  123    ----
  -----
  13776 <---------------- Addition

Il en va de même en binaire, avec une réduction plus élaborée de la matrice.

Cela dit, les raisons pour lesquelles ils peuvent prendre le même temps:

  1. Pour simplifier l'architecture en pipeline, toutes les instructions régulières peuvent être conçues pour prendre le même nombre de cycles (les exceptions sont des mouvements de mémoire par exemple, qui dépendent du temps nécessaire pour parler à la mémoire externe).
  2. Puisque l'additionneur pour la dernière étape du multiplicateur est comme l'additionneur pour une instruction d'ajout ... pourquoi ne pas utiliser le même additionneur en sautant la génération et la réduction de la matrice? S'ils utilisent le même additionneur, ils prendront évidemment le même temps.

Bien sûr, il existe des architectures plus complexes où ce n'est pas le cas et vous pouvez obtenir des valeurs complètement différentes. Vous avez également des architectures qui prennent plusieurs instructions en parallèle lorsqu'elles ne dépendent pas les unes des autres, et puis vous êtes un peu à la merci de votre compilateur ... et du système d'exploitation.

La seule façon d'exécuter ce test rigoureusement serait d'exécuter dans Assembly et sans système d'exploitation - sinon il y a trop de variables.

2
user3684405

Même si c'était le cas, cela nous dit surtout quelle restriction l'horloge impose à notre matériel. Nous ne pouvons pas cadencer plus haut parce que la chaleur (?), Mais le nombre de portes d'instruction ADD qu'un signal pourrait passer pendant une horloge pourrait être très nombreux mais une seule instruction ADD n'en utiliserait qu'une seule. Ainsi, même si cela peut à un moment donné prendre autant de cycles d'horloge, tout le temps de propagation des signaux n'est pas utilisé.

Si nous pouvions cadencer plus haut, nous pourrions battre. rendre ADD plus rapide probablement de plusieurs ordres de grandeur.

2
mathreadler

Cela dépend vraiment de votre machine. Bien sûr, la multiplication d'entiers est assez complexe par rapport à l'addition, mais pas mal de CPU AMD peuvent exécuter une multiplication en un seul cycle. C'est aussi rapide que l'addition.

D'autres processeurs prennent trois ou quatre cycles pour effectuer une multiplication, ce qui est un peu plus lent que l'addition. Mais ce n'est pas du tout la pénalité de performance que vous avez dû subir il y a dix ans (à l'époque, une multiplication 32 bits pouvait prendre une trentaine de cycles sur certains CPU).

Donc, oui, la multiplication est dans la même classe de vitesse de nos jours, mais non, elle n'est toujours pas aussi rapide que l'addition sur tous les CPU.

1
cmaster

Non, ce n'est pas le cas, et en fait, il est nettement plus lent (ce qui s'est traduit par un succès de 15% pour le programme réel que j'exécutais).

Je l'ai réalisé moi-même en posant cette question il y a quelques jours à peine ici .

0
Mehrdad