Apple M1 - Mythen, Mutmaßungen und warum x86 doch (nicht?) am Ende ist! (Teil 2)

Geöffnet als Baustelle! ;) Umbauarbeiten erfolgen noch über einige Bereiche!
Bild von Apple.com.

Frontend bei x86-CPUs - ich hab euch nicht alles verraten

Im letzten Abschnitt des ersten Teiles habe ich euch das Verhältnis von decodierten Befehlen zu den ALUs errechnet und doch ist das da nicht ganz richtig, was ich geschrieben habe, manchmal ist eine Vereinfachung eben nicht ganz richtig und verfälscht das Bild, auch wenn die Tendenz richtig ist. Moderne x86-CPUs decodierend 4 Befehle pro Takt und können damit 2/3 der ALUs mit Befehlen versorgen, der M1 schafft 8 Befehle und versorgt damit 4/5 der ALUs mit Befehlen, soweit so richtig, was ich euch aber unterschlagen habe ist der µOp-Cache moderner x86-CPUs. In diesem Cache legt eine moderne CPU heute dekodierte Befehle ab, um sich die komplexe Umwandlung von Befehlen zu sparen. Daher sieht das Frontend heute ungefähr so aus:

Bild von Teralios.de


Gerade die Umwandlung von x86/IA32-Befehlen ist recht aufwendig und komplex und je mehr Befehle gleichzeitig decodiert werden sollen, um so komplexer wird auch der Decoder. Natürlich bin ich mir bewusst, dass AMD vor ca. 18 Jahren - zur Einführung von AMD64 (x64 oder x86_64) - bei einem Interview sagte, dass die Kompatibilität zu 32 Bit und 16 Bit IA32-Code nicht wirklich komplex ist und nur wenige Transistoren kostet, gleichzeitig muss man hier aber auch beachten, dass die CPUs zu dieser Zeit häufig noch 2-fach Skalar ausgelegt waren und man AMD64 ohnehin auf x86 aufbaute und daher ohnehin ein CISC - µOPs-Decoder benötigt wurde, der die meisten x86-Befehle decodieren musste, das ändert aber nichts daran, dass CISC-Befehle deutlich komplexer zu decodieren sind, als RISC-Befehle, was auch dazu führte, dass Intel mit SandyBridge den µOP-Cache einführte und AMD mit Zen nachzog.


Mit diesem µOP-Cache können theoretisch bis zu 12 OPs im Dispatch-Block auflaufen und dort umsortiert werden, sodass man die Befehle möglichst effektiv auf alle Einheiten aufteilen kann. Damit stehen bei modernen x86 im besten Fall nun 12 OPs für 6 ALUs zur Verfügung und damit zwei Operationen mehr als Rechenwerke vorhanden sind.


Nun muss man aber auch wissen, was im µOP-Cache gespeichert wird. Als Intel diese Funktion mit SandyBridge einführte, sprach man von Schleifen, die ja immer wieder ausgeführt werden müssen und um sich den energiehungrigen Aufwand das Decodieren zu sparen - werden Schleifen im µOP-Cachen zwischen gespeichert und wenn die Sprungvorhersage ermittelt, dass man in diese Schleife erneut kommt, werden die Befehle aus dem µOPs-Cache geladen und ausgeführt.


Heute werden auch weitere Sprünge - nicht nur Schleifen - im µOPs-Cache gespeichert und bei Leerlauf werden diese Sprünge spekulativ - also auf Verdacht - ausgeführt und deren Ergebnisse zwischen gespeichert bis man sie braucht oder verwerfen kann. Man sieht an diesen Maßnahmen, dass Intel aber auch AMD bemüht sind den Decoder weitgehend schmal zu halten, um dennoch ein breites Backend auszulasten.


ARM ist hier mit ihren RISC-Befehlen im Vorteil und dass kann sich auch Apple zu nutzen machen: Viele Befehle aus ARMv8 benötigen keine aufwendige Decodierung, sondern können quasi on-the-fly umgewandelt werden und landen dann im Dispatch-Block und können dort umsortiert werden. Natürlich besitzt heute ARM auch komplexere Befehle, deren Decodierung ist aber immer noch einfacher als bei CISC-Befehlen und damit ist auch der Decoder um ein vielfaches einfacher und kann auch entsprechend breit ausgelegt werden, zumal auch die Umwandlung nicht viel Energie benötigt


