Autor Thema: CPU - Design: Stack - Software oder Hardware Implementation  (Gelesen 13045 mal)

chris12

  • Beiträge: 134
    • Profil anzeigen
Gespeichert
Moin Leute,

Seit einiger Zeit arbeite ich an einer neuen CPU. Nun stellt sich mir aber eine Frage zum Design und zwar, ob es günstiger wäre den Stack in die Hardware einzubauen (also mit Stackpointer push und pop), oder ob es auch performant wäre den Stack als Makro zum Assembler dazuzugeben.
Im Instructionset sind die Befehle ld r#, imm (LoaD immediate to register r#), lm r#1, [imm+r#2] (Load from Memory at address r#2 (displacement) + immediate to r#1) und sm (Store to Memory at address ...)

Was haltet ihr von den beiden Varianten? was für die praktikablere.

mfg
OS? Pah! Zuerst die CPU, dann die Plattform und _dann_ das OS!

LittleFox

  • Beiträge: 306
    • Profil anzeigen
    • LF-Net.org
Gespeichert
« Antwort #1 am: 18. August 2011, 22:37 »
einwas vorneweg: ich bau keine cpu und werde auch nicht damit anfangen, eeinnert mich bitte daran :P

ich würde denken das die hardware variante besser ist - da du für deine cpu dann bestimmt auch einen assembler proggen willst, wäre der aufwand für dich nur verschoben, für andere assemblerentwickler aber erhöht.
Außerdem denke ich das die hardware variange schneller sein könnte, da sie nur aus einem befehl besteht und nicht aus mehreren

grüße,
littlefox

ot.: gibt es richtige tastaruren für handys?...

Svenska

  • Beiträge: 1 792
    • Profil anzeigen
Gespeichert
« Antwort #2 am: 19. August 2011, 02:59 »
Hallo,

Erik wird dazu sicherlich noch mehr sagen können als ich.

Wenn du mich fragst, möchtest du einen Hardware-Stack haben, kein Assemblermakro. Multitasking benötigt einen Task-State, den du (wenn du keinen Speicherschutz brauchst) einfach auf den Stack pushen kannst, anschließend veränderst du den Stackpointer und hast einen billigen Kontextwechsel erledigt.

Legst du den Stackpointer fest (wie z.B. der 6502 vom C64 das getan hat), verbaust du dir jede Form von Multitasking, wenn du ihn in Assemblermakros aufbaust, verlierst du dauerhaft ein Allzweck-Register plus jede Menge Rechenzeit - es sei denn, alle Stackzugriffe sind zur Assemblierzeit ausrechenbar, was weder bei iterativen noch bei rekursiven Algorithmen funktioniert.

Der Stackpointer braucht nur 3 Operationen zu unterstützen: push, pop und load immediate; wenn der Compiler/Assembler den Stack mühselig aus 2-3 Instruktionen zusammenbasteln muss, geht dir ein normales Register dauerhaft und für jedes push/pop noch ein weitere Register temporär verloren. Je nachdem, wieviele Register du insgesamt hast, kann das sehr teuer werden.

ot.: gibt es richtige tastaruren für andys?...
Bluetooth...

Gruß,
Svenska

MasterLee

  • Beiträge: 49
    • Profil anzeigen
Gespeichert
« Antwort #3 am: 19. August 2011, 07:25 »
Es kommt doch sicherlich noch ein bisschen auf dem Rest des CPU-Design an. Ein Befehl der ein Speicherstelle liest oder schreibt und den Zeiger automatisch incrementier bzw. decrementiert könnte das völlig ausreichend sein. Bei ARM z.B. sind PUSH und POP nur Synonyme für bestimmte Modi von STM und LDM. So kann man wenn man will mit mehreren Stacks gleichzeitig arbeiten.

ot.: gibt es richtige tastaruren für handys?...
N8 an richtiger Tatatur

LittleFox

  • Beiträge: 306
    • Profil anzeigen
    • LF-Net.org
Gespeichert
« Antwort #4 am: 19. August 2011, 09:18 »
wenn ein befehl nur an einer speicherstelle liest, bzw. schreibt ist das ja ok, aber in welchem register speicherst du den pointer? Wenn die CPU das intern macht geht kein Allzweckregister verloren, außerdem ist 'pop' ein befehl 'load' und 'inc' sind aber schon 2 ;)

<ot>
ah, geht bestimmt auch bei meinem (C6), thx :D
</ot>

DeepDancer

  • Beiträge: 58
    • Profil anzeigen
Gespeichert
« Antwort #5 am: 19. August 2011, 11:32 »
ot.: gibt es richtige tastaruren für handys?...

Es gibt sogar Handys mit richtiger Tastatur gleich dran oder drin... meine schicken BlackBerrys zum Beispiel ...  :-D

erik.vikinger

  • Beiträge: 1 277
    • Profil anzeigen
Gespeichert
« Antwort #6 am: 19. August 2011, 12:28 »
Hallo,


Was haltet ihr von den beiden Varianten? was für die praktikablere.
Die Antwort auf diese Frage hat mehr Auswirkungen als Du wohl im Moment siehst.


