PC-Prozessor statt DSP?

15. April 2009, 10:26 Uhr | Peter Massam

Sind PC-Prozessoren wie der »Pentium« die besseren DSPs? Tatsächlich sind solche CPUs oft mit deutlich höherer Frequenz getaktet als DSPs. Und da sie oft im Gaming-Bereich eingesetzt werden, unterstützen viele von ihnen beispielsweise auch komplexe Vektoroperationen. Doch welche Vor- und Nachteile hat es, PC-Prozessoren als DSPs einzusetzen?

Diesen Artikel anhören

Sind PC-Prozessoren wie der »Pentium« die besseren DSPs? Tatsächlich sind solche CPUs oft mit deutlich höherer Frequenz getaktet als DSPs. Und da sie oft im Gaming-Bereich eingesetzt werden, unterstützen viele von ihnen beispielsweise auch komplexe Vektoroperationen. Doch welche Vor- und Nachteile hat es, PC-Prozessoren als DSPs einzusetzen?

Auf der Suche nach der richtigen Plattform für die Verarbeitung eines DSP-Algorithmus’ schauen sich viele Ingenieure als erstes bei den speziellen DSPs (digitale Signalprozessoren) um, während andere zunächst FPGAs, ASICs oder andere programmierbare Logikbausteine in Erwägung ziehen. Nur wenige werden jedoch in der Plattform, mit der sie den Großteil ihrer Arbeitstage verbringen, einen geeigneten Kandidaten sehen. Dabei stellen moderne PCs leistungsfähige Systeme dar. Deren CPUs werden mit deutlich höherer Frequenz getaktet als DSPs, und die von vielen PC-Prozessoren unterstützten Vektoroperationen sind mit jenen vergleichbar, die in DSPs implementiert sind.

Ist die Zeit des reinrassigen DSPs damit abgelaufen? Wie jede technische Aufgabe lässt sich auch ein digitales Signalverarbeitungsproblem auf vielerlei Weise lösen, und jede Lösung hat ihre spezifischen Vor- und Nachteile. Das Resultat ist in der Regel ein Kompromiss aus Preis, Performance, Verfügbarkeit, Flexibilität, Stromverbrauch, usw.

FPGAs erzielen durch Parallelverarbeitung ein Höchstmaß an Leistungsfähigkeit, allerdings zu einem hohen Preis. ASICs wiederum können diesen Performance-Vorteil zu niedrigeren Kosten bieten – allerdings nur, wenn die Stückzahlen hoch sind. PC-Prozessoren haben gegenüber programmierbaren Logikbausteinen eine unübertroffene Flexibilität, geraten aber bezüglich der Performance ins Hintertreffen. Reine DSPs bieten Unterstützung für sehr spezielle Algorithmentypen (z.B. Bit-Reversed-Adressierung für FFT-Algorithmen oder Add-Compare-Select für Viterbi-Algorithmen).

Ein entscheidender Vorteil, den die Verwendung einer PC-basierten Plattform für die digitale Signalverarbeitung hat, ist schlicht die Tatsache, dass den meisten Ingenieuren ein solches System zur Verfügung steht. Das Design von Algorithmen und Software kann daher mit weitgehender Annäherung an die finale Hardware voranschreiten, lange bevor diese tatsächlich verfügbar ist. Mit Benchmark-Implementierungen wichtiger Algorithmen lässt sich außerdem rasch ermitteln, ob die betreffende Plattform die für das Produkt erforderliche Verarbeitungsleistung auch wirklich aufbringen kann.

Schnelle Evaluierung

Dies ist zumindest der Idealfall. Vorsicht ist jedoch allein schon wegen der großen Vielfalt an PC-Prozessoren geboten. Die Unterstützung für Vektoroperationen kann stark differieren, auch die Details des Cache-Systems haben großen Einfluss auf die Performance. Der große Wert, den die PC-Welt auf den Markennamen des Prozessors legt, kann ein Teil des Problems sein. Selbst Ingenieure wissen nur selten, welche spezielle CPU in ihrem PC verbaut ist – vielleicht ein »Celeron«. Aber welche der vielen Varianten dieses Prozessors ist es genau?