Natürlich können Intel und AMD auch versuchen ein 6-fach skalares Backend und ein 4-fach skalares FPU-Backend umsetzen, nur müsste man dann auch den Decoder weiter verbreitern und ebenso auch den µOP-Cache. Die Zutaten sind AMD und Intel bekannt, die Frage ist aber, ob x86 als ISA solche Backends und Frontends auch wirklich effizient zulässt.

Woher kommen eigentlich die Daten? - Von Registern, Caches und Arbeitsspeicher

Bis jetzt wissen wir, dass Apples Firestorm-Kerne ein breiteres Frontend sowie Backend als moderne x86-CPUs haben und daher ein großer Teil ihrer Geschwindigkeit kommt, solche Front- und Backends wollen aber auch mit Daten versorgt werden und ebenso mit Befehlen. Sieht man sich die Caches vom M1 an und nimmt Zen 3 als Gegenpart - ja ich habe mich nun für Zen 3 entschieden, da hier die Informationen einfacher zu finden sind - an, dann fällt auf, dass Apple den einzelnen Kernen möglichst dicke Caches zur Verfügung stellt, auch wenn Zen 3 mit seinem L3-Cache durchaus ein Monster ist, aber brechen wir es mal in einer Tabelle runter:

CacheM1 - Firestorm
Zen 3
L1 - Befehle
192 KiByte
32 KiByte
L1 - Daten
128 KiByte
32 KiByte
L23 MiByte - pro Kern (gesammt 12 MiByte)
512 KiByte
L3
-4 MiByte pro Kern (32 MiB gesammt)
µOPs-Cache-4096 µOPs

Wir sehen, um das Backend des Firestorms auszulasten, greift Apple auf gerade zu gigantische L1-Caches zurück und auch der L2-Cache ist nicht ohne, auch wenn dieser von allen CPUs gemein genutzt wird. Gerade der Datencache ist hier bei jedoch auch wichtig, warum, kommt später. Nimmt man diese Zahlen, sieht man, dass die Firestorm-Kerne 6-mal mehr L1i-Cache haben sowie den vierfachen Datencache im Level 1 und die 6-fache Menge an L2-Cache - rechnerisch, es kann aber auch das 24-fache werden, wenn ein Firestorm den Cache für sich selbst hat.


Natürlich müssen Daten aus diesen Caches geholt werden und dass passiert bei einer CPU mit den Load und ebenso den Store-Einheiten. Ein Firestorm-Kern greift dabei auf 2 LOAD (LD) und 1 ST-Einheit zurück sowie eine Einheit, die beides kann und je nach Bedarf ein Load oder eben ein Store verarbeitet. Zen 3 hat 3 Load-Einheiten und 2 Store-Einheiten. Beide sind also ungefähr gleich mächtig und genau hier liegt der nächste Knackpunkt: Die Register! Load und Store laden und schreiben Daten aus den Registern in die Caches und sollten gewisse Daten nicht in den Caches gefunden werden, geht es in den nächsten Cache, bis man zum RAM kommt, dabei ist gilt: Sind die Daten in den Registern, ist es für die Leistung der CPU am besten, danach folgt der L1d-Cache, dann der L2 und so weiter, das benötigt nicht nur Zeit, sondern eben auch Energie.