Vorweg: in meiner CPU hab ich mich dazu entschieden keinen expliziten Stack in Hardware zu unterstützen sondern ich mache PUSH und POP mit den ganz normalen Speicherzugriffsbefehlen. Ein 'PUSH R12' ist bei mir 'STAW SegE:[SP+4],R12' (im 32Bit-Modus, im 64Bit-Mode wäre die 4 ne 8 ) und ein 'POP R7' wird ein 'LDBW R7,SegE:[SP-4]' (hier das selbe wegen 32Bit/64Bit-Modi). Das STAW bedeutet STore and update Address-Register (SP in dem Fall, was nur ein Makro für R62 ist) After Access and Write it back. Das LDBW bedeutet LoaD and update Address-Register Befor Access and Write it back. Daraus erkennt man auch das bei mir der Stack nach oben wächst (auch diese Designentscheidung hat einige Auswirkungen). Meine normalen Speicherzugriffsbefehle haben also immer die Fähigkeit zur eigentlichen Adresse (die normalerweise aus einem Register kommt) noch ein Offset (das eine Konstante ist oder auch aus einem Register kommt und dann noch geshiftet werden kann) dazu zu Rechnen (beim Rechnen unterscheide ich noch explizit zwischen Addieren und Subtrahieren um damit Überläufe sicher verbieten zu können, es gibt bei mir bei den Adressen keinen Warp-Around) und dann diesen Wert auch optional wieder im Adressregister abzulegen, mit dieser Flexibilität hat man einige Vorteile und eben auch gleich noch die Stack-Operationen mit erledigt. Das einzigste was so nicht geht ist das Ablegen von Konstanten auf dem Stack (z.B. PUSH DWORD 4711), dafür müsste ich diese Konstante in ein Register laden und dieses dann auf den Stack speichern, aber das wird IMHO auch nicht so oft benötigt.


Das man für den Stack-Pointer ein Register benötigt ist in beiden Fällen gegeben, ebenso das man dieses Register mit den normalen Befehlen ansprechen können muss (ein 'SUB/ADD SP,16' ist wichtig um mal schnell 16 Bytes Stack-Frame allozieren/freigeben zu können). Größere Auswirkungen hat die Entscheidung für oder gegen richten HW-Stack eher bei anderen Sachen. CPUs ohne echten HW-Stack haben üblicherweise keinen CALL-Befehl sondern einen Linked-Branch was bedeutet das dort die Rücksprungadresse nicht auf dem Stack sondern in einem extra Register landet (dem Link-Register) und die aufgerufene Funktion dieses selber sichern/wiederherstellen muss (im Rahmen des Funktions-Prolog/Epilog) falls sie ihrerseits wieder andere Funktionen aufruft (was auf Leaf-Funktionen eben nicht zu trifft und bei diesen 2 Speicherzugriffe erspart). Ein 'RET' wird dann als 'MOV IP,LR' (also Kopieren vom Link-Register in den Instruction-Pointer) umgesetzt was wieder OpCodes spart.

Ein weiterer wesentlicher Punkt sind Interrupts und die Art wie von der CPU die unterbrochene Umgebung gesichert wird. CPUs die keinen HW-Stack haben benutzen dazu üblicherweise das Konzept der Schattenregister indem die wesentlichen Register (also mindestens der aktuelle Instruction-Pointer und der aktuelle Stack-Pointer) in der CPU mehrfach vorhanden sind und je nach aktuellem Modus ein bestimmtes Set davon tatsächlich benutzt wird. Dies hat auch Auswirkungen darauf wie verschiedene Exceptions/Interrupts verschachtelbar sind. ARM hat für die Register R14 und R15 und Flags gleich für jeden einzelnen Modus ein extra Set, das kostet zwar einiges an Logik bietet aber auch relativ schnelle Modus-Wechsel. Auf meiner CPU gibt es für die Register 60..63 nur 2 Sets, einmal für den User-Mode und einmal für den System-Mode, so das der System-Mode grundsätzlich nicht unterbrechbar ist und eine Exception im Kernel (egal ob Syscall-Handler, IRQ-Handler oder Exception-Handler) grundsätzlich in einem CPU-Shutdown endet, das hat zwar auch ein paar unangenehme Auswirkungen aber bei einem Mikro-Kernel-OS kann man damit ganz gut leben.

Eine weitere wichtige Designentscheidung ist in welche Richtung der Stack wachsen soll. Früher hat man sich dazu entschieden den Stack von oben nach unten wachsen zu lassen weil das dem damaligen Modell eines Prozess (kein Multi-Threading und nur einen zusammenhängenden Speicherbereich) am besten entsprach. Aus heutiger Sicht, wo Multi-Threading und z.B. Buffer-Overflows relevant sind, sieht die Sache anders aus. Mit einer CPU die das nicht per HW macht ist man da grundsätzlich flexibel, auf ARM/PowerPC/SPARC/... (auch auf meiner CPU) ist es eine rein willkürliche Festlegung da die CPUs aufgrund der flexiblen normalen Speicherzugriffsbefehle eigentlich beides unterstützen können. Ich habe mich für nach oben wachsende Stacks entschieden, das ist schon wegen meinen Segmenten die bessere Wahl, außerdem bietet man so weniger Angriffsfläche für Buffer-Overflows  u.ä. weil die eigene Rücksprungadresse auf dem Stack dann vor lokalen Arrays usw. liegt und damit eben nicht erreichbar ist (höchstens mit einem negativen Index und das ist nicht so einfach und auf meiner CPU wegen der expliziten Unterscheidung zwischen + und - beim Offset gar nicht möglich).


Wie eine CPU mit HW-Stack arbeitet kennt hier ja jeder von x86 zur genüge, um auch mal zu sehen wie andere CPUs das ohne expliziten HW-Stack machen kann ich nur empfehlen sich mal die Dokumentationen von ARM/MicroBlaze/PowerPC/SPARC genau durchzulesen (am besten von allen genannten CPUs ;)).


Grüße
Erik
Reality is that which, when you stop believing in it, doesn't go away.

chris12

  • Beiträge: 134
    • Profil anzeigen
