Da ich krank bin, im Bett liege und ich mich gerne ablenken möchte, schreibe ich mal zusammen, was ich über diese Themen weiß.
Vorab meine Qualifikation: ich bin Softwarearchitekt und seit 2001 als Entwickler tätig, hab mich in meiner Jugend exzessiv mit x86 auseinandergesetzt (Sound- und Grafikdemoszene, falls das jemandem hier etwas sagt). Programmiere heute hauptberuflich in C#, privat auch noch in C++, Ruby und x86-Assembler.
Ich schreibe das hier deshalb, weil ich damit ausdrücken möchte, daß das nicht irgendwelche “Spinnereien” oder “Halbwissen” darstellt, sondern durchwegs auf Fakten und, wo angegeben, “educated guesses” basiert.
Threads, Cores und Scheduling
Grundsätzlich ist es so, daß Windows SELBST das Konzept des “Threads” kennt, das ist tief in der sogenannten “Win32” (heute halt “Win64”) API verankert. Windows macht das grundsätzlich so, daß neu gestartete Threads auf jene Cores verteilt werden, die gerade am wenigstens zu tun haben.
Die Messung dazu basiert, wie übrigens ÄHNLICH (nicht identisch - bei DAWs ist da eine Besonderheit dabei, auf die ich später eingehen werde) auch bei Cubase, auf der Zeit, die einem Kern zwischen “Kontextwechseln” (das ist, wenn ein Kern von einem zugewiesenen Thread zum nächsten Thread schaltet) übrig bleibt. Meines Wissens nach wird das bei Windows über einen bestimmten Zeitraum ermittelt.
Nehmen wir also z.B. einen Dualcore-Prozessor her, ohne HT (um es einfacher zu halten).
Core 0 hat 70% Last, Core 1 hat 10% Last.
Kommt nun ein Thread dazu, dann wird dieser neue Thread zuerst Core 1 zugewiesen, weil ja Core 0 ohnehin schon recht tüchtig am werken ist.
Cubase, so wie die meisten DAWs wohl, erzeugt nun für jede Spur sinnvollerweise (das hätte ich exakt auch so programmiert) einen eigenen Thread. Die Zuteilung zu Cores erfolgt standardmäßig von Windows her durch den Windows-Scheduler, es ist aber auch möglich, über die sogenannte “ThreadAffinityMask” (so heißt das Konzept in der Windows-API), Threads durch das Programm zu “schedulen”.
(Scheduling = Threads einerseits auf die Cores zu verteilen und andererseits, diese auch “reihum” mit Rechenzeit zu versorgen. Ein Thread kann auch, durch sogenanntes “Yielding”, selbst sagen: “herst, ich bin fertig für den Moment, ich gebe freiwillig den Rest meiner mir zustehenden Rechenzeit ab”. Lustigerweise hieß die Methode in der Windows-API früher auch so. Nämlich “Yield()”. Heute nimmt man eher “Sleep()” dafür.)
Wie bereits angedeutet erfolgt die Zuweisung von Rechenzeit an Threads “reihum”, das heißt, daß mehrmals pro Sekunde zwischen den Threads pro Core umgeschalten wird. Das geht so schnell, daß der Benutzer (und auch der Programmierer in vielen Fällen) den Eindruck hat, daß die Threads nicht nur über Cores hinweg (dort ist es ja der Fall), sondern auch pro Core (dort wird umgeschalten) tatsächlich parallel laufen.
Die ASIO-Auslastung, die Spursynchronisation, die Divergenz zur CPU-Auslastung - und warum niedrige Latenzen übermäßig Rechenzeit fressen
Das ist ein happigeres Thema, weil hier viele Mißverständnisse vorliegen und viel Viertel- und (im Optimalfall oft) Halbwissen herangezogen werden, um ein Urteil zu bilden - und mir als Entwickler stellts dann manchmal die Haare auf, wenn ich die vielen Vermutungen lese (ist nicht böse gemeint, aber manchmal ist es wirklich schauderhaft).
Grundsätzlich zeigt die ASIO-Auslastung an, wieviel Zeit von der Zeit, die zur Berechnung eines gesamten Playbackbuffers zur Verfügung steht, tatsächlich gebraucht wird.
Rechenbeispiel:
Latenz = 10ms
Berechnungen für alle VSTis, etc… benötigen pro 10ms-Buffer 5ms ergibt eine ASIO-Auslastung von 50%.
Das wäre ja einfach.
Aber:
Es gibt Abhängigkeiten. Sends, Gruppen, Sidechains. Hier muß synchronisiert werden.
Ein Beispiel:
Zwei Spuren + 1 Send.
Spur 1 braucht 1 ms, Spur 2 braucht 2 ms, beide senden auch zum Send.
Das bedeutet aber, daß der Send erst nach 2 ms überhaupt mit der Berechnung des, z.B., Halls beginnen kann. Vorher ists da zappenduster, weil der ja auf die Datenpakete der anderen beiden Threads warten muß (das nennen wir Entwickler übrigens “Threadsynchronisation”, bzw. ist das das Teilgebiet “Threadkommunikation”, das damit untrennbar verbunden ist).
Ist jetzt der Core, der mit dem Send beschäftigt ist, ansonsten unterbeschäftigt, dann kann es sein, daß hier sogar die ASIO-Auslastung gar nicht steigt, wenn auf diesem Core ein anderer Thread hinzukäme, weil der nur den freien Zeitslot der ersten 2 ms vor Einsetzen der Hallberechnung nutzen könnte.
Somit aber kann es sein, daß der Taskmanager unter Windows nur, hm, 10% CPU-Last anzeigt, die ASIO-Auslastung aber bereits bei 50% liegt, weil da eben, durch die Synchronisation bedingt, Rechenzeit brach liegt.
Warum aber brauchen nun niedrigere Latenzen mehr Rechenleistung?
Das hat folgende Gründe (ich HOFFE, die Liste ist erschöpfend, bin mir da aber nicht zu 100% sicher):
- Kontextwechsel (umschalten zwischen Threads) kosten Zeit durch Umladen der CPU-Register, Cache-Thrashing, etc… (früher sprach man auch noch vom hierzu nötigen Interrupt, aber heute lacht man über die paar Clockcycles, nebenbei bemerkt - die Pipeline des Cores muß auch neu befüllt werden, das ist aber nicht so schlimm, weil das eigentlich nur einem Branch Prediction-Miss entspricht, der zwar auch ungünstig, aber “zu überleben” ist)
- Manche (nicht alle) DSP-Algorithmen sind effizienter, wenn sie “durchgehalten” werden über größere Datenblöcke (wegen der Rechenzeit, die für die Initialisierung der Berechnung eines neuen Datenblocks benötigt wird)
- Der Sychronisationsaufwand steigt, wenn häufiger sychronisiert werden muß
- Unter bestimmten Umständen kann (muß aber nicht) auch die Kommunikationsinitialisierung mit der Audiohardware Rechenzeit fressen (da sind RME ziemlich vorn, die haben einfach ASIO direkt in die Hardware implementiert, was ich ziemlich leiwand finde, ich werde auch nie wieder was anderes kaufen)
SMT (Hyperthreading)
Was macht jetzt also Hyperthreading?
Nun, es ist so, daß der limitierende Faktor bei heutigen CPUs (pipelined, “superskalar”, RISCish, etc…), was die Ausführungseffizienz betrifft, eher auf der Seite des “architectural state” als bei den tatsächlich befehlsausführenden Einheiten zu suchen ist.
Soll heißen:
So eine CPU kann ur schnell z.B. multiplizieren - auch mehrere Multiplikationen auf einmal (das nannte man früher “superskalare Architektur”), aber die “Anlieferung” der CPU-Befehle und der innere Zustand der CPU (Register, Pipeline) sind hier limitierend.
Um jetzt die Ausführungseinheiten (z.B. eben so eine Multiplikatorschaltung) besser auszulasten, tut die CPU so, als wäre da nicht 1 Core, sondern gleich 2 Cores, wo in der Realität aber nur 1 Core vorhanden ist (der “architectural state” ist also 2x vorhanden, die Ausführungseinheiten bleiben aber gleich von der Zahl her).
Somit schaut eine 2-Core-CPU wie eine 4-Core-CPU aus.
Natürlich kann man damit nicht die volle Leistung einer mit doppelt sovielen echten Cores ausgestatteten CPU erreichen, aber man kann die Ausführungseinheiten besser auslasten (die stehen dann nicht einfach nur mit Zigarette und Bierflasche herum, sondern hackeln wirklich was) und so einiges an Leistung rausholen.
FRÜHER (!) war das ineffizient umgesetzt, sowohl im Betriebssystem (die frühen Scheduler kannten den Unterschied zwischen “echten” und “simulierten” Cores nicht und haben nicht darauf Rücksicht genommen, heutige Scheduler tun das aber), als auch in der CPU - darum war, in der PC-Bronzezeit (Mitte der Nullerjahre), Cubase OHNE HT besser dran.
Aber mit den heutigen Sandy Bridges, etc… ist das alles kein Thema mehr. Da würde ich eher mal ausprobieren, wie sich die eigenen Projekte mit HT verhalten. Bei MIR ists MIT HT wesentlich besser, aber das liegt wohl definitiv an der Projektstruktur.