Gehen wir kurz auf die ISA IA32 und ARMv8 ein: Moderne x86-CPUs haben im normalen Teil heute üblicherweise 16 Register (R0 - R15), bei einer 4-fach Skalaren CPU müssen alle Register in dem Fall zumindest viermal vorhanden sein, macht also mindestens 64. Dazu kommt, dass eine AMD-CPU auch noch zwei Threads verarbeiten kann, also muss die Anzahl noch mal verdoppelt werden auf 128 Register im Registerfile, da man sonst jedes Mal Daten aus dem Cache laden müsste und am Ende - ihr könnt es euch denken - braucht man natürlich für die LD- und ST-Einheiten noch Register, damit man Date hineinschreiben aber auch heraus speichern kann, was am Ende bei Zen 3 in 192 Register mündet, nicht sehr wenige auf den ersten Blick und hier könnte man sehr viel Rechnen, aber schauen wir uns den Firestorm an - und die Werte sind nicht ganz gesichert - sollen hier ca. 354 Register vorhanden sein, also knapp die doppelte Menge. ARMv8 sieht 31 Register (R0 - R30) für den normalen Teil vor, macht bei 6 ALUs also bereits 186 Register, da der M1 nur einen Thread pro Kern hat, müssen wir diese nicht verdoppeln und doch sind diese Verdoppelt, so dass sich da die LD- & ST-Einheiten richtig austoben können.


Natürlich kann man mit den 192 Registern im Zen 3, auch dank Register-Renaming, sehr viel erreichen, gerade auch be 4 ALUs, man muss aber eben bedenken, dass sich 2 Threads diese 192 Register teilen müssen und ebenso die 4 ALUs und das liegt eben an der ISA, die heute nur 16 Register vorsieht. Man muss bei x86-Prozessoren also deutlich früher auf Daten im L1 und L2-Cache zurückgreifen, als bei einer ARMv8-CPU, die mit 31 Registern deutlich mehr Potenzial bietet.


Und ähnlich verhält es sich auch im FPU-Teil: Neon sieht hier 32 Register vor, MMX und SSE anfangs nur 8 Register, die mit AMD64 auf 16 Erweitert wurden und erst mit AVX512 sind 32 Register vorgeschrieben und das sieht man hier auch an den LD- und ST-Einheiten, gerade im Verhältnis zu den ALUs.


Um euch das ganze noch mal im Detail zu zeigen - ein Bild ist manchmal besser als jeder Text, die Backends von Zen 3 (Oben) und Firestorm (Unten)

Zen 3 (Oben) und M1 (Unten). / Bild von Teralios.de


Wir sehen hier, bei AMD benötigt man 3 LD und 2 ST-Einheiten um ein Backend mit 4 ALUs zu versorgen, während man mit einer ähnlichen Konfiguration beim M1 ganze 6 ALUs versorgt und das liegt eben wirklich an den Registern, man kann einfach wesentlich länger mit 31 Registern in einem Thread rechnen als mit 16 Registern und muss so seltener auf den L1, L2 und den RAM ausweichen, das spart Zeit und Energie. Dazu kommt eben, dass der am nächsten liegende Cache beim M1 sehr groß ist, was im Fall der Fälle auch wieder Zeit spart und Energie und so weiter.


Das Geheimnis, warum also der M1 von Apple so schnell ist, ist relativ einfach: Ein breites Backend wird von einem breiten Frontend versorgt und kann auf viele Register zugreifen und auch sehr große Caches, die auch sehr nah liegen. AMD und Intel könnten auch entsprechend breite Backends und Frontends bauen, aber CISC-Befehle sind komplex zu decodieren, weswegen man hier mit Caches versucht den Decoder zu entlasten, ARMv8-Befehle sind einfacher zu decodieren, weswegen man diesen Spaß (noch) nicht braucht.


Apple hat mit den Firestorm-Kernen ein sehr breites Design umgesetzt und schafft es diese Leistung auf die Straße zu bringen bei einem Thread, die IPC liegt beim Cinebench um ca. 40 % höher, was man hier gut auch als Durchschnitt sehen kann. Dazu kommt, dass die Infrastruktur durch die schiere Anzahl an Registern sogar etwas schmaler ausfallen kann als bei modernen x86-CPUs - LD- und & ST-Einheiten - und man doch viele ALUs versorgen kann.

Die letzte Bastion SIMD! Oder wie man mit Kanonen auf Spatzen schießt