Gespeichert
« Antwort #7 am: 19. August 2011, 13:59 »
Danke erik, dass du so detailliert auf meine Frage eingegangen bist (und nicht auf Handys angesprochen hast btw ;) ).
Die Sache mit den Interrupts stand bei mir auch noch zur Diskussion, aber da hatte ich mir noch nicht so Gedanken gemacht. Das Problem mit den Schattenregistern ist in der Tat eine erheblich für mich, da ich eigentlich auf viele Register verzichten wollte. (Gut 16 Allzweckregister ist etwas mehr, aber gut). Ich werde mich wahrscheinlich für eine Mischung aus beidem entscheiden, d.h. minimale Implementierung in Hardware und den Rest als mitgelieferte Software.
Wenn man jetzt LMIR r#1, [r#2] (Load from Memory and Increment Register; r#1 <= [r#2]; r#2 <= r#2 + 1) hat bzw LMDR sollte es eigentlich schon fast reichen.

Das Problem ist allerdings, dass ich nach dem RISC Prinzip arbeiten will und alle Befehle so gut wie gleich lang halte (Befehle mit Konstanten Zahlen als Parameter mal ausgeschlossen) und auch die Taktzyklen gleich lang halten will.
Bei einer früheren CPU brauchte jeder Befehl 5 Stufen in der Ausführung (Fetch (Befehl Laden), Step (bei multibyte Befehlen zur nächsten Adresse), Load (bei multibyte Befehlen das nächste Byte laden), Execute (dekodierten Befehl an die Komponenten schicken) und Jump (zur nächsten Adresse springen)), diese konnten aber durch geschickte Kombination der einzelnen Stufen (Fetch und Step, Load, Execute und Jump) auf drei Taktzyklen verkürzt werden. Dieses würde ich gerne beibehalten, da es die CPU sehr beschleunigt.
OS? Pah! Zuerst die CPU, dann die Plattform und _dann_ das OS!

erik.vikinger

  • Beiträge: 1 277
    • Profil anzeigen
Gespeichert
« Antwort #8 am: 19. August 2011, 15:13 »
Hallo,


Danke erik, dass du so detailliert auf meine Frage eingegangen bist
Kein Problem und für Handys interessiere ich mich nicht (meines hat nicht mal ne Kamera oder ähnlichen Spielkram).

Das Problem mit den Schattenregistern ist in der Tat eine erheblich für mich, da ich eigentlich auf viele Register verzichten wollte.
Register kosten nicht sehr viel, an der Stelle solltest Du nicht zu sparsam sein, Du musst dich ja nicht gleich am Itanium mit 2 mal 128 Register orientieren. Das einzigste was Register wirklich kosten ist Platz in den Befehlen. Wenn Du alle Befehle mit 32Bit codieren möchtest dann sind mehr als 16 Register schon recht viel. Aber gerade da helfen dann die Schattenregister weil Du in der CPU mehr Register zur Verfügung hast als die SW momentan ansprechen kann, zum Zugriff auf die Schattenregister als ganzes reicht ein einfacher Spezial-MOV der dann für die Schattenregister auch ausreichend OpCode-Bits übrig hat. Hierfür möchte ich Dir noch mal wärmstens die ARM-Dokus ans Herz legen.

Ich werde mich wahrscheinlich für eine Mischung aus beidem entscheiden, d.h. minimale Implementierung in Hardware und den Rest als mitgelieferte Software.
Hm, ich hab zwar keine Ahnung wie Du das meinst aber vielleicht wirst Du Deine CPU dann später mal genauer vorstellen.

Wenn man jetzt LMIR r#1, [r#2] (Load from Memory and Increment Register; r#1 <= [r#2]; r#2 <= r#2 + 1) hat bzw LMDR sollte es eigentlich schon fast reichen.
Du hast da IMHO noch vergessen auf die Reihenfolge der einzelnen Operationen einzugehen. Es spielt eine Rolle ob Du erst das Offset drauf rechnest und dann den Zugriff mit dieser Adresse ausführst oder ob Du die Basis-Adresse sofort für den Zugriff benutzt und danach erst das Offset drauf rechnest, also pre-inkrement/dekrement vs. post-inkrement/dekrement (letzteres ergibt natürlich nur Sinn wenn das Ergebnis auch abgespeichert wird wogegen bei ersterem das durchaus auch entfallen kann). Schau Dir das auch noch mal bei x86 beim PUSH und POP genau an. Je nach dem wie das implementiert ist zeigt der Stack-Pointer immer auf das oberste Element (wie bei x86) oder auf das nächste freie Byte (wie auf meiner CPU).

Ein weiterer Punkt über den Du nachdenken solltest ist welche Daten-Größen Du alles unterstützen möchtest, also z.B. nur den nativen Type (32Bit oder 64 Bit) oder nur den nativen + Bytes oder alle vom Byte bis zum nativen Type. x86 hat da wimre ein paar Einschränkenden weil die OpCodes nur ein einziges Bit für die Operandengröße haben und im 32Bit-PM-Mode dann eben in machen Fällen kein 16Bit Wert mehr geht.


Über den internen Aufbau der CPU würde ich mir in der Phase des Designs noch keine so detaillierten Gedanken machen, Du wirst dafür so oder so eine brauchbare HW-Realisierung finden (falls Du Dir nicht allzu unsinnige Befehle ausdenkst).


Grüße
Erik
Reality is that which, when you stop believing in it, doesn't go away.

FlashBurn

  • Beiträge: 844
    • Profil anzeigen
Gespeichert
« Antwort #9 am: 04. September 2011, 22:47 »
Auch wenn es mehr als leicht Offtopic ist, gibt es eigentlich Überlegungen/Papers/Studien wieviele Register optimal sind, wenn man Aufwand und Nutzen vergleicht?

