Wenn das Verständnis , wie primitive Operatoren wie +
, -
, *
und /
sind in C implementiert, fand ich die folgenden Ausschnitt aus einer interessanten Antwort .
// replaces the + operator
int add(int x, int y) {
while(x) {
int t = (x & y) <<1;
y ^= x;
x = t;
}
return y;
}
Es scheint, dass diese Funktion zeigt, wie +
tatsächlich im Hintergrund funktioniert . Es ist jedoch zu verwirrend für mich, um es zu verstehen. Ich habe geglaubt, dass solche Operationen mit Assembly-Direktiven ausgeführt werden, die vom Compiler für eine lange Zeit generiert wurden!
Wird der +
Operator als der Code implementiert, der in den meisten Implementierungen angegeben ist? Nutzt dies das Komplement von zwei oder andere implementierungsabhängige Funktionen?
c
operators
bitwise-operators
Nalzok
quelle
quelle
add
Maschinenanweisungen, die vermutlich alle CPUs haben und als Hardware-Addierer implementiert sind, die in wenigen Takten funktionieren.+
Bediener nutzt sehr wahrscheinlich implementierungsdefinierte Funktionen. Diese werden als "Maschinensprache" und "CPU" bezeichnet. Was ist deine Frage? Wenn Sie wissen möchten, wie Ausdrücke in Maschinencode konvertiert werden, lesen Sie bitte die Compilerkonstruktion.+
s in Assembly-Direktiven wieadd
vom Compiler übersetzt werden?+
Operationen werden in eine Variante (oder Kombination) von Maschinencode-add
Anweisungen kompiliert . Ihr Code ist in jedem realen Szenario verworren und nutzlos, kann jedoch dazu dienen, binäre Operationen zu lehren.Antworten:
Um pedantisch zu sein, gibt die C-Spezifikation nicht an, wie die Addition implementiert wird.
+
Um jedoch realistisch zu sein, wird der Operator für ganzzahlige Typen, die kleiner oder gleich der Wortgröße Ihrer CPU sind, direkt in einen Additionsbefehl für die CPU übersetzt, und größere ganzzahlige Typen werden in mehrere Additionsbefehle mit einigen zusätzlichen Bits übersetzt, um den Überlauf zu behandeln.Die CPU verwendet intern Logikschaltungen, um die Addition zu implementieren, und verwendet keine Schleifen, Bitverschiebungen oder andere Elemente, die der Funktionsweise von C sehr ähnlich sind.
quelle
Wenn Sie zwei Bits hinzufügen, ist das folgende Ergebnis: (Wahrheitstabelle)
Wenn Sie also bitweise xor ausführen, können Sie die Summe ohne Übertrag erhalten. Und wenn Sie bitweise tun und Sie können die Übertragsbits bekommen.
Erweiterung dieser Beobachtung für Multibit-Nummern
a
undb
Einmal
b
ist0
:Der Algorithmus läuft also auf Folgendes hinaus:
Wenn Sie die Rekursion loswerden und sie in eine Schleife konvertieren
In Anbetracht des obigen Algorithmus sollte die Erklärung aus dem Code einfacher sein:
Tragen Sie Bits. Das Übertragsbit ist 1, wenn 1 Bit rechts in beiden Operanden 1 ist.
Addition ohne Übertrag (Übertragsbits ignoriert)
Verwenden Sie x erneut, um das Tragen festzulegen
Wiederholen, solange mehr Übertragsbits vorhanden sind
Eine rekursive Implementierung (leichter zu verstehen) wäre:
Nein. Normalerweise (fast immer) bedeutet das Hinzufügen von Ganzzahlen das Hinzufügen von Maschinenbefehlen. Dies zeigt nur eine alternative Implementierung mit bitweisem xor und und.
quelle
Nein. Dies wird in die native
add
Maschinenanweisung übersetzt, die tatsächlich den Hardware-Addierer verwendetALU
.Wenn Sie sich fragen, wie der Computer hinzufügt, finden Sie hier einen einfachen Addierer.
Alles im Computer wird mit Logikgattern ausgeführt, die hauptsächlich aus Transistoren bestehen. Der Volladdierer enthält Halbaddierer.
Ein grundlegendes Tutorial zu Logikgattern und Addierern finden Sie hier . Das Video ist sehr hilfreich, wenn auch lang.
In diesem Video wird ein einfacher Halbaddierer gezeigt. Wenn Sie eine kurze Beschreibung wünschen, ist dies:
Wie funktioniert nun der Halbaddierer? Nun, es besteht aus drei logischen Gattern, die
and
,xor
und dasnand
. Dasnand
gibt einen positiven Strom, wenn beide Eingänge negativ sind, was bedeutet, dass dies den Fall von 0 und 0 löst. Dasxor
gibt einen positiven Ausgang, einer der Eingänge ist positiv und der andere negativ, was bedeutet, dass es das Problem von löst 1 und 0. Dasand
gibt nur dann einen positiven Ausgang, wenn beide Eingänge positiv sind, so dass das Problem von 1 und 1 gelöst ist. Im Grunde haben wir jetzt unseren Halbaddierer. Wir können aber immer noch nur Bits hinzufügen.Jetzt machen wir unseren Volladdierer. Ein Volladdierer besteht darin, den Halbaddierer immer wieder aufzurufen. Nun hat dies einen Carry. Wenn wir 1 und 1 addieren, erhalten wir einen Übertrag 1. Der Volladdierer nimmt also den Übertrag vom Halbaddierer, speichert ihn und übergibt ihn als weiteres Argument an den Halbaddierer.
Wenn Sie sich nicht sicher sind, wie Sie den Übertrag übergeben können, addieren Sie die Bits zunächst mit dem Halbaddierer und dann mit der Summe und dem Übertrag. Jetzt haben Sie den Übertrag mit den beiden Bits hinzugefügt. Also machst du das immer wieder, bis die Bits, die du hinzufügen musst, vorbei sind und dann bekommst du dein Ergebnis.
Überrascht? So passiert es tatsächlich. Es sieht nach einem langen Prozess aus, aber der Computer erledigt ihn in Bruchteilen einer Nanosekunde oder genauer gesagt in einem halben Taktzyklus. Manchmal wird es sogar in einem einzigen Taktzyklus ausgeführt. Grundsätzlich verfügt der Computer über
ALU
(einen GroßteilCPU
), Speicher, Busse usw.Wenn Sie Computerhardware aus Logikgattern, Speicher und der ALU lernen und einen Computer simulieren möchten, können Sie diesen Kurs sehen, aus dem ich all dies gelernt habe: Bauen Sie einen modernen Computer aus den ersten Prinzipien
Es ist kostenlos, wenn Sie kein E-Zertifikat möchten. Der zweite Teil des Kurses steht im Frühjahr dieses Jahres an
quelle
C verwendet eine abstrakte Maschine, um zu beschreiben, was C-Code tut. Wie es funktioniert, ist also nicht festgelegt. Es gibt C "Compiler", die beispielsweise C tatsächlich in eine Skriptsprache kompilieren.
In den meisten C-Implementierungen werden jedoch
+
zwischen zwei Ganzzahlen, die kleiner als die Ganzzahlgröße der Maschine sind, (nach vielen Schritten) in eine Assembly-Anweisung übersetzt. Die Montageanweisung wird in Maschinencode übersetzt und in Ihre ausführbare Datei eingebettet. Assembly ist eine Sprache, die aus dem Maschinencode "einen Schritt entfernt" ist und leichter zu lesen sein soll als eine Reihe gepackter Binärdateien.Dieser Maschinencode (nach vielen Schritten) wird dann von der Zielhardwareplattform interpretiert, wo er vom Befehlsdecoder auf der CPU interpretiert wird. Dieser Befehlsdecoder nimmt den Befehl auf und übersetzt ihn in Signale, die entlang "Steuerleitungen" gesendet werden sollen. Diese Signale leiten Daten von Registern und Speicher durch die CPU, wo die Werte häufig in einer arithmetischen Logikeinheit addiert werden.
Die arithmetische Logikeinheit kann separate Addierer und Multiplikatoren haben oder sie miteinander mischen.
Die arithmetische Logikeinheit verfügt über eine Reihe von Transistoren, die die Additionsoperation ausführen und dann den Ausgang erzeugen. Diese Ausgabe wird über die vom Befehlsdecoder erzeugten Signale geleitet und in einem Speicher oder Registern gespeichert.
Das Layout dieser Transistoren sowohl in der arithmetischen Logikeinheit als auch im Befehlsdecoder (sowie in Teilen, die ich beschönigt habe) wird in der Anlage in den Chip geätzt. Das Ätzmuster wird häufig durch Kompilieren einer Hardwarebeschreibungssprache erzeugt, die eine Abstraktion dessen, was mit was verbunden ist und wie sie arbeiten, nimmt und Transistoren und Verbindungsleitungen erzeugt.
Die Hardwarebeschreibungssprache kann Verschiebungen und Schleifen enthalten, die keine Ereignisse beschreiben zeitliche (wie nacheinander), sondern räumliche beschreiben. Sie beschreibt die Verbindungen zwischen verschiedenen Teilen der Hardware. Dieser Code sieht möglicherweise sehr vage aus wie der Code, den Sie oben gepostet haben.
Das Obige beschönigt viele Teile und Schichten und enthält Ungenauigkeiten. Dies liegt sowohl an meiner eigenen Inkompetenz (ich habe sowohl Hardware als auch Compiler geschrieben, bin aber kein Experte in beiden) und daran, dass vollständige Details eine oder zwei Karrieren erfordern würden und keinen SO-Beitrag.
Hier ist ein SO-Beitrag über einen 8-Bit-Addierer. Hier ist ein Nicht-SO-Beitrag, in dem Sie einige der Addierer bemerken, die nur
operator+
in der HDL verwendet werden! (Die HDL selbst versteht+
und generiert den Addierercode der unteren Ebene für Sie).quelle
Fast jeder moderne Prozessor, der kompilierten C-Code ausführen kann, unterstützt die Ganzzahladdition. Der Code, den Sie gepostet haben, ist eine clevere Möglichkeit, eine Ganzzahladdition durchzuführen, ohne einen Ganzzahl-Add-Opcode auszuführen, aber es ist nicht so, wie eine Ganzzahladdition normalerweise durchgeführt wird. Tatsächlich verwendet die Funktionsverknüpfung wahrscheinlich eine Form der Ganzzahladdition, um den Stapelzeiger anzupassen.
Der Code, den Sie gepostet haben, basiert auf der Beobachtung, dass Sie ihn beim Hinzufügen von x und y in die gemeinsamen Bits und die für x oder y eindeutigen Bits zerlegen können.
Der Ausdruck
x & y
(bitweises UND) gibt die für x und y gemeinsamen Bits an. Der Ausdruckx ^ y
(bitweises exklusives ODER) gibt die Bits an, die für eines von x oder y eindeutig sind.Die Summe
x + y
kann umgeschrieben werden als die Summe des Zweifachen der gemeinsamen Bits (da sowohl x als auch y diese Bits beitragen) plus der Bits, die für x oder y eindeutig sind.(x & y) << 1
ist doppelt so groß wie die Bits, die sie gemeinsam haben (die Linksverschiebung um 1 multipliziert effektiv mit zwei).x ^ y
sind die Bits, die für eines von x oder y eindeutig sind.Wenn wir also x durch den ersten Wert und y durch den zweiten ersetzen, sollte die Summe unverändert bleiben. Sie können sich den ersten Wert als Übertrag der bitweisen Additionen und den zweiten als niederwertiges Bit der bitweisen Additionen vorstellen.
Dieser Vorgang wird fortgesetzt, bis x Null ist. An diesem Punkt enthält y die Summe.
quelle
Der Code, den Sie gefunden haben, versucht zu erklären, wie sehr primitive Computerhardware eine "add" -Anweisung implementieren kann. Ich sage "könnte", weil ich garantieren kann, dass diese Methode von keiner CPU verwendet wird, und ich werde erklären, warum.
Im normalen Leben verwenden Sie Dezimalzahlen und haben gelernt, wie man sie hinzufügt: Um zwei Zahlen hinzuzufügen, fügen Sie die niedrigsten zwei Ziffern hinzu. Wenn das Ergebnis kleiner als 10 ist, notieren Sie das Ergebnis und fahren mit der nächsten Ziffernposition fort. Wenn das Ergebnis 10 oder mehr ist, schreiben Sie das Ergebnis minus 10 auf, fahren mit der nächsten Ziffer fort und kaufen. Denken Sie daran, 1 weitere hinzuzufügen. Zum Beispiel: 23 + 37, Sie addieren 3 + 7 = 10, Sie schreiben 0 auf und denken daran, 1 weitere für die nächste Position hinzuzufügen. An der 10er Position addieren Sie (2 + 3) + 1 = 6 und schreiben das auf. Ergebnis ist 60.
Mit Binärzahlen können Sie genau dasselbe tun. Der Unterschied besteht darin, dass die einzigen Ziffern 0 und 1 sind, sodass die einzig möglichen Summen 0, 1, 2 sind. Bei einer 32-Bit-Zahl würden Sie eine Ziffernposition nach der anderen behandeln. Und so würde es wirklich primitive Computerhardware tun.
Dieser Code funktioniert anders. Sie wissen, dass die Summe zweier Binärziffern 2 ist, wenn beide Ziffern 1 sind. Wenn also beide Ziffern 1 sind, würden Sie an der nächsten Binärposition 1 weitere hinzufügen und 0 aufschreiben. Das ist, was die Berechnung von t bewirkt: Es findet alle Stellen Dabei sind beide Binärziffern 1 (das ist das &) und werden an die nächste Ziffernposition (<< 1) verschoben. Dann wird addiert: 0 + 0 = 0, 0 + 1 = 1, 1 + 0 = 1, 1 + 1 ist 2, aber wir schreiben 0 auf. Das macht der Ausschluss oder Operator.
Aber alle Einsen, die Sie an der nächsten Stelle behandeln mussten, wurden nicht behandelt. Sie müssen noch hinzugefügt werden. Deshalb macht der Code eine Schleife: In der nächsten Iteration werden alle zusätzlichen Einsen hinzugefügt.
Warum macht das kein Prozessor so? Weil es eine Schleife ist und Prozessoren keine Schleifen mögen und es langsam ist. Es ist langsam, da im schlimmsten Fall 32 Iterationen erforderlich sind: Wenn Sie der Zahl 0xffffffff (32 1-Bit) 1 hinzufügen, löscht die erste Iteration Bit 0 von y und setzt x auf 2. Die zweite Iteration löscht Bit 1 von y und setzt x auf 4. Und so weiter. Es dauert 32 Iterationen, um das Ergebnis zu erhalten. Jede Iteration muss jedoch alle Bits von x und y verarbeiten, was viel Hardware erfordert.
Ein primitiver Prozessor erledigt die Dinge genauso schnell wie die Dezimalarithmetik, von der niedrigsten bis zur höchsten Position. Es sind ebenfalls 32 Schritte erforderlich, aber jeder Schritt verarbeitet nur zwei Bits plus einen Wert von der vorherigen Bitposition, sodass die Implementierung viel einfacher ist. Und selbst in einem primitiven Computer kann man es sich leisten, dies zu tun, ohne Schleifen implementieren zu müssen.
Eine moderne, schnelle und komplexe CPU verwendet einen "bedingten Summenaddierer". Insbesondere wenn die Anzahl der Bits hoch ist, beispielsweise ein 64-Bit-Addierer, spart dies viel Zeit.
Ein 64-Bit-Addierer besteht aus zwei Teilen: Erstens einem 32-Bit-Addierer für das niedrigste 32-Bit. Dieser 32-Bit-Addierer erzeugt eine Summe und einen "Übertrag" (ein Indikator dafür, dass der nächsten Bitposition eine 1 hinzugefügt werden muss). Zweitens zwei 32-Bit-Addierer für die höheren 32-Bit: Einer addiert x + y, der andere addiert x + y + 1. Alle drei Addierer arbeiten parallel. Wenn der erste Addierer seinen Übertrag erzeugt hat, wählt die CPU nur aus, welches der beiden Ergebnisse x + y oder x + y + 1 das richtige ist, und Sie haben das vollständige Ergebnis. Ein 64-Bit-Addierer dauert also nur ein kleines bisschen länger als ein 32-Bit-Addierer, nicht doppelt so lang.
Die 32-Bit-Addiererteile werden wieder als bedingte Summenaddierer implementiert, wobei mehrere 16-Bit-Addierer verwendet werden, und die 16-Bit-Addierer sind bedingte Summenaddierer und so weiter.
quelle
Beantworten wir die eigentliche Frage. Alle Operatoren werden vom Compiler als interne Datenstruktur implementiert, die nach einigen Transformationen schließlich in Code übersetzt wird. Sie können nicht sagen, welcher Code durch eine einzelne Addition generiert wird, da fast kein Compiler der realen Welt Code für einzelne Anweisungen generiert.
Dem Compiler steht es frei, Code zu generieren, solange er sich so verhält, als ob die eigentlichen Operationen gemäß dem Standard ausgeführt würden. Aber was tatsächlich passiert, kann etwas völlig anderes sein.
Ein einfaches Beispiel:
Hier müssen keine zusätzlichen Anweisungen generiert werden. Es ist für den Compiler völlig legal, dies zu übersetzen in:
Oder der Compiler bemerkt, dass Sie die Funktion
foo
einige Male hintereinander aufrufen und dass es sich um eine einfache Arithmetik handelt, die Vektoranweisungen dafür generiert. Oder dass das Ergebnis der Addition später für die Array-Indizierung verwendet wird und dielea
Anweisung verwendet wird.Sie können einfach nicht darüber sprechen, wie ein Operator implementiert wird, da er fast nie alleine verwendet wird.
quelle
Wenn eine Aufschlüsselung des Codes anderen hilft, nehmen Sie das folgende Beispiel
x=2, y=6
:x
ist nicht Null, also füge hinzu zuy
:x & y = 2
weil2 <<1 = 4
weil<< 1
alle Bits nach links verschoben:Zusammengefasst bunkern dieses Ergebnis,
4
int
mitWenden Sie nun das bitweise XOR an
y^=x
:Also
x=2, y=4
. Zum Schluss summieren Sie,t+y
indem Sie zurücksetzenx=t
und zum Anfang derwhile
Schleife zurückkehren:Wenn
t=0
(oder am Anfang der Schleifex=0
), beenden Sie mitquelle
Aus Interesse auf dem Atmega328P-Prozessor mit dem avr-g ++ - Compiler implementiert der folgende Code das Hinzufügen eines durch Subtrahieren von -1:
Generierter Code:
Beachten Sie insbesondere, dass das Hinzufügen von der erfolgt
subi
Addieren Befehl erfolgt (Konstante vom Register subtrahieren), wobei 0xFF in diesem Fall effektiv -1 ist.Interessant ist auch, dass dieser spezielle Prozessor keine
addi
Anweisung hat, was impliziert, dass die Designer dachten, dass ein Subtrahieren des Komplements von den Compiler-Autoren angemessen gehandhabt würde.Es wäre wahrscheinlich fair zu sagen, dass Compiler-Writer versuchen würden, den gewünschten Effekt (Hinzufügen einer Zahl zu einer anderen) auf die effizienteste Art und Weise zu implementieren, die für diese bestimmte Architektur möglich ist. Wenn dies das Subtrahieren des Komplements erfordert, soll es so sein.
quelle