Während der M1 durch seinen breiten Aufbau mit den meisten CPUs heute - zumindest was die IPC angeht - den Boden wischt, gibt es noch eine Bastion für x86-CPUs, die sie in der nächsten Zeit auch zum Teil halten wird, auch wenn dort die Konkurrenz groß ist, besonders durch GPUs, es sind die SIMD-Einheiten oder eben das, was wir in dem Text oft als FPU bezeichnet haben: Single Instructions Multiple Data, oder eben kurz SIMD. Bei diesen Einheiten wird eine Operation auf viele Werte angewendet und ich sprach ja bereits von Vektoren. Die FPU beim Firestorm-Kern besteht aus 4 ALUs die mit 128 Bit arbeiten, moderne x86-CPUs bieten mindestens 2 ALUs mit 256 Bit und mit AVX512 sogar 512 Bit. Nimmt man nun das Maximum bei x86 - 2 mal 512 Bit, dann kommt man auf 1024 Bit Vektorbreite, beim M1 nur auf 512 Bit, damit ist die theoretische Rechenleistung - bei einem GHz - mindestens gleichwertig bis doppelt so hoch.


Nur hier gibt es ein kleines Problem: Man muss diese breiten Einheiten auch füllen können. Bei 512 Bit braucht man bei SP-Float (32 Bit) pro Einheit 16 Werte: [a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16]. Ein ziemlich langer Vektor und davon benötigt man zwei, um wirklich die Rechenleistung abzurufen. Es gibt Szenarien, in denen man solche Vektoren durchaus nutzen kann - zum Beispiel Bildbearbeitung - nur müssen die Algorithmen auch dafür optimiert werden.


Im M1 reichen Vektoren mit 4 Elementen [a1, a2, a3, a4]. So einen Vektor zu befüllen, fällt in der Regel wesentlich einfacher und da man 4 solcher Vektoren verarbeiten kann, gibt es auch Szenarien, bei denen diese Art von FPU am Ende des Tages eher seine Rechenleistung auf die Straße bringt, als die ultrabreiten AVX512-Einheiten.


Dazu kommt, dass man im HPC-Markt für SIMD heute eher auf GPUs zurückgreift, da diese viele solcher SIMD-ALUs haben. Klar, eine GPU muss über den Treiber angesprochen werden, nur kann man dann gleich eine Vielzahl von Vektoren berechnen lassen, bei einer NVIDIA A100 hat man 128 Vektoren mit 64 Werten, bei einer 3090 TI sogar 164 Vektoren mit 64 Werten.


Man kann also an der Stelle sagen: Die 128 Bit-ALUs decken das meiste ab und erst in speziellen Fällen profitiert man von 256 Bit und je breiter die Vektor-ALU wird, umso unwahrscheinlicher wird es, dass man im Alltag die ALU wirklich auslastet. Das bedeutet, in speziellen Fällen, wird eine moderne x86-CPU mit dem M1 den Boden wischen, aber da muss die Software auch mitspielen und optimiert sein.

Hoffnung AMX? Nein, nicht wirklich!

Intel hat vor einiger Zeit schon ihre Advance Matrix Extension angekündigt, also eine Erweiterung, die es x86-Prozessoren spezielle Matrizen-Operationen beibringen soll. Für sich gesehen eine tolle Idee und sicher auch praktisch, nur gibt es da ein Problem: Der M1 besitzt bereits eine NPU/TPU - Neural Processing Unit / Tensore Processing Unit, die auf Matrizen-Operationen spezialisiert sind. In der Regel wird hier ein MAD/MAC durch geführt und AMX könnte mächtiger werden, doch für den Alltag reicht das, was die NPU/TPUs heutiger ARM-Prozessoren können. Klar, die NPU muss über eine API angesprochen werden und man muss seine Software entsprechend dafür optimieren, das wird aber bei AMX nicht anders sein.


Egal wie man es an dieser Stelle nun drehen und wenden will - auch wenn es so manchen Journalisten und PC-Freak nicht gefällt - Apple hat mit dem M1 eine sehr gute CPU entwickelt, die die Vorzüge von breiten Front- und Backends zeigt und welches Potenzial in der ARM ISA steckt. Vor Jahren konnte man ARM noch belächeln, weil die ISA in der Regel in sparsamen kleinen Kernen verwendet wurden, die zwar effizient waren, aber einem x86-Kern nicht wirklich gefährlich werden konnte. Nur lag das eben nicht an der ISA, wie es so manche Möchtegerns über Jahre behauptet haben, sondern einfach daran, dass man kleine Kerne entworfen hat. Der Kommentar so manches Journalisten und PC-Freaks, dass ARM nicht für hohe Leistun ausgelegt ist, während es x86 schon ist, hat sich spätestens mit dem M1 als Bullshit entpuppt und genau das ist das Problem zurzeit: Für manche kann eben nicht sein, was nicht sein darf!