Sicher man kann bei verdammt vielen Registern dann jeden Scheiß speichern, aber der Performance-Vorteil dürfte halt irgendwann den Aufwand nicht mehr Wert sein und mich würde mal interessieren ab wann das so im Schnitt der Fall ist.

Svenska

  • Beiträge: 1 792
    • Profil anzeigen
Gespeichert
« Antwort #10 am: 05. September 2011, 01:08 »
Je mehr Register du hast, desto weniger (teure) Speicherzugriffe musst du machen; der Cache fängt das nur teilweise ab. Andererseits wird mit steigender Anzahl der Register ein Kontextswitch teurer (Schattenregister fangen das auch nur teilweise ab) und du hast mehr Schaltungsaufwand.

Papers kenne ich nicht, würde das Optimum aber ungefähr da vermuten, wo aktuelle Architekturen leben (MIPS: 32, ARM: 16). Plus die üblichen Tricks und Kniffe.

Gruß,
Svenska

FlashBurn

  • Beiträge: 844
    • Profil anzeigen
Gespeichert
« Antwort #11 am: 05. September 2011, 08:43 »
Zitat von: svenska
Je mehr Register du hast, desto weniger (teure) Speicherzugriffe musst du machen
Das ist schon klar, aber speichere z.B. mal nen Array in den Registern, das ist wahrscheinlich mehr Aufwand (in Form von Code) und freier Zugriff über ein Index ist dann auch schwierig (höchstens über self-modifying-code).
Ich denke mir halt, das die Anzahl der Variablen, wo es sich wirklich lohnt sie in Register zu packen, gar nicht so hoch ist.

Ein anderes Problem ist, wenn du eh ständig nur auf wenigen Registern rechnen kannst (weil du z.B. auf das Ergebnis "warten" musst) wird das mit den vielen Registern auch nicht besser.

erik.vikinger

  • Beiträge: 1 277
    • Profil anzeigen
Gespeichert
« Antwort #12 am: 05. September 2011, 12:24 »
Hallo,


gibt es eigentlich Überlegungen/Papers/Studien wieviele Register optimal sind, wenn man Aufwand und Nutzen vergleicht?
Ja, ich hab sowas mal gelesen, wimre war das von Renesas. Ich muss das aber erst suchen also einen Link o.ä gibt es wohl eher Heute (oder Morgen) Abend.


Wimre sind die zu dem Ergebnis gekommen das je nach Programm zwischen 16 und 32 Register optimal sind, ausgehend von einer 32Bit-CPU und Benchmarks die auf den Embedded-Bereich abzielen (es ging in dem Paper auch um kleine µC). Gerade bei größeren Programmen, z.B. aus dem wissenschaftlichen/mathematischen Bereich, dürften aber einige der Beobachtungen in dem Paper nicht mehr ganz zutreffen. Gerade gut massiv parallelisierbare Algorithmen können noch mal enorm von mehr Registern profitieren (Stichwort Loop-Unrolling und anschließendes Code-Interleaving). Es wird schon seine Gründe haben warum der Itanium mit seinen extrem vielen Registern gerade im absoluten High-End-Computing eine nahezu unangefochtene Performance pro Takt hat (die absolute Performance ist nicht ganz so unangefochten da der Itanium nur auf den halben Takt in Relation zu anderen aktuellen CPUs kommt), nebst dessen das massives EPIC mit wenigen Registern keinen Sinn ergibt.

Der wesentliche Grund warum mehr als 32 Register in klassischen 32Bit-CPUs kaum Sinn ergeben liegt an den benötigten Bits im OpCode. Für 32 Register benötigt man 5 Bits und für echte 3 Operanden-Befehle sind das dann schon 15 Bits so das ein 32Bit-RISC-Befehl schon mal zur Hälfte voll ist. Wenn man da z.B. auf 64 Register und 6Bit pro Operand hoch geht dann belegen 3 Operanden bereits 18 Bits so das nur noch 14 Bits für den eigentlichen OpCode übrig bleiben. Auch eine 16Bit-OpCode-Variabnte mit nur 2 Operanden ist dann schon sehr viel schwieriger da dort nur noch 4 OpCode-Bits übrig blieben. Für viele interessante Befehle wie z.B. MAC oder etliche Adressierungsarten oder auch die Schifter-Operanden von ARM benötigt man aber oft 4 Operanden so das bei 64 Registern in den 32Bit-Befehlen nur noch 8 Bit für den eigentlichen OpCode übrig bleiben würden. Vor allem machen Operanden die mehr als 5 Bit belegen auf einer 32Bit-CPU dann Probleme wenn man mal einen Schift-Faktor o.ä. unterbringen möchte da dann Bits ungenutzt bleiben würden.

IBM hat mal eine RISC-CPU konstruiert die 40Bits pro Befehl benutzt hat, dort sind immer 819 Befehle pro 4kB-Page verfügbar und das letzte Byte pro Page ist ungenutzt. Die krumme Befehlsgröße macht aber bei Sprüngen einige Probleme, da man das entweder mit echter Byte-Adressierung (wie bei x86) lösen kann was aber in jeder Sprung-Adresse 2,32 Bits verschwendet (da ja nur an jeder 5. Adresse tatsächlich ein Befehl liegen kann) oder man benötigt irgendwelche ekligen Trick in der HW mit denen z.B. mit 5 multipliziert werden muss um von einer Befehls-Nummer auf die Adresse zu kommen. Beides ist recht unschön, ich weiß jetzt auch gar nicht mehr wie IBM das gelöst hat. Der Itanium hat übrigens 2 * 128 Register (CPU und FPU getrennt) und 41Bit pro Befehl, er umgeht das Problem mit den Sprungziehen aber auch auf geschickte Weise indem er immer 3 Befehle in einem 128Bit-Paket zusammenfasst und diese 3 Befehle immer explizit parallel ausgeführt werden so das bei den Sprüngen immer nur ganze Pakete adressierbar sind. Die Itanium-Pakete haben aber dafür das Problem das die enthaltenen Befehle immer unabhängig voneinander sein müssen und da das nicht immer klappt muss gar nicht mal so selten mit NOPs aufgefüllt werden (was den Code teilweise erheblich aufbläht was Intel mit riesigen Caches versucht auszugleichen), ein Problem mit dem eigentlich alle EPIC-CPUs mehr oder minder zu kämpfen haben.

