Alors que je m'implique de plus en plus dans la théorie derrière la programmation, je me trouve fasciné et abasourdi par des choses apparemment simples. Je me rends compte que ma compréhension de la majorité des processus fondamentaux est justifiée par une logique circulaire
Q : Comment ça marche?
R : Parce que c'est le cas!
Je déteste cette réalisation! J'adore la connaissance, et en plus j'aime apprendre, ce qui m'amène à ma question (même si elle est large).
Question:
Comment les opérateurs mathématiques fondamentaux sont-ils évalués avec les langages de programmation?
Comment les méthodes actuelles ont-elles été améliorées?
Exemple
var = 5 * 5;
Mon interprétation:
$num1 = 5; $num2 = 5; $num3 = 0;
while ($num2 > 0) {
$num3 = $num3 + $num1;
$num2 = $num2 - 1;
}
echo $num3;
Cela semble être très inefficace. Avec des facteurs plus élevés, cette méthode est très lente tandis que la méthode intégrée standard est instantanée. Comment simuleriez-vous la multiplication sans itérer l'addition?
var = 5 / 5;
Comment cela se fait-il? Je ne peux pas penser à un moyen de le diviser littéralement 5 en 5 parties égales.
var = 5 ^ 5;
Itérations d'itérations d'addition? Mon interprétation:
$base = 5;
$mod = 5;
$num1 = $base;
while ($mod > 1) {
$num2 = 5; $num3 = 0;
while ($num2 > 0) {
$num3 = $num3 + $num1;
$num2 = $num2 - 1;
}
$num1 = $num3;
$mod -=1;
}
echo $num3;
Encore une fois, c'est extrêmement inefficace, mais je ne peux pas penser à une autre façon de le faire. Cette même question s'étend à toutes les fonctions mathématiques liées qui sont gérées automatiquement.
la source
Réponses:
Pour vraiment comprendre comment fonctionne l'arithmétique à l'intérieur d'un ordinateur, vous devez avoir programmé en langage assembleur. De préférence un avec une petite taille de mot et sans instructions de multiplication et de division. Quelque chose comme le 6502.
Sur le 6502, pratiquement toute l'arithmétique se fait dans un registre appelé l'accumulateur. (Un registre est un emplacement de mémoire spécial à l'intérieur du processeur auquel on peut accéder rapidement.) Donc, pour ajouter deux nombres, vous chargez le premier nombre dans l'accumulateur, puis vous y ajoutez le deuxième.
Mais c'est trop simpliste. Le 6502 étant un processeur 8 bits, il ne peut gérer que des nombres compris entre 0 et 255. La plupart du temps, vous souhaiterez pouvoir travailler avec des nombres plus importants. Vous devez les ajouter en morceaux, 8 bits à la fois. Le processeur a un indicateur Carry qui est défini lorsque le résultat de l'ajout de deux nombres déborde de l'accumulateur. Le processeur ajoute que lors de l'ajout, il peut donc être utilisé pour "porter le 1" en supposant que vous commencez par l'octet de poids faible d'un nombre. Un ajout de plusieurs octets sur le 6502 ressemble à ceci:
La soustraction est similaire, sauf que vous définissez le report en premier, utilisez l'instruction SBC au lieu d'ADC et, à la fin, le report est clair s'il y a eu un dépassement.
Mais attendez! Et les nombres négatifs? Eh bien, avec le 6502, ceux-ci sont stockés dans un format appelé complément à deux. En supposant un nombre de 8 bits, -1 est stocké en tant que 255, car lorsque vous ajoutez 255 à quelque chose, vous obtenez un de moins dans l'accumulateur (plus un report). -2 est stocké comme 254 et ainsi de suite, jusqu'à -128, qui est stocké comme 128. Ainsi, pour les entiers signés, la moitié de la plage 0-255 d'un octet est utilisée pour les nombres positifs et la moitié pour les nombres négatifs. (Cette convention vous permet de simplement vérifier le bit élevé d'un nombre pour voir s'il est négatif.)
Pensez-y comme une horloge de 24 heures: ajouter 23 à l'heure se traduira par une heure une heure plus tôt (le lendemain). Donc 23 est l'équivalent modulaire de l'horloge à -1.
Lorsque vous utilisez plus d'un octet, vous devez utiliser des nombres plus grands pour les négatifs. Par exemple, les entiers 16 bits ont une plage de 0 à 65536. Ainsi, 65535 est utilisé pour représenter -1, et ainsi de suite, car l'ajout de 65535 à n'importe quel nombre entraîne une de moins (plus un report).
Sur le 6502, il n'y a que quatre opérations arithmétiques: additionner, soustraire, multiplier par deux (décalage vers la gauche) et diviser par deux (décalage vers la droite). La multiplication et la division peuvent être effectuées en utilisant uniquement ces opérations lors du traitement en binaire. Par exemple, envisagez de multiplier 5 (binaire 101) et 3 (binaire 11). Comme pour la multiplication décimale longue, nous commençons par le chiffre droit du multiplicateur et multiplions 101 par 1, donnant 101. Ensuite, nous déplaçons le multiplicande à gauche et multiplions 1010 par 1, donnant 1010. Ensuite, nous additionnons ces résultats ensemble, donnant 1111, ou 15. Puisque nous ne multiplions jamais que par 1 ou 0, nous ne multiplions pas vraiment; chaque bit du multiplicateur sert simplement d' indicateur qui nous indique s'il faut ajouter le multiplicande (décalé) ou non.
La division est analogue à la division longue manuelle utilisant des diviseurs d'essai, sauf en binaire. Si vous divisez par une constante, il est possible de le faire d'une manière analogue à la soustraction: plutôt que de diviser par X, vous multipliez par un rendu précalculé de 1 / X qui produit le résultat souhaité plus un débordement. Aujourd'hui encore, c'est plus rapide que la division.
Essayez maintenant de faire des calculs à virgule flottante dans l'assemblage ou de convertir des nombres à virgule flottante en formats de sortie agréables dans l'assemblage. Et rappelez-vous, nous sommes en 1979 et la vitesse d'horloge est de 1 MHz, vous devez donc le faire le plus efficacement possible.
Les choses fonctionnent toujours à peu près comme ça aujourd'hui, sauf avec des tailles de mots plus grandes et plus de registres, et bien sûr la plupart des calculs sont effectués par le matériel maintenant. Mais cela se fait toujours de la même manière fondamentale. Si vous additionnez le nombre de décalages et les ajouts requis pour une multiplication, par exemple, cela correspond assez bien au nombre de cycles requis pour une instruction de multiplication matérielle sur les premiers processeurs ayant une telle instruction, comme la 6809, où elle a été exécutée en microcode de la même manière que vous le feriez manuellement. (Si vous avez un budget de transistors plus important, il existe des moyens plus rapides d'effectuer les décalages et les ajouts, de sorte que les processeurs modernes n'effectuent pas ces opérations de manière séquentielle et peuvent effectuer des multiplications en aussi peu qu'un seul cycle.)
la source
Binary Multiplier
pour les détails.En fin de compte, les opérations arithmétiques de base sont effectuées sur le matériel. Plus précisément, dans le CPU (ou en fait, une sous-partie de celui-ci)
En d'autres termes, ce sont des circuits électroniques. Définissez les bits appropriés en entrée et vous obtiendrez les bits appropriés en sortie. Il s'agit d'une combinaison de portes logiques de base.
http://en.wikipedia.org/wiki/Adder_%28electronics%29
http://en.wikipedia.org/wiki/Binary_multiplier
la source
Tout cela est couvert avec une patience approfondie dans L'art de la programmation informatique de Don Knuth.
Des algorithmes efficaces pour additionner, soustraire, multiplier et diviser sont tous décrits en détail.
Vous pouvez lire des choses comme celle-ci qui couvrent bien la division.
http://research.microsoft.com/pubs/151917/divmodnote.pdf
la source
Cela se fait en picosecondes par des circuits électroniques. Google «multiplicateur matériel», etc. pour plus de détails. Les processeurs modernes sont le résultat extrêmement complexe de décennies d'amélioration continue.
BTW, Puisque vous ne multipliez pas par des ajouts répétés, pourquoi imaginez-vous qu'un ordinateur le ferait?
la source
Ce n'est pas censé être une réponse approfondie, mais cela devrait vous donner une idée de la façon dont les choses sont mises en œuvre. Comme vous le savez probablement, les nombres sont représentés en binaire. Par exemple, un ordinateur pourrait représenter le nombre 5 comme 00000101. Une opération très basique qu'un ordinateur peut faire est de décaler vers la gauche, ce qui donnerait 00001010 qui est décimal 10. S'il était décalé deux fois vers la droite, ce serait 00010100 (décimal 20). Chaque fois que nous décalons les chiffres vers la gauche 1 fois, nous doublons le nombre. Disons que j'avais un nombre x et que je voulais le multiplier par 17. Je pouvais décaler x vers la gauche 4 fois puis ajouter x au résultat (16x + x = 17x). Ce serait un moyen efficace de multiplier un nombre par 17. Cela devrait vous donner un aperçu de la façon dont un ordinateur peut multiplier de grands nombres sans simplement utiliser l'addition répétée.
La division peut utiliser des combinaisons d'addition, de soustraction, de décalage à droite, de décalage à gauche, etc. Il existe également de nombreuses astuces pour augmenter le nombre d'exposants.
la source
shl r0, 4
.Quand j'étais enfant, j'ai appris à multiplier et à diviser avec un stylo et un papier, sans perdre de temps avec trop d'ajouts. Plus tard, j'ai appris que les racines carrées sont également calculables de cette façon.
À l'université, j'ai appris à calculer des opérations trigonométriques et logarithmiques avec une douzaine de multiplications, divisions et additions. Ils l'ont appelé la série Taylor.
Avant cela, mon père m'a donné un livre où ces opérations complexes étaient déjà calculées pour des centaines de valeurs et présentées dans des tableaux. Il y avait aussi quelques explications pour estimer l'erreur lorsque vous vouliez le sinus d'une valeur entre deux valeurs calculées.
Les unités entières, les unités à virgule flottante, le GPU et le DSP implémentent simplement toutes ces anciennes techniques sur le silicium.
la source
Je vais essayer de vous donner une idée de la façon dont les circuits numériques sont conçus pour résoudre les problèmes de traitement numérique en utilisant les problèmes que vous posez: comment les processeurs implémentent-ils les ajouts et les multiplications.
Tout d'abord, permet de résoudre la question directe: comment un langage de programmation évalue-t-il efficacement les multiplications et les ajouts. La réponse est simple, ils les compilent en multipliant et en ajoutant des instructions. Par exemple, le code suivant:
est simplement compilé en quelque chose comme:
(notez que l'assemblage ci-dessus est pour un CPU imaginaire qui n'existe pas, par souci de simplicité).
À ce stade, vous vous rendez compte que la réponse ci-dessus déplace simplement le problème et le résout par magie matérielle. La question de suivi est évidemment comment fonctionne cette magie matérielle?
Regardons d'abord le problème le plus simple: l'addition.
D'abord, nous faisons un problème familier, en ajoutant des nombres réguliers de base 10:
La première étape serait d'ajouter 7 et 8. Mais cela donne 15, ce qui est plus qu'un seul chiffre. Nous portons donc le 1:
Maintenant, nous ajoutons 1, 1 et 2 ensemble:
Ainsi, nous obtenons les règles suivantes:
lorsque le résultat de l'addition est supérieur à un chiffre, nous conservons le chiffre le moins significatif et reportons le chiffre le plus significatif vers l'avant
si nous avons un chiffre reporté dans notre colonne, nous l'ajoutons avec les chiffres que nous ajoutons
Il est maintenant temps d'interpréter les règles ci-dessus dans la base 2 - l'algèbre booléenne.
Donc, dans l'algèbre booléenne, en ajoutant 0 et 1 ensemble = 1. En ajoutant 0 et 0 = 0. Et en ajoutant 1 et 1 = 10 qui est plus d'un chiffre, nous reportons donc le 1.
À partir de cela, nous pouvons construire une table de vérité:
À partir de cela, nous pouvons construire deux circuits / équations booléennes - un pour la sortie de somme et un pour la sortie de report. La façon la plus naïve est de simplement lister toutes les entrées. Toute table de vérité, quelle que soit sa taille et sa complexité, peut être reformulée sous cette forme:
Il s'agit essentiellement de la somme des produits. Nous ne regardons que les sorties qui donnent un 1 et ignorons les 0:
Remplaçons le ET OU et NON par des symboles de langage de programmation pour le rendre plus facile à lire:
Fondamentalement, nous avons converti le tableau comme suit:
Cela peut être directement mis en œuvre sous forme de circuit:
Les lecteurs attentifs remarqueront à ce stade que la logique ci-dessus peut en fait être implémentée comme une seule porte - une porte XOR qui a commodément le comportement requis par notre table de vérité:
Mais si votre matériel ne vous fournit pas de porte XOR, les étapes ci-dessus expliquent comment vous allez le définir et l'implémenter en termes de portes ET, OU et NON.
La manière de convertir les portes logiques en matériel réel dépend du matériel dont vous disposez. Ils peuvent être implémentés à l'aide de divers mécanismes physiques tant que le mécanisme fournit une sorte de comportement de commutation. Des portes logiques ont été mises en œuvre avec tout, des jets d'eau ou des bouffées d'air (fluidique) aux transisiteurs (électronique) aux chutes de billes. C'est un gros sujet à part entière, je vais donc le passer sous silence et dire qu'il est possible d'implémenter des portes logiques en tant que périphériques physiques.
Maintenant, nous faisons de même pour le signal de portage. Puisqu'il n'y a qu'une seule condition où le signal de report est vrai, l'équation est simplement:
Alors, c'est simple:
En les combinant ensemble, nous obtenons ce que l'on appelle le demi-additionneur:
Au fait, les équations du circuit ci-dessus ressemblent à ceci:
Le demi-additionneur manque quelque chose. Nous avons implémenté la première règle - si le résultat est supérieur à un chiffre par rapport au report, mais nous n'avons pas implémenté la deuxième règle - s'il y a un report, ajoutez-le avec les chiffres.
Donc, pour implémenter un additionneur complet, un circuit d'addition qui peut ajouter des nombres à plus d'un chiffre, nous devons définir une table de vérité:
L'équation de somme est maintenant:
Nous pouvons suivre le même processus pour factoriser et simplifier l'équation et l'interpréter comme un circuit, etc., comme nous l'avons fait ci-dessus, mais je pense que cette réponse devient trop longue.
Vous devriez maintenant avoir une idée de la façon dont la logique numérique est conçue. Il y a d'autres astuces que je n'ai pas mentionnées, telles que les cartes de Karnaugh (utilisées pour simplifier les tables de vérité) et les compilateurs logiques tels que l'espresso (pour que vous n'ayez pas à factoriser les équations booléennes à la main), mais la base est fondamentalement ce que j'ai décrit ci-dessus:
Décomposez le problème jusqu'à ce que vous puissiez travailler au niveau d'un seul bit (chiffre).
Définissez les sorties souhaitées à l'aide d'une table de vérité.
Convertissez le tableau en une équation booléenne et simplifiez l'équation.
Interprétez l'équation comme des portes logiques.
Convertissez votre circuit logique en circuits matériels réels en implémentant des portes logiques.
C'est ainsi que les problèmes fondamentaux (ou plutôt de bas niveau) sont vraiment résolus - beaucoup, beaucoup de tables de vérité. Le véritable travail de création consiste à décomposer une tâche complexe telle que le décodage MP3 au niveau du bit afin que vous puissiez y travailler avec des tables de vérité.
Désolé, je n'ai pas le temps d'expliquer comment implémenter la multiplication. Vous pouvez essayer de vous y attaquer en déterminant les règles de durée de la multiplication, puis en l'interprétant en binaire, puis essayez de la décomposer en tables de vérité. Ou vous pouvez lire Wikipedia: http://en.wikipedia.org/wiki/Binary_multiplier
la source
les instructions arithmétiques de base sont exécutées avec des instructions d'assemblage très efficaces.
Des instructions plus complexes (ou abstraites) sont soit exécutées en assemblage avec des mécanismes de boucle, soit gérées dans des bibliothèques std.
Pendant que vous étudiez les mathématiques à l'université, vous commencerez à étudier des choses comme le calcul lambda et la notation Big-O. Tous ces éléments, et bien d'autres, sont utilisés par les programmeurs pour évaluer et créer des algorithmes efficaces. Quoi qu'il en soit, les choses de base sont généralement accomplies au niveau bas, comme en assemblage ou en c avec des pointeurs.
Une excellente introduction à ce sujet est "Code" de Charles Petzold.
la source
Procurez-vous un livre comme Fundamentals of Digital Logic ... , qui je pense est encore assez standard pour les étudiants de Freshman / Sophomore EE, et progressez à travers (ed: cela coûte une petite fortune, alors recherchez une édition utilisée ou antérieure de il). Cela vous guidera à travers les additionneurs et les multiplicateurs, et vous donnera suffisamment de contexte pour commencer à comprendre certains des principes derrière ce que fait le matériel.
Votre réponse deviendra, à court terme, "parce qu'elle rassemble énormément d'éléments de logique plus simple pour évoquer ce comportement complexe" au lieu de "parce qu'elle le fait".
Si vous voulez essayer de comprendre tous les principes de la façon dont les programmes sont compilés et exécutés, optez pour cela conjointement, afin que vous puissiez finalement voir comment tout se réunit au milieu.
la source
Il y a beaucoup de bonnes réponses ici. Vous avez également commencé avec la bonne idée: des opérations complexes comme la multiplication sont construites à partir d'opérations plus simples. Comme vous l'avez deviné, il existe des moyens plus rapides de multiplier sans instruction de multiplication que d'utiliser une série d'additions. Toute multiplication peut être implémentée comme la somme de multiplications plus petites ou comme une combinaison de décalages et d'additions. Exemples:
La division peut également être divisée en petites opérations. XOR (^) est une instruction intégrée sur tous les processeurs que j'ai jamais examinés, mais même ainsi, elle peut être implémentée comme une combinaison de AND, OR et NOT.
J'ai le sentiment, cependant, que des réponses spécifiques vous seront moins satisfaisantes qu'une idée générale des types d'instructions fournies par un processeur et de la façon dont ces instructions peuvent être combinées en opérations plus complexes. Il n'y a rien de mieux pour ce genre de curiosité qu'une bonne dose de langage d'assemblage. Voici une introduction très accessible au langage d'assemblage MIPS.
la source
Voici comment un processeur moderne peut implémenter la multiplication de deux entiers 64 bits:
Vous savez faire une multiplication à longue main. Pour multiplier deux nombres à 10 chiffres, vous devez multiplier un nombre à 10 chiffres par chacun des 10 chiffres de l'autre nombre, écrire le résultat à 11 chiffres l'un en dessous de l'autre et décalé, puis ajouter tout le nombre.
Un processeur moderne fait cela avec tous les 64 par 64 bits. Cependant, une multiplication de deux nombres à un seul bit est très simple: 1 x 1 = 1, tous les autres produits sont nuls. Ceci est implémenté avec un et logique. Et contrairement au produit décimal, où le résultat peut être à deux chiffres, un produit binaire de nombres à un seul bit est toujours un bit.
Alors maintenant, vous avez 64 lignes de 64 bits qui doivent être ajoutées. Mais 64 ajouts de nombres 64 bits sont slooooooow. Le processeur utilise donc un additionneur 3/2 ou un additionneur 7/3: si vous ajoutez 3 nombres à un seul bit, le résultat peut être 0, 1, 2 ou 3, ce qui correspond à deux bits. Si vous ajoutez 7 nombres à un seul bit, le résultat est un nombre de 0 à 7, qui peut être représenté par 3 bits. IBM prétend qu'ils peuvent faire un additionneur 7/3 avec seulement 18 circuits primitifs (documentation PowerPC), je parie qu'Intel et ARM peuvent également le faire.
Vous disposez de 4096 bits, regroupez-les en environ 600 groupes de 7 bits dans les mêmes positions binaires et utilisez environ 600 additionneurs 7/3 pour réduire le résultat de 4096 bits à moins de 2000. Ensuite, vous faites la même chose encore et encore, jusqu'à ce que vous vous retrouviez avec des paires de bits qui peuvent être introduites dans un additionneur complet ordinaire.
la source