Als weiterer gravierender Pluspunkt kommt die Leistungsfähigkeit der PC-Compiler hinzu. Getrieben vom intensiven Wettbewerb, wurden sie über die Jahre immer weiter verbessert. Anders ist es bei den Compilern für DSPs: Sie bringen es beim Optimieren von schleifenbasiertem Code häufig auf sehr gute Ergebnisse, doch bei Steuerungscode sind sie nur selten effizient. Dies mag als der beste Kompromiss erscheinen, doch würden es die meisten Entwickler wohl vorziehen, wenn der Compiler effizienten Steuerungscode generierte. Tatsache ist, dass es meist wesentlich einfacher ist, die inneren Schleifen in Assemblercode umzuschreiben als den Steuerungscode (in Assembler) neu zu schreiben. Der Hauptspeicher von Desktop- und Laptop-PCs ist in der Regel mehrere hundert Megabyte groß. Das ist wesentlich mehr als in den meisten DSP-basierten Systemen zur Verfügung steht und kann für einige DSP-Algorithmen ein großer Vorteil sein. Da moderne PC-Architekturen mehrere Cache-Ebenen enthalten, können außerdem relativ langsame (und damit preisgünstige) Speicherbausteine verwendet werden. Die Compiler besitzen ferner genaue Informationen über das Cache-System und können es deshalb die meiste Zeit effizient nutzen. Ungeachtet dessen sind die Cache-Systeme komplex, was nachteilig sein kann, wenn die Datenverarbeitungsschleifen über die Fähigkeiten des Compilers hinaus optimiert werden müssen. Die Bedingungen für einen »Cache Hit« oder »Cache Miss« zu identifizieren, kann alles andere als trivial sein.

DSP-Algorithmen erfordern meist nur den Zugriff auf eine überschaubare Zahl einfacher Hardware-Devices, für die nur selten komplizierte Treiber erforderlich sind. Allerdings sind die Algorithmen lediglich ein Teil eines Systems, das meist in der Lage sein muss, über komplexere Kanäle zu kommunizieren. Häufig übernimmt diese Aufgabe ein separater Controller, doch wenn beide Elemente des Systems auf einer gemeinsamen Plattform zusammengefasst werden sollen, wird die PC-basierte Lösung attraktiv. Für die meisten Betriebssysteme, die auf einer PC-Architektur lauffähig sind, gibt es eine große Auswahl an Gerätetreibern.

Zu den Nebeneffekten, die das Wachstum der Gaming-Industrie mit sich gebracht hat, gehört die Tatsache, dass Vektoroperationen Eingang in das Design der Prozessoren für Desktop- und Laptop-PCs gefunden haben. Diese Operationen unterstützen die Manipulation der Grafikdaten, die zur Darstellung immer realistischerer Spieleumgebungen nötig sind. Da es sich hierbei um einen Teilbereich der DSP-Technik handelt, überrascht es nicht, dass die besagten Vektoroperationen auch für andere DSP-Algorithmen benutzt werden können.

Spiele als Wegbereiter

Das Konzept der auch als SIMD-Operationen (Single Instruction Multiple Data) bezeichneten Vektoroperationen dürfte den meisten Ingenieuren, die bereits mit modernen DSPs zu tun hatten, vertraut sein. Im Prinzip geht es darum, einen einzigen Befehl zu verwenden, um eine Operation mit mehreren Datenwerten auf einmal auszuführen. Zum Beispiel bewirkt der ADDPS-Befehl aus den SSE-Erweiterungen, dass vier Gleitkomma-Wertepaare einfacher Genauigkeit gleichzeitig addiert werden. Von diesen Vektoroperationen gibt es eine ganze Reihe von Varianten. Bei Intel sind es die Befehlssätze »MMX«, »SSE«, »SSE2«, »SSE3«, »SSSE3« und »SSE4«, während die Bezeichnungen der AMDVarianten »3DNow!«, »Enhanced 3DNow!«, »3DNow! Professional« und »SSE5« lauten. Trotz der vielen Ähnlichkeiten zwischen den Extensions von AMD und Intel sind sie nicht immer kompatibel. Im Allgemeinen ist es am besten, die Details eines jeden Befehlssatzes zu untersuchen und die Wahl des Prozessors daran auszurichten, welcher Befehlssatz den Anforderungen des jeweiligen Algorithmus’ optimal gerecht wird.