Das entscheidende Kriterium wie viele Register optimal sind hängt also auch zu einem erheblichen Teil von der OpCode-Struktur ab. Mehr Register machen die Befehle größer (und oft auch komplexer bzw. übliche Code-Kompressionstricks wie Thumb usw. schwieriger) und kosten an der Stelle Performance oder man versucht die Anzahl der möglichen Operaden strickt auf 3 zu begrenzen und muss dann in etlichen Situationen mehr Befehle benutzen (und für Zwischenergebnisse zwischen diesen Befehlen weitere Register belegen und Read-after-Write-Abhängigkeiten abwarten usw.) was auch wieder Performance kostet. Der Itanium geht meises Wissens nach den zweiten Weg und beschränkt sich strickt auf maximal 3 Operanden pro Befehl (was z.B. keinen nicht-destrucktiven MAC-Befehl erlaubt und ihm auch keine tollen Adressierungsarten wie bei x86 ermöglicht).

Man muss aber auch genau prüfen wie viel ein gegebener Algorithmus überhaupt theoretisch von mehr Registern profitieren kann und da ist es eben auch nur selten das mehr als 16 oder 32 Register noch einen so erheblich Performancevorteil bringen das man dafür bereit ist die ganzen Probleme und Schwierigkeiten auf sich zu nehmen. Auf einer klassischen RISC-CPU wird man kaum von der 32Bit-Standard-Befehlsgröße abweichen.

In meiner CPU hab ich 64 Register, was schon wegen dem 64Bit-Modus Sinn macht da ja auch Schift-Faktoren u.ä. dann 6 Bit benötigen. Das Problem mit der OpCode-Größe bin ich mit einem ähnlichen aber flexibleren Ansatz wie der Itanium angegangen so das ich Befehle mit 20 Bit (nur max. 2 Operanden und oft auch nur die ersten 32 Register), 39 Bit (meistens 3 Operanden und etliches auch mit 4 Operanden) und 53 Bit (bis zu 4 bzw. 5 Operanden) in einem 128 Bit-Paket nahezu beliebig kombinieren kann. Das erkaufe ich mir mit einem recht komplexen Decoder (also viel HW-Aufwand), einen ebenfalls sehr komplexen Assembler (also viel SW-Aufwand in der Tool-Chain) und da ich jeden Befehl individuell anspringen können will (EPIC ist mMn keine so tolle Idee für eine General-Purpose-CPU) auch mit etwas Verschwendung in den Sprungzielen (ich gehe bei der Befehlsadressierung von 8 Befehlen pro Paket aus aber in Wirklichkeit sind ja nur mindestens 2 und maximal 6 Befehle drin, so das immer mindestens 0,75 Bit und maximal 2 Bit pro Sprungziel verschwendet werden).


Das mit den Arrays in Registern ist auch kein so großes Problem, das lohnt sich ja eh nur wenn das Array ziemlich klein ist (damit es überhaupt komplett rein passt) und da kann der Compiler dann auch noch Loop-Unrolling benutzen so das er gar keine Index-Adressierung benötigt. Der Itanium kann für solche Tricks sein Registerfile teilweise rotieren so das man hier nicht zwangsläufig auf Loop-Unrolling angewiesen ist, ob die Compiler das auch tatsächlich benutzen weiß ich aber nicht (ich vermute mal eher nicht da beim Itanium Loop-Unrolling ein sehr wichtiges Werkzeug zur Performancesteigerung ist und wenn man das eh nutzt macht das Rotieren des Register-Files kaum noch Sinn).


Grüße
Erik
Reality is that which, when you stop believing in it, doesn't go away.

erik.vikinger

  • Beiträge: 1 277
    • Profil anzeigen
Gespeichert
« Antwort #13 am: 06. September 2011, 22:09 »
Hallo,


Ich muss das aber erst suchen also einen Link o.ä gibt es wohl eher Heute (oder Morgen) Abend.
Ich hab es weder heute noch gestern Abend gefunden. Ich war mir eigentlich ziemlich sicher das ich das im Zusammenhang mit Renesas gelesen hab aber auf deren Home-Page hab ich nichts derartiges gefunden. Sorry, aber ich muss das leider schuldig bleiben.


Wimre sind die zu dem Ergebnis gekommen das je nach Programm zwischen 16 und 32 Register optimal sind
Damit wollte ich nicht ausdrücken das mehr Register gar keine zusätzliche Performance mehr bringen können, sondern das der Zuwachs teilweise so klein wird das sich mehr Register einfach nicht mehr lohnen (die kosten ja schließlich Transistoren). In dem Paper waren wimre etliche Diagramme wo die Performance in Relation zur Anzahl der Register aufgetragen war, in allen Diagrammen hat die Performance mit mehr Register immer zugenommen nur bei vielen Programme ist ab einer gewissen Anzahl an Registern die weitere Steigerung sehr gering ausgefallen. Der Knick war meistens bei 16 bis 32 Registern, es waren aber auch Programme dabei wo der Knick erst bei 64 oder noch mehr Registern war. Die einzelnen Programme waren wimre allesamt verschiedene "typische" Embedded-Aufgaben. Ob da auch Floating-Point-Dinge mit dabei waren kann ich jetzt nicht mehr mit Bestimmtheit sagen aber ich glaube schon.