Entsprechend versucht man nun nach Strohhalmen zu greifen, wie eben der initiale Hinweis, dass ja die Messung zur "Single-Core-Performance" moderne x86-CPUs benachteiligen würde, was aber auch - und an dieser Stelle sei mir die direkte Wortwahl erlaubt - einfach nur stumpfer Bullshit ist, denn wo nichts ist (6 gegen 4 Rechenwerke, 4 gegen 2 Rechenwerke in der FPU, 31/32 Register gegen 16/16(32 bei AVX512), kann auch nichts herkommen.


x86 ist Ende der 70er entstanden, als Prozessoren noch primär über Assembler programmiert wurden, RISC ist ein Kind der 80er, als man immer stärker auf Programmiersprachen zurückgreifen konnte, weil die Compiler immer besser wurden und es daher nicht mehr wichtig war, dass ein Mensch den Assembler-Code lesen kann. Ebenso bemerkbar macht sich der technologische Fortschritt bei der Halbleiterfertigung in dieser Zeit: In den 70er waren Transistoren wertvoller als in den 80er also versuchte man so viele wie nötig, aber so wenig wie möglich in der ISA unterzubringen um Transistoren zu sparen. In den 80er erkannte man dann aber, dass Speicherzugriffe Energie kosten und da Transistoren billiger wurden, hat man in den ISA mit Registern nicht mehr gegeizt - Intels eigentlicher IA32/x86-Nachfolger IA64 hat sogar ganze 128 Register und warum? Weil das Warten auf Caches und RAM Leistung kostet und Energie verschwendet, oder um es aus dem IA64-Artikel von Wikipedia zu zitieren:

Die Architektur versucht, die Wartezeiten auf den Speicher zu verringern, indem eine große Zahl Register auf dem Prozessor vorhanden ist. So gibt es 128 64-Bit-Register für ganzzahlige Berechnungen, 128 82-Bit-Register speziell für Gleitkomma-Daten und 64 1-Bit-Vorhersageregister, über die die Befehle bedingt ausgeführt werden. Dies erlaubt es, mehr Informationen in den Registern zu halten, anstatt jedes Mal den langsamen Weg über Cache oder Arbeitsspeicher zu beschreiten, wenn Daten benötigt werden.

x86 wird nicht so schnell verschwinden

Zum Schluss sei aber gesagt: x86 wird so schnell nicht verschwinden. Ja, Apple zeigt mit dem M1, was möglich ist - auch ohne µOP-Cache - mit einer moderne(re)n ISA und auch Amazon, Microsoft und auch NVIDIA blasen zum Angriff auf die x86-Welt, es wird stürmisch werden. Doch ein Problem kann weder Apple, Amazon, Microsoft und auch nicht NVIDIA zeitnah lösen: Der Softwaresupport. Apple hat sein kleines eigenes Universum mit Swift, Cocoa und Metal geschaffen, was die Portierung zwischen x86 und eben ARMv8 einfach macht. Microsoft hat diese Arbeit aber noch vorsich und selbst wenn Microsoft ihre APIs weiter aufräumt und vereinheitlicht, bleibt alte Software hier schnell auf der Strecke. Dazu kommt, dass Intel schon immer darum bemüht war, hochoptimierte Standardbibliotheken zur Verfügung zu stellen und solange ARM hier nicht nach zieht, wird x86 nicht so schnell verschwinden und wer weiß, vielleicht beflügelt ja die ARM-Entwicklung die x86-Entwicklung wieder und Intel und AMD finden neue kreative Lösungen, um doch ultrabreite Backends zu nutzen, wer weiß!


Ein kleiner Hinweis am Ende [09.06.2021]

Ich habe nun ein neues MacBookPro 13" und werde bei Zeiten entsprechende Benchmarks nachreichen!