Bei der Anwendung dieser Erweiterungen gilt, es bestimmte Feinheiten zu beachten. Am wichtigsten ist, dass das Abspeichern und Laden der Daten für die Vektoroperationen in großen Blöcken (z.B. jeweils 64 Bit oder 128 Bit gleichzeitig) erfolgt. Diese Datenworte müssen an den richtigen Adressgrenzen ausgerichtet werden, um effiziente Speicherzugriffe ausführen zu können. Eine weitere Besonderheit ist, dass die Nutzung dieser Extensions nicht mit konventionellen Gleitkomma-Operationen verzahnt werden kann, da die Befehlssätze auf dieselben Ressourcen zugreifen. Zum Beispiel muss der Befehl »EMMS« der MMX-Extensions am Ende einer MMX-Routine stehen, damit nachfolgende Gleitkomma-Operationen korrekt ablaufen.

Die Verwendung von Vektoroperationen hat des Weiteren gravierende Auswirkungen auf das Design der Algorithmen-Implementierungen.

Die Anpassung einer Implementierung an eine SIMDArchitektur nennt sich Vektorisierung. Bild 1 zeigt eine vektorisierte Implementierung eines FIR-Filters (Finite Impulse Response), in der ein Vektor vier Werte enthält. Zu Beginn einer jeden Iteration der äußeren Schleife wird ein Vektor mit Signalproben aus dem Speicher geholt. In der inneren Schleife multipliziert der Prozessor vier Koeffizienten-Vektoren mit dem Signalproben-Vektor, die Ergebnisse summiert er in vier Akkumulatoren auf. Nach der letzten Schleife befinden sich in diesen Akkumulatoren Teilsummen, die zum Produzieren von vier Ergebniswerten kombiniert werden müssen. Hierbei ist hervorzuheben, dass dieser Prozess in verschiedenen Bereichen ineffizient ist. Zum Beispiel muss eine Reihe von Multiplikationen mit Null explizit ausgeführt werden, und außerdem sind vier Tabellen mit Filterkoeffizienten zu erstellen (eine Tabelle für jeden Offset der Koeffizienten innerhalb der Vektorstruktur). Alternative Strukturen können für Filter mit wenigen Koeffizienten besser geeignet sein, und sogar eine skalare Implementierung kann sich als beste Variante herausstellen.

Job_05_Plextrek_01_af.jpg
Bild 1: Vektorisierte Implementierung eines FIR-Filters

Die Sache mit dem Cache