Ein weiterer Aspeckt für die Anzahl der Register ist ob man auch Floating-Point unterstützen möchte und ob man dafür ein eigenes Register-File haben will oder ob das mit den normalen Registern mit erledigt werden soll. Viele CPUs (von denen die überhaupt Floating-Point unterstützen) haben eine dedizierte FPU welche auch eigene Register hat, damit kann man wieder das eine oder andere Bit in den OpCodes sparen da ja kaum Befehle mit einem beliebigen Gemisch aus beiden Register-Files arbeiten müssen, dafür benötigt man z.B. die Lade/Speicher-Befehle doppelt. Es gibt aber auch einige CPUs die die Floating-Point-Daten direkt in den normalen GP-Register mit abarbeiten, wenn dieses große Register-File größer ist als ein einzelnes spezifisches kann man in vielen Situationen die Register flexibler auf die verschiedenen Anwendungserfordernisse (Daten oder Adresse oder Schleifenzähler oder ....) besser verteilen so das man nicht in die Verlegenheit kommt das in einem der kleinen Register-Files die Register ausgehen obwohl nebenan noch was frei ist. In meiner CPU möchte ich auch für alles (inklusive Floating-Point, falls ich das irgendwann einmal unterstützen sollte) die 64 universellen GP-Register benutzen. Eine weitere Variante des Aufteilens sind Adressregister. Wenn man z.B. anstelle von 16 GP-Registern lieber 8 Datenregister und 8 Adressregister hat können viele Operanden in vielen Befehlen mit 3 anstatt mit 4 Bit codiert werden weil sich ja meistens aus dem Befehl ergibt ob sich der Operand auf Daten oder Adressen bezieht, aber auch diese Aufteilung hat den Nachteil das man eben aus verschiedenen Register-Files nehmen muss und eventuell eines erschöpft ist während im anderen noch verfügbar wäre. Dieses Aufteilen hat aber auch einen interessanten Vorteil: die Register in den verschiedenen Register-Files können unterschiedliche Größen haben. Ein klassisches Beispiel sind da die FPU-Register die oft größer sind als die normalen GP-Register, was ja für extra große Floating-Point-Werte (z.B. mit 80 Bit) und erst recht für SIMD-Register auch echt Sinn ergibt. Ansonsten wäre z.B. die Floating-Point-Verarbeitung auf einer 64Bit-CPU auf 64Bit-Doubles beschränkt (was im Wissenschaftlichen Umfeld oft als zu wenig angesehen wird, gerade in diesen Kreisen wird es als echter Vorteil erachtet das die FP-Register des Itanium sogar mehr als 80 Bit haben und das der Itanium auch die Fähigkeit hat diese Werte verlustfrei im Speicher (mit einem speziellen 128Bit-FP-Format) abzulegen). Es gibt aber auch etliche 16Bit-µC die normale 16Bit-Register für die Daten und 24Bit-Adressregister haben, damit kommt man auch über die 64kB-Grenze hinweg und spart sich trotzdem sowas wie diese leidigen x86-RM-Segmente und Offsets. Eine weitere sehr interessante Variante der Register-Aufteilung sind kleine virtuelle Fenster über einem deutlich größeren physikalischen Register-File, z.B. in der Art wie SPARC das macht, damit kann man durchaus kompakte OpCodes generieren (weil ja nur relativ wenige Register gleichzeitig zugänglich sind) und trotzdem hohe Performance erreichen (weil ja physisch doch recht viele Register vorhanden sind).


Welche Anzahl an Registern wirklich optimal ist kann man definitiv nicht pauschal sagen. Es kommt auf recht viele Aspekte an: die OpCode-Struktur (feste Befehlsgröße mit z.B. immer 32 Bit oder flexible Anzahl an Bytes, was typischerweise RISC vs. CISC entspricht), ob man stur nur maximal 3 Operanden-Befehle hat, verschiedene Datengrößen (z.B. FP mit mehr als 32/64 Bit), ob es komplexe Adressierungsarten gibt (wo ich jetzt sowas die wie ARM-Schifter-Operanden mal mit einbeziehe), wie das Register-Set allgemein aufgebaut ist (alles in einem oder hübsch Aufgeteilt) aber auch ob die CPU außer Speicherzugriffe alles nur in Registern machen kann oder ob es möglich ist viele Operationen direkt im Speicher auszuführen (letzteres setzt zwar zwingend leistungsfähige Adressierungsarten voraus dafür benötigt man die Register kaum für andere Dinge als Adressen, Offsets, ein paar Konstanten und Schleifenzählern u.ä.m.).


Grüße
Erik


PS.: mich würde mal interessieren ob das was ich, zu Themen wie diesem, so schreibe überhaupt irgendjemanden hier ernsthaft interessiert oder ob ich es besser lassen sollte die Forumsdatenbank mit solchen Dingen zuzuspamen.
Reality is that which, when you stop believing in it, doesn't go away.

FlashBurn

  • Beiträge: 844
    • Profil anzeigen
Gespeichert
« Antwort #14 am: 06. September 2011, 22:30 »
Mich interessiert es ;)

Und wenn du einmal dabei bist, könntest du ja zu noch einem ähnlichen Thema deine Meinung/Wissen äußern.

RISC vs. CISC

Also ich würde mal sagen, das RISC alleine schon wegen einer geringeren Komplexität stromsparender gemacht werden kann, da sind wir uns einig. Was aber die Performance betrifft, kann es sein, das da CISC durch aus im Vorteil ist oder würde RISC bei gleicher Performance (und da meine ich auf aktueller Core-i Ebene) und selben Features (64bit, SIMD) deutlich weniger verbrauchen?
Im Endeffekt geht es mir genauer darum, wo die Vorteile eines festen Opcode Formats (sprich feste Befehlslänge) gegenüber eines dynamischen Opcode Formats (sprich dynamische Befehlslänge) liegen (und natürlich auch die Nachteile).

Svenska

  • Beiträge: 1 792
    • Profil anzeigen
Gespeichert
« Antwort #15 am: 06. September 2011, 23:07 »
Hallo,

Ich denke, das sollte in einen neuen Thread verlagert werden.
Off-Topic passt aber. ;-) taljeth? :-P

Feste Opcodes sind wesentlich einfacher zu dekodieren, außerdem fangen alle Befehle z.B. auf einer 4-Byte-Grenze an. Das macht sowohl Speicherzugriffe einfacher als auch das Debuggen. Der Preis dafür ist, dass du keine kleineren Speicherzugriffe machen kannst und du daher jedes Datenwort ausrichten musst. Ein 32-Bit-Zugriff auf 0x1f45 geht beispielsweise auf ARM nicht, da kommt Müll raus; bei x86 teilt die Hardware das transparent auf zwei Buszyklen auf.

Dynamische Opcodelängen führen aber zu nahezu beliebiger Erweiterbarkeit des Befehlssatzes, also lassen sich damit unglaublich komplizierte Befehle nachbilden. Ein Beispiel dafür ist der iAPX 432, der Objektorientierung und Garbage Collection in Hardware unterstützt - oder halt Befehle, die einen vollständigen Kontextwechsel inklusive Speicherschutz mit Sicherung aller relevanten Parameter durchführen können (x86: Sprung durch ein TSS).

Wichtig ist, wie gut der Compiler mit vielen, sehr komplexen Befehlen verschiedener Ausführungszeit klarkommt und wie gut er die Interna der CPU für z.B. Parallelisierbarkeit berücksichtigen kann. Einfache Compiler nehmen eher einfache Befehle und bauen daraus dann die Funktionalität zusammen, was auf CISC ein Performance-Töter ist. In Assembler mag man die Chips auch nicht programmieren, weil es dort viel zu viele Instruktionen gibt, die verschiedenste Randbedingungen haben können. Ein P6+ ist ein RISC-Prozessor, der den CISC-Befehlssatz relativ aufwändig in einfache Befehle (µOps) zerlegt und diese dann out-of-order ausführt.

RISC-Designs können wesentlich höhere Taktrate erreichen, CISC-Designs wesentlich mehr Arbeit pro Instruktion verrichten - sehr gut optimierende Compiler vorausgesetzt.

Ich vermute mal, bei CISC erweitert man lieber den Befehlssatz, um bestimmte Anwendungsfälle besser darzustellen, während man bei RISC dann doch lieber einen Spezialchip daneben baut; das dürfte aber auch am üblichen Einsatz im Embedded-Bereich liegen (eine Settop-Box mit simplem Prozessor und Hardware-MPEG{2,4}-{En,De}coder ist halt eine billigere und stromsparendere Lösung als ein sehr komplexer, optimierter Allzweckprozessor, der das in Software tut). Den ganzen ARM-Chips sieht man das ja auch an, dass dort eigentlich nur viel Peripherie auf ein Die gestopft wurde.

Soweit ich weiß, baut Reneses CISC-Chips, dagegen stehen ARM, MIPS usw. mit RISC-Designs.

So, jetzt möchte ich diesen Text zerpflückt und korrigiert haben. :-P

Gruß,
Svenska

erik.vikinger

  • Beiträge: 1 277
    • Profil anzeigen
Gespeichert
« Antwort #16 am: 10. September 2011, 18:16 »
Hallo,


Das klassisches RISC (also feste Befehlsbreite mit Zweierpotenz und immer gleicher Aufbau der Befehle) für die selbe Performance mit sehr viel weniger Transistoren, und damit auch mit sehr viel weniger Energie (trotz etwas höherem Takt weil manche Dinge eben mit mehr Befehlen durchgeführt werden müssen), auskommt ist also Konsens. Gut. ;)

Bei richtig dicken CPUs, also mit vielen Funktionseinheiten wie FPU, SSE, AVX usw. und einen hohen Maß an Instruction-Level-Parallelism, spielt das aber keine große Rolle mehr weil der Decoder nur noch einen kleinen Teil der CPU darstellt. Da moderne CISC-CPUs intern auch nur RISC machen gibt es auch keinen wesentlichen Performanceunterschied mehr in dieser Gewichtsklasse. Auch entsprechen die richtig dicken RISC-CPUs nicht mehr so ganz dem R von RISC. Aktuelle x86-CPUs haben fast 1000 Befehle und ein aktueller PowerPC (mit Altivec und all dem anderen Erweiterungskram der da noch so existiert) dürfte sicher auch auf etwa die Hälfte kommen, von "Reduced" kann da nicht mehr die Rede sein und der Decoder ist sicher auch nicht mehr so schlank wie der eines alten ARMv5.