Das Speichermanagement ist meist derjenige Faktor, der die weitreichendsten Auswirkungen auf die Performance einer PC-CPU hat, da der eigentliche Hauptspeicher (gegenüber dem Speicher eines DSP-Systems) stets langsam ist. Dieser Speicherengpass betrifft übrigens nicht nur DSP-Algorithmen, sondern die gesamte auf einem PC laufende Software. Aus diesen Gründen haben die Prozessorhersteller viel Erfindergeist in das Design von Cache-Speichern investiert, um die Langsamkeit des Speichers wirksam zu kompensieren. Die Cache-Systeme der verschiedenen PC-CPUs unterscheiden sich erheblich. Als Beispiel soll hier der Level-1-Daten-Cache des »Pentium 4« dienen (Bild 2). Dabei handelt es sich um einen »4-Way Set Associative «-Cache mit jeweils 64 Byte langen Zeilen und einer Kapazität von 8 KByte. Speicherinhalte werden stets in Blöcken zu je 64 Byte im Cache abgelegt. Welche Zeile im Cache verwendet wird, hängt von der Adresse des ersten Bytes in dieser Zeile ab, die logischerweise immer ein Vielfaches von 64 ist. Fünf Bits der Adresse dienen zur Identifikation einer von 32 Zeilen, die sich zum Cachen eines bestimmten Datenblocks nutzen lassen. Jede Zeile wird somit zum Ablegen einer ganzen Reihe verschiedener Adressregionen (Pages oder Seiten) verwendet. Um zu verhindern, dass eine Cache-Zeile durch den ersten Zugriff von/aus einer ebenfalls dieser Zeile zugeordneten Adresse überschrieben wird, gibt es für jede Zeile vier »Ways«. Überschrieben wird stets derjenige Weg, dessen Nutzung am längsten zurückliegt.

Job_05_Plextrek_02_af.jpg
Bild 2: Level-1-Cache eines »Pentium 4«

Aus dieser Architektur ergeben sich verschiedene Konsequenzen:

  • Wird ein Cache-Zugriff nicht fündig (Cache Miss), sind 64 Bytes aus dem Speicher (bzw. bei vielen CPUs aus dem Level-2-Cache) zu laden, auch wenn nur ein einziges Byte benötigt wird. Cache-Misses kosten deshalb viel Verarbeitungszeit.
  • Wird ein Datenblock, den ein Algorithmus braucht, auf den Beginn einer Zeile ausgerichtet, kommt es für das erste Byte zu einem Cache-Miss, doch die nächsten 63 Bytes werden im Cache gefunden, falls sie benötigt werden.
  • Werden die Daten nicht sequenziell benötigt, ist Sorgfalt bei der Anordnung der Daten im Speicher erforderlich. Im ungünstigsten Fall kann es bei jedem vierten Zugriff zu einem Cache-Miss kommen, wenn die Daten von Adressen mit Abständen von jeweils 2048 gelesen werden (diese Daten werden von verschiedenen Seiten denselben Cache-Zeilen zugewiesen). Ein Beispiel wäre ein Algorithmus, der nacheinander Bytes mit jeweils demselben Offset aus mehreren 2048 Byte fassenden Puffern lesen muss. Bei jedem vierten Lesezugriff kommt es hier zu einem Cache-Miss. Würde man die Puffergröße hier auf 2112 Bytes (2048 + 64) anheben, richteten sich die Lesezugriffe an verschiedene Cache-Zeilen und es käme deutlich seltener zu Cache-Misses.

Cache-Misses vermeiden

Es gibt mehrere Verfahren, die Cache-Nutzung zu optimieren. Erwähnenswert ist jedoch die in vielen CPUs verfügbare »Prefetch«-Anweisung, die den Cache-Controller anweist, den Inhalt einer bestimmten Speicherstelle (tatsächlich sogar die gesamte Zeile, in der sich die betreffende Adresse befindet) dediziert in den Cache zu laden. Mit diesem Befehl lässt sich der Cache auf eine Instruktion vorbereiten, die zu einem späteren Zeitpunkt einen Lesezugriff auf diese Adresse ausführt. Zwar erhöht Prefetch den Verarbeitungsaufwand in einer Schleife, doch kann sie die Gesamtverarbeitungszeit erheblich reduzieren, indem sie den von einem Cache-Miss verursachten Stillstand vermeidet.

Auswirkungen auf die Verarbeitungsgeschwindigkeit hat jedoch nicht nur der Daten-Cache, sondern auch der Befehls-Cache. Den effizientesten Code erhält man, wenn man die innere Schleife eines Algorithmus’ so arrangiert, dass sie in eine einzige Cache-Zeile passt. Die meisten »x86«-CPUs besitzen separate Level-1-Caches für Befehle und Daten, und in älteren Designs sind die Cache-Speicher ähnlich gestaltet. Anders ist es bei neueren CPU-Architekturen: Hier hat man den Befehls-Cache in die CPU hinein verlagert – häufig im Anschluss an die Decodiereinheit.