Der wesentliche Nachteil des festen Befehlsformates klassischer RISC-CPUs ist die eingeschränkte Flexibilität, gerade auch was das Hinzufügen von zusätzlichen Coprozessoren angeht, das kostet nunmal eine Menge OpCode-Bits. Die Befehle einfach noch breiter zu machen, so wie z.B. der Itanium das tut, ist meiner Meinung nach die einzigste Möglichkeit um in Zukunft noch leistungsfähige RISC-CPUs entwickeln zu können. Der Itanium hat z.B. 41 Bit pro Befehl und in jedem Befehl sind 3 * 7 Bit für die maximal 3 Operanden fest reserviert so das trotzdem noch 20 Bits für den eigentlichen Befehlsopcode übrig bleiben, damit kann man schon so einiges Anfangen. Der Decoder des Itanium ist zwar nicht gerade klein weil er eben eine große Menge an verschiedenen Befehlen unterstützt (und viele Befehle auch etliche Attribute/Optionen haben) aber in Relation zu ähnlich umfangreichen CPUs geradezu winzig da die Befehle des Itanium sehr homogen aufgebaut sind. Trotz dessen das der Itanium recht unflexible Befehle hat (eben nur auf maximal 3 Operanden beschränkt) erreicht er eine enorm hohe Performance pro Takt (was gerade im wissenschaftlichen/mathematischem Umfeld auch zu einem guten Teil seinem riesigem Register-File geschuldet ist).

Der klare Pluspunkt der konstant-breiten und ausgerichteten RISC-Befehle (was auch auf den Itanium zutrifft) ist das es keine unerwünschten Befehle gibt, wenn man bei x86 einfach an einem anderen Byte anfängt zu disassemblieren kommt man oft auf völlig andere aber trotzdem sinnvolle/gültige Befehlsketten. Das macht das spekulative Dekodieren von Befehlen die die CPU noch nicht ausdrücklich angefordert hat quasi unmöglich aber eben gerade wegen des aufwendigen Decoders bei flexiblen Befehlen wäre es wünschenswert wenn der Decoder schon mal im Voraus eventuell benötigte Befehle dekodiert und in einem Cache ablegt, das würde bei Verzweigungen die Latenz erheblich senken wenn die Verarbeitungsstufe auf fertig dekodierte Befehle (µOPs) zugreifen könnte und nicht erst warten muss bis der Decoder die neu angeforderten Befehle ausspuckt. Intel hat das beim P4 versucht zu verwirklichen, der Trace-Cache (L1-Code-Cache) des P4 enthielt nur dekodierte Befehle aber er kann eben nicht spekulativ im Voraus Befehle laden weil er ja zum dekodieren die richtigen Byte-Adressen benötigt. Ein weiterer Nachteil ist das der Decoder bei flexiblen Befehlen nur sehr eingeschränkte parallelisierbar (also langsamer) ist da ja jeder Befehl erst dekodiert werden kann nachdem vom vorangegangenen Befehl zumindest die Länge exakt ermittelt wurde. Wenn AMD und Intel versprechen das deren Decoder bis zu 4 Befehle pro Takt liefern können dann trifft das nur auf ganz einfache Befehle zu deren Länge extrem einfach zu ermitteln ist (also nur durch analysieren des ersten Bytes), schon bei komplexeren Adressierungsarten oder Präfixen bricht die Decoder-Performance drastisch zusammen.

Ein weiterer Nachteil der unerwünschten enthaltenen Befehlsketten bei flexiblen Befehlen ist das damit dem Return-Based-Programming erst so richtig die Tür geöffnet wird, es sind eben eventuell Befehle im Binär-Code enthalten die der Compiler gar nicht absichtlich erzeugt hat (und eventuell auch nie erzeugen würde). Das ist zwar für den CPU-Designer nur sekundär aber eine Plattform die leichter angreifbar ist ist eben auch nicht so schön. Auf der anderen Seite wurde auch schon gezeigt das Return-Based-Programming selbst bei SPARC (einer absolutem Anti-x86-Architektur) möglich ist so das dieser Aspekt auch nicht überbewertet werden sollte, trotzdem sind hier die Befehle mit fester Größe zumindest leicht im Vorteil.

Im Übrigen ist dieser Disput keine Angelegenheit für "RISC vs. CISC", den wirklich "Reduced" sind heutige RISC-CPUs schon lange nicht mehr und es gibt auch durchaus sehr schlanke CISC-CPUs (z.B. einige CISC-CPUs von Renesas haben gerade mal 100 Befehle und das inklusive Floating-Point, übrigens baut Renesas auch ordentliche RISC-CPUs). Dieser Kampf müsste eigentlich "flexible Befehlsgröße vs. feste Befehlsgröße" heißen.


Der Preis dafür ist, dass du keine kleineren Speicherzugriffe machen kannst und du daher jedes Datenwort ausrichten musst....
Das ist so nicht ganz richtig, grundsätzlich unterstützt auch die ARM-Architektur unausgerichtete Zugriffe nur die realen CPUs können das meistens nicht weil man für unausgerichtete Zugriffe eben mehr Transistoren benötigt und der Hersteller sich sagt das er das dann doch lieber dem Compiler überlässt. Im übrigen sind die Komponenten in der CPU die die nächsten Befehle einließt und die die Nutzdaten Lesen/Schreiben nicht die selben so das jeweilige Einschränkungen nicht auch zwangsläufig für die anderen Komponenten gelten müssen.

RISC-Designs können wesentlich höhere Taktrate erreichen, CISC-Designs wesentlich mehr Arbeit pro Instruktion verrichten
Auch diesen Satz möchte ich nicht unkommentiert stehen lassen. Man kann prinzipiell mit jeder Befehlsarchitektur die selbe Taktrate erreichen, komplexere Vorgänge benötigen dann eben nur mehr Takte. Und komplexe Befehle sind heutzutage längst kein Privileg mehr für CISC-CPUs.

So, jetzt möchte ich diesen Text zerpflückt und korrigiert haben. :-P
Zufrieden? :evil:


Grüße
Erik
Reality is that which, when you stop believing in it, doesn't go away.

 

Einloggen