Die Kapazität von Level-1-Daten-Caches liegt heute meist zwischen 8 KByte und 64 KByte. CPUs von AMD besitzen vorwiegend größere Caches, die dafür in weniger »Ways« gegliedert sind (zwei sind üblich). Dagegen findet man in Intel-CPUs tendenziell kleinere Cache-Speicher mit mehr »Ways« (meist vier). Bei Level-2-Caches gibt es keine Aufteilung in Befehls- und Daten-Caches mehr. Ebenso wie Level-1-Caches verfügen sie vorzugsweise über 64-Byte-Zeilen, während die Zahl der »Ways« in der Regel größer ist. Ihre Kapazität liegt derzeit zwischen 0 KByte (d.h. kein Level-2-Cache vorhanden) und mehreren Megabyte. Nach wie vor selten sind Level-3-Caches. Die »Phenom«-Reihe von AMD verfügt über 2 MByte Level-3-Cache, den sich die vier Cores, die jeweils mit 512 KByte Level-2-Cache ausgestattet sind, teilen.

Mittlerweile können Anwender aus einer immer größer werdenden Palette an Multicore-PC-Prozessoren wählen, wenn ein Algorithmus eine Verarbeitungsleistung auf diesem Niveau benötigt. Die Probleme, die beim Programmieren von derartiger Prozessoren auftreten, ähneln jenen, mit denen man bei der Programmierung von Multicore-DSPs (z.B. dem »ADSPBF561 « aus der »Blackfin«-Serie von Analog Devices) konfrontiert wird.

Mehrere Kerne effizient nutzen

Tatsächlich haben die vorgefundenen Besonderheiten sogar Ähnlichkeit mit jenen beim Design von Mehrprozessor-Systemen. Meist geht es um die Frage, wie sich die anstehenden Algorithmen so auf die verfügbaren Cores verteilen lassen, dass sich die optimale Performance einstellt. Möchte der Entwickler die Aktivitäten der einzelnen Cores eng miteinander verzahnen (z.B. bei der Verteilung einer FFT-Implementierung dergestalt, dass jeder Core eine der zusammengesetzten FFTs implementiert), wird der Datenaustausch zwischen den Cores kritisch. Diese Tatsache ist der Beweggrund für die Einführung des Level-3-Cache, den alle Cores gemeinsam nutzen.

Möchte der Entwickler die Aktivitäten der Cores nicht eng miteinander verzahnen, wird es schwierig, die Last so zu verteilen, dass alle Cores effizient genutzt werden (die Cores verarbeiten dann verschiedene Algorithmen und sind mit großer Wahrscheinlichkeit zu unterschiedlichen Zeiten mit ihrer Arbeit fertig). Häufig gehen Entwickler den Weg, einen Core für Management-Aufgaben zu verwenden und den beziehungsweise die anderen für DSP-Algorithmen zu nutzen. Dies dürfte wahrscheinlich ineffizient sein, jedoch ist das Design hierbei am einfachsten.

Außerdem ist dies ein klassisches Beispiel für den immer wieder erforderlichen Kompromiss zwischen der Entwicklungszeit und der Effizienz zur Laufzeit.

Die Auswahl an Compilern ist groß. Es lohnt sich jedoch, nach jenen Ausschau zu halten, welche die Vektoroperationen des Zielprozessors mit den so genannten »Intrinsics« unterstützen. Hierbei handelt es sich um Funktionen, die 1:1 einem CPU-Befehl entsprechen. Da sie jedoch als Funktionen auftreten (und nicht als Schlüsselwörter), können sie durch eine gleichwertige Implementierung ersetzt werden, wenn die gewählte Ziel-Architektur diesen CPU-Befehl nicht unterstützen sollte.

Listing 1 zeigt als Beispiel C++-Code, der Intrinsics nutzt. Bei »v4sf« handelt es sich um einen Vektortyp. Der Compiler (in diesem Fall ein GNUC++-Compiler) erkennt diesen Typ, wenn das Ziel eine geeignete CPU ist, und interpretiert ihn als einen MMX-Vektor bestehend aus vier Gleitkomma-Werten mit einfacher Genauigkeit. Unterstützt die verwendete CPU die MMX-Operationen jedoch nicht, kann der Typ als Klasse dienen, um die betreffende Funktion nachzubilden. Die Zuweisungsoperationen können somit entweder MMX-Instruktionen oder Klassenmitglied-Funktionen sein. Ebenso ist die Intrinsics-Funktion »__builtin_ia32_mulps« entweder ein einzelner MMX-MULPS-Befehl, der die vier Elemente des Vektors mit jenen eines anderen Vektors multipliziert, oder es handelt sich um eine Funktion, Bild 2: Level-1-Cache eines »Pentium 4« die dasselbe leistet. Dieser Code ist sogar auf einen anderen ANSI-C++-Compiler portierbar, in dem die vom ursprünglichen Compiler verwendeten Intrinsics nicht berücksichtigt sind.


v4sf a4_0;
v4sf a4_1;

v4sf sample = delays4[n];
v4sf coeff_0 = hImpl4[0];
v4sf coeff_1 = hImpl4[1];

a4_0 = __builtin_ia32_mulps(sample, coeff_0);
a4_1 = __builtin_ia32_mulps(sample, coeff_1);

Listing 1: C++-Code, der Intrinsics nutzt


Tools und Bibliotheken

Profiling-Tools sind besonders wichtig, wenn DSP-Software für eine PC-CPU geschrieben wird – speziell solche, die das Cache-Verhalten beobachten können. Ein gutes Beispiel für die Entwicklung auf Linux-Plattformen ist die »Valgrind Tool Suite«, zu der das Tool »Cachgrind« gehört [1]. Obwohl speziell Cachgrind sehr langsam sein kann (die Software wird von einer virtuellen CPU verarbeitet), stellt es ein überaus mächtiges Werkzeug dar.

Zusätzlich gibt es viele Bibliotheken, welche die Entwicklung einer DSP-Implementierung vereinfachen können. Neben den zahlreichen kommerziellen Bibliotheken sind auch eine ganze Reihe kostenloser Libraries erhältlich. Zwar müssen Entwickler dann darauf achten, die Lizenzbedingungen zu erfüllen, doch das Qualitätsniveau der Bibliotheken ist meist sehr hoch. Erstaunlich effizient sind beispielsweise die FFTW-Bibliotheken [2]. Sie sind für die nichtkommerzielle Nutzung im Rahmen der GPL erhältlich, doch gibt es auch alternative Lizenzen, die andere Nutzungsbedingungen zulassen. (rh)

Peter Massam
arbeitet als Senior Technical Consultant bei

Plextek
Telefon 00 44/17 99/53 32 00
www.plextek.co.uk

Links

[1] www.valgrind.org
[2] www.fftw.org

Siehe auch:

DSP-Algorithmen effizient implementieren – Teil 1


  1. PC-Prozessor statt DSP?
  2. Schnelle Evaluierung
  3. Cache-Misses vermeiden
  4. PC-Prozessor statt DSP?
  5. PC-Prozessor statt DSP?
  6. Sind PC-Prozessoren wie der »Pentium« die besseren DSPs? Tatsächlich sind solche CPUs oft mit deutlich höherer Frequenz getaktet als DSPs. Und da sie oft im Gaming-Bereich eingesetzt werden, unterstützen viele von ihnen beispielsweise auch komplexe Vektoroperationen. Doch welche Vor- und Nachteile hat es, PC-Prozessoren als DSPs einzusetzen?

Jetzt kostenfreie Newsletter bestellen!