-
TECHNISCHES GEBIET
-
Ausführungsformen der vorliegenden Erfindung betreffen ein computerimplementierte Verfahren, insbesondere ein computerimplementiertes Verfahren zum Ausführen numerischer Algorithmen auf heterogenen Computerarchitekturen, ein heterogenes Computersystem und ein computerlesbares Medium.
-
HINTERGRUND
-
Insbesondere im Bereich der Numerik und der Datenanalyse besteht ein großer Bedarf zur effizienten Nutzung heterogener Rechenresourcen. Dies schließt die Aufgabe der Parallelisierung von Programmen ein, was sich oftmals als mühsamer, kostenintensiver Prozess erweist und signifikantes Fachwissen über die zugrundeliegenden Funktionsweisen der Computer-Architektur und der damit zusammenhängenden Programmiertechnologien erfordert.
-
Traditionelle Ausgangspunkte sind die bekannten Allzweck-Programmiersprachen (GPL von engl.: general purpose programming language), wie C/C++, C#, FORTRAN, Pascal und Java. Diese Sprachen zielen im allgemeinen auf eine geringe Granularität bei der Datenverarbeitung; sie verarbeiten skalare Daten als grundlegende Datenstrukturen mithilfe allgemeiner Kontrollinstruktionen. Während die Kompilertechnologien für Einzelprozessorsysteme erfolgreich verbessert werden konnten, bleibt die Kompilierung von allgemeinen Programmen (GPL) für heterogene Rechnersysteme noch immer anspruchsvoll.
-
Beispielsweise hat der Compiler (Kompiler) hier bestimmte Entscheidungen zu treffen: welche Recheneinheit ausgewählt, wie die Recheneinheiten zum Bearbeiten der Daten konfiguriert und wie die Daten den Recheneinheiten zur Bearbeitung bereitgestellt werden. Eine Herausforderung besteht in der Diversität der (sich schnell weiterentwickelnden) Computer-Hardware, der Komplexität von aus allgemeinen GPL-Programmiersprachelementen gebildeter Programme und Instruktionsgraphen, die dynamische Natur der zu verarbeitenden Daten und die Zeitpunkte, zu denen alle entscheidenden Informationen letztendlich verfügbar sind. Zudem erfordern Kompiler oft noch erhebliche Entscheidungen seitens des Programmierers. Im Ergebnis wird das parallele Potential vieler Software, z.B. der meisten GPL-basierten Programme, typischerweise nur teilweise genutzt bzw. effizient auf heterogenen Systemen übersetzt. Aufgrund des eingeführten zusätzlichen Aufwands (Overheads) lohnen sich die bisher verwendeten automatisierten Parallelisierungsansätze nur für relativ große Datenmengen bzw. große Codesegmente.
-
Daher besteht ein Bedarf, die Parallelisierung von Softwareprogrammen für heterogene Computerarchitekturen zu verbessern.
-
KURZDARSTELLUNG
-
Gemäß einer Ausführungsform eines computerimplementierten Verfahrens umfasst das Verfahren ein Initialisieren einer ersten Ausführungseinheit eines heterogenen Rechensystems mit einem ersten Rechenkern und einer zweiten Ausführungseinheit des heterogenen Rechensystems mit einem zweiten Rechenkern. Sowohl der erste Rechenkern als auch der zweite Rechenkern sind eingerichtet, eine von einem Programmsegment abgeleitete numerische Operation auszuführen, das eingerichtet ist, eine erste Datenstruktur zu empfangen, die mehrere Elemente eines gemeinsamen Datentyps speichert. Das Programmsegment enthält eine Funktionsmetainformation, Daten aufweisend, die sich auf eine Größe einer Ausgabe der numerischen Operation, eine Struktur der Ausgabe und/oder einen Aufwand zur Erzeugung der Ausgabe beziehen. Die Funktionsmetainformation und eine Datenmetainformation einer Laufzeitinstanz der ersten Datenstruktur werden verwendet, um erste erwartete Kosten für ein Ausführen des ersten Rechenkerns auf der ersten Ausführungseinheit zum Durchführen der numerischen Operation mit der Laufzeitinstanz zu bestimmen, und um zweite erwartete Kosten für ein Ausführen des zweiten Rechenkerns auf der zweiten Ausführungseinheit zum Durchführen der numerischen Operation mit der Laufzeitinstanz zu bestimmen. Die Datenmetainformation enthält eine Laufzeitgrößeninformation der Laufzeitinstanz, eine Laufzeitortinformation der Laufzeitinstanz, eine Laufzeitsynchronisationsinformation der Laufzeitinstanz und/oder eine Laufzeittypinformation der Laufzeitinstanz. Wenn die ersten erwarteten Kosten kleiner oder gleich den zweiten erwarteten Kosten sind, wird zum Durchführen der numerischen Operation mit der Laufzeitinstanz der erste Rechenkern auf der ersten Ausführungseinheit ausgeführt. Wenn die ersten erwarteten Kosten höher sind als die zweiten erwarteten Kosten, wird zum Durchführen der numerischen Operation mit der Laufzeitinstanz der zweite Rechenkern auf der zweiten Ausführungseinheit ausgeführt.
-
Im Kontext der vorliegenden Erfindung soll der Term Ausführungseinheit ein Hardware-Gerät beschreiben, insbesondere einen Prozessor, der in der Lage ist, Operationen und/oder Berechnungen auszuführen, welche normalerweise als Instruktionen in einem gerätespezifischen Rechenkern (eng: compute kernel) gespeichert sind.
-
Die Ausführungseinheit kann ein Einzelkernprozessor (single core CPU / zentrale Ausführungseinheit), ein Mehrkernprozessor (multicore CPU), eine CPU mit parallelen Befehlssatzerweiterungen (wie etwa SSE/AVX), eine GPU (Grafikprozessor, auch: Grafikkarte), eine CPU mit mindestens einem integrierten Grafikprozessor, ein numerischer Koprozessor, SIMD Beschleunigereinheit (single instruction, multiple data accelerator), ein Mikrokontroller, ein Mikroprozessor wie etwa ein DSP (digital signal processor), ein FPGA (field-programmable gate array) oder eine Kombination eines oder mehrerer solcher Einheiten, die typischerweise einen sogenannten virtuellen Prozessor bilden, sein.
-
Im Zusammenhang mit dieser Beschreibung soll der Term „Rechenkern“ eine kompilierte Software beschreiben, die in der Lage ist, spezielle numerische Operationen mit der Laufzeitinstanz einer Datenstruktur, welche in der Lage ist, mehrere Elemente eines gemeinsamen Element-Datentyps zu speichern, zu vollziehen, sobald sie durch eine Ausführungseinheit ausgeführt wird.
-
Die erste Datenstruktur und / oder die Laufzeitinstanz der ersten Datenstruktur kann geeignet sein, eine variable Anzahl an Elementen eines gemeinsamen Datenyps zu speichern, etwa integer (Integer Datentyp), float (Fließkomma Datentyp), complex (komplexer Datentyp, im mathematischen Sinne) usw.
-
Die erste Datenstruktur und / oder die Laufzeitinstanz der ersten Datenstruktur sind typischerweise N-dimensionale rechtwinklige (engl: rectilinear) Datenstrukturen.
-
Die erste Datenstruktur und / oder die Laufzeitinstanz der ersten Datenstruktur können eine Collection (siehe .NET Framework) ein Vektor (engl: vector), ein Feld (im Folgenden wie im Englischen auch als Array bezeichnet), eine Liste, eine Warteschlange (engl: queue), ein Stapel (engl: stack), ein Graph, ein Baum (engl. tree), oder Zeiger (engl. pointer) zu einer solchen Datenstruktur sein. Aus Gründen der Vereinfachung wird im Folgenden davon ausgegangen, dass die Datenstruktur ein multidimensionales Array repräsentiert (ein Vektor, eine Matrix oder höher-dimensionale mathematische Daten) und dass die numerische Operation eine Array-Operation in der detaillierten Beschreibung darstellt, wenn nichts anderes definiert wurde. Im Folgenden werden die Terme rechtwinkliges Array (engl: rectilinear array) und Matrix synonym gebraucht.
-
Typischerweise speichert die Laufzeitinstanz der ersten Datenstruktur Daten, die technische Informationen in verschiedenen Stufen der Verarbeitung über den Ausführungsverlauf eines Algorithmus repräsentieren. Hierbei kann es sich z.B. um Messdaten, Bilddaten, Tondaten, Simulationsdaten, Kontrolldaten, Zustandsdaten usw. handeln, sowie um Zwischenergebnisse.
-
Da die Kosten zur Laufzeit bestimmt werden, und zwar vor der Ausführung einer numerischen Operation und sowohl die Funktionsmetainformationen als auch die Metadaten der Laufzeitinstanz der ersten Datenstruktur berücksichtigt werden, kann die numerische Operation durch jene Ausführungseinheit ausgeführt werden, welche für die zu diesem Zeitpunkt aktuelle Laufzeitkonfiguration am besten hierzu geeignet ist. Verglichen mit anderen Verfahren kann dies mit geringeren Nebenkosten erreicht werden. Im Resultat kann das heterogene Rechensystem somit Datenstrukturen verschiedener Größe (im Mittel) effizienter verarbeiten, als andere Ansätze.
-
Zum Beispiel können die Kosten zur Ausführung der numerischen Operation der Berechnung der Summe der Quadrate aller Elemente einer Matrix A, sum(A^2), als eine Funktion der Größe von A ausgedrückt werden. Wird der Ausdruck mehrfach ausgewertet, so können individuelle Aufrufe individuelle Größen von A assoziieren. Typischerweise, wird die Operation sum(A^2) effizient auf einer GPU ausgeführt, sofern A ausreichend groß ist, insbesondere für Elementtypen, welche auf der GPU unterstützt werden. Wenn A dagegen nur einige wenige Elemente oder nur rein einzelnes Element enthält, wäre die CPU als Ausführungseinheit vielleicht besser geeignet, insbesondere, wenn nur die CPU aber nicht die GPU direkten Zugriff auf die Matrix A zum Zeitpunkt der beabsichtigten Ausführung hat.
-
Daher hängen die zu erwartenden Gesamtkosten der Ausführung üblicherweise von der numerischen Operation, der Größe der Laufzeitinstanz der Datenstruktur, deren Speicherort und den Recheneigenschaften der verfügbaren Ausführungseinheiten (CPU, GPU) ab.
-
Aus diesem Grunde können traditionelle Compiler nicht alle Informationen berücksichtigen: die breite Diversität der zur Laufzeit potentiell verfügbaren Hardware (CPUs, Multikern-CPUs, CPUs mit paralleler Befehlssatzerweiterung, GPUs, FPGAs, Digitale Signalprozessoren (DSP), diverse Speichermodelle), die Komplexität allgemeiner Programmiersprachen und Anweisungsgraphen, die Dynamik der zu verarbeitenden Daten (Datengrößen und Speicherorte ändern sich bei verschiedenen Aufrufen desselben Kodesegmentes). Der Zeitpunkt, zu dem all diese einschränkenden Informationen letztendlich verfügbar werden, macht eine signifikante Laufzeitunterstützung notwendig. Sowohl statische Kompilierung, als auch dynamische Kompilierung zur Laufzeit führen zu großen Nebenkosten (Overhead) zusätzlich zur eigentlichen numerischen Berechnung. Diese lassen das Programm für vergleichsweise kleine oder mittlere Datengrößen, und somit auch für Daten variabler Größe und/oder Gestalt ineffizient (langsam) werden.
-
Ebenso führt das traditionelle Kompilieren (Übersetzen) spezialisierter Sprachen, die in technischen Bereichen genutzt werden (domänenspezifische Sprachen / DSLs) und die Formulierung numerischer Algorithmen basierend auf n-dimensionalen Array Datenstrukturen erlauben, wie Mathwork's MATLAB, ILNumerics und NumPy, tendenziell erst für hinreichend große Eingabedaten zu guten Ausführungsgeschwindigkeiten. Der Aufwand des manuellen Verteilens hinreichend komplexer Programmteile hingegen wird hier auf den Programmierer zur Entwicklungszeit verlegt. Offensichtlich ist dieser Ansatz weder geeignet, sich ohne erneute Anpassung des Programms auf veränderte Hardware Bedingungen einzustellen, noch können heterogene Rechenressourcen effizient für kleine Programmsegmente und für variierende (kleine) Eingabedatengrößen genutzt werden.
-
Im Unterschied dazu erlaubt der hier vorgestellte Ansatz die gewünschten Aktionen (insbesondere das Zusammentragen von Informationen und das Treffen von Entscheidungen) zu den jeweils günstigsten Zeitpunkten zu tätigen: zur Kompilierzeit, zur Startzeit und zur Laufzeit des Programms. Dies ermöglicht die Reduktion von Nebenkosten und eine automatische Anpassung an eine breite Vielfalt an Datengrößen und Hardware Konfigurationen.
-
Entsprechend können kleine Eingabedaten, Eingabedaten mittlerer Größe und große Eingabedaten effizient durch die jeweils am besten geeignete Ausführungseinheit mit geringen Nebenkosten (Overhead), durch eine einfache kompilierte Repräsentation des Programsegmentes verarbeitet werden.
-
Ein durch einen Nutzer generierter Programmcode kann zum Beispiel analysiert werden, um darin Programsegmente mit entsprechenden (bekannten) numerischen Operationen zur Kompilierzeit zu identifizieren. Desweiteren können Funktionsmetainformationen der entsprechenden numerischen Operation bestimmt und ein entsprechendes Laufzeitsegment, inklusive eines entsprechenden ersten Rechenkerns, eines entsprechenden zweiten Rechenkerns und der entsprechenden Funktionsmetainformationen zur Kompilierzeit gebildet werden.
-
Der Programmcode kann mehrere, üblicherweise aufeinanderfolgende Programmsegmente enthalten, die eine Programmsequenz formen.
-
Der in diesem Dokument verwendete Begriff „Sequenz“ soll ein Nutzerprogramm oder einen Bereich des Nutzerprogrammes beschreiben, welcher aus Programmsegmenten besteht.
-
Der in dieser Spezifikation verwendete Begriff „Programmsegment“ soll Softwarekode beschreiben, welcher mit typischerweise abstrakten Instruktionen (Befehlen/Anweisungen) assoziiert ist, welche eine oder mehrere Datenstrukturen, welche in der Lage sind, mehrere Elemente eines gemeinsamen Datentyps zu speichern, als Eingabe und/oder Ausgabeargumente verarbeiten. Das „Programsegment“ kann einfache Sprachelemente, wie etwa Arrayfunktionen, -statements, -operatoren und/oder Kombinationen davon beinhalten. Der Ausdruck ‚sin(A) + linsolve(B + 3*cos(A))‘, mit Matrizen A und B, könnte beispielsweise ein Programmsegment darstellen. Alternativ könnten ‚sin(A)‘ und ‚linsolve(B + 3*cos(A))‘ entsprechende Programmsegmente sein.
-
Der in dieser Spezifikation verwendete Begriff „Laufzeitsegment“ soll eine kompilierte Repräsentation (Ausprägung) eines Programmsegmentes beschreiben. Die kompilierte Ausprägung ist auf einem heterogenen Computersystem ausführbar und enthält üblicherweise entsprechende Rechenkerne für jede Ausführungseinheit des heterogenen Computersystems und Kontrollinstruktionen zum Berechnen der zu erwartenden Gesamtkosten für Laufzeitinstanzen der Datenstruktur(en) und zum Auswählen einer Ausführungseinheit zum Ausführen ihres Rechenkerns und / oder zum Verteilen der Arbeitslast auf zwei oder mehrere Ausführungseinheiten unter Berücksichtigung der zu erwartenden Kosten. Zusätzlich können weitere Informationen bei der Auswahl der Ausführungseinheit und / oder der Verteilung der Arbeitslast berücksichtigt werden, wie die momentane Auslastung der Ausführungseinheiten und / oder die Datenlokalität.
-
Laufzeitsegmente können durch einen Compiler erstellt, zusammengeführt und / oder überarbeitet oder optimiert und zur Laufzeit instanziiert und aktualisiert werden.
-
Funktionsmetadaten werden typischerweise mit dem Laufzeitsegment gespeichert. Oftmals werden Funktionsmetainformationen mit dem Programmsegment gespeichert und können davon abgeleitet sein. Der Compiler kann diese Informationen aktualisieren und pflegen wenn Segmente zusammengeführt, abgeändert und / oder optimiert werden und sobald die konkreten Eigenschaften der Ausführungseinheiten zur Laufzeit bekannt werden.
-
Die numerischen Operationen im Programmkode können einem reduzierten Satz von Recheninstruktionen entsprechen, welche für Datenstrukturen ausgelegt sind, die mehrere Elemente eines gemeinsamen Datentyps speichern können. Hierdurch kann eine effiziente Erstellung und / oder Ausführung von Rechenkernen und / oder Funktionsmetainformationen unterstützt werden.
-
Der Programmkode kann ebenso zwischengelagerte Abschnitte ohne numerische Operationen, die vom Compiler identifiziert werden können, beinhalten. Diese Abschnitte könnten mit traditionellen Methoden gehandhabt werden.
-
Die Programmsegmente könnten zuerst in eine Zwischendarstellung (-repräsentation) übersetzt werden, insbesondere in eine Bytecode-Repräsentation oder in eine entsprechende Repräsentation, welche die Ausführung auf den Ausführungseinheiten unterstützt, etwa durch Nutzung entsprechender Compiler-Infrastrukturen, wie z.B. LLVM (low level virtual machine) oder Roslyn.
-
Später, etwa zur Startzeit auf dem individuellen heterogenen Rechensystem, könnten die Zwischendarstellungen in direkt ausführbare Rechenkerne kompiliert werden, welche Bytecode und / oder Maschineninstruktionen enthalten.
-
Zum Startzeitpunkt können geeignete Ausführungseinheiten des jeweiligen heterogenen Rechensystems ermittelt und die Laufzeitsegmente entsprechend den ermittelten Ausführungseinheiten aktualisiert werden. Dies kann die Aktualisierung der Rechenkerne, der Zwischendarstellung der Rechenkerne und/oder der Funktionsmetainformation(en) beinhalten. Beispielsweise können ein CPU-Kern-Template und ein GPU-Kern-Template entsprechend den Eigenschaften der aktuellen Geräte aktualisiert werden. Insbesondere können numerische Aufwandsfaktoren in den Funktionsmetainformationen und/oder Längen und/oder Typen von Vektordatentypen in der Zwischendarstellung der Rechenkerne zur Ausführung einer bestimmten numerischen Operation auf den eigentlichen Ausführungseinheiten in den Templaten aktualisiert werden
-
Weiterhin können die Ausführungseinheiten mit den jeweiligen ersten Rechnerkernen initialisiert werden.
-
Nach der Initialisierung werden die Funktionsmetainformation und die Datenmetainformation von Laufzeitinstanzen von Datenstrukturen verwendet, um zur Laufzeit die jeweils zu erwartenden Kosten für die Ausführung der Rechenkerne auf den jeweiligen Ausführungseinheiten zu ermitteln.
-
In einer Ausführungsform kann ein verzögerter Ausführungsplan (engl. lazy execution scheme) implementiert werden. Dabei werden potentiell zeitraubende Initialisierungsaufgaben auf den Zeitpunkt verschoben, zu dem das Ergebnis der Initialisierung zum ersten Mal zur Ausführung benötigt wird. Beispielsweise können die Rechenkerne eines bestimmten Laufzeitsegments für eine bestimmte Ausführungseinheit erst dann der Ausführungseinheit zugeordnet und initialisiert werden, wenn für das entsprechende Laufzeitsegment entschieden wurde, den entsprechenden Rechenkern auf der entsprechenden Ausführungseinheit auszuführen.
-
Typischerweise werden bei der Ermittlung der zu erwartenden Kosten sowohl Berechnungskosten als auch Transferkosten (Übertragungskosten, die sich auf mögliche Kosten im Zusammenhang mit einer Übertragung der Laufzeitinstanz auf eine andere Ausführungseinheit beziehen) berücksichtigt.
-
Weiterhin kann eine polynomiale Kostenfunktion zur Ermittlung der zu erwartenden Kosten verwendet werden.
-
Insbesondere hat sich in vielen Fällen eine bi-lineare Kostenfunktion zur Ermittlung der zu erwartenden Kosten als geeignet erwiesen. Viele einfache numerische Operationen haben einen numerischen Aufwand (Rechenkosten), der nur oder im Wesentlichen von der Anzahl der gespeicherten Elemente abhängt, wie z.B. die Berechnung eines Funktionsergebnisses für alle Elemente einzeln. Dementsprechend kann die Anzahl der Elemente einfach mit einem entsprechenden numerischen Aufwandsfaktor multipliziert werden, der die parallelen Rechenmöglichkeiten der Ausführungseinheit darstellt, um die zu erwartenden Rechenkosten zu berechnen. Ebenso können die erwarteten Berechnungskosten durch Multiplikation der Anzahl der zu kopierenden Elemente mit einem Transferfaktor (oder Null, wenn die Ausführungseinheit direkten Zugriff auf die gespeicherten Elemente hat) berechnet und zu den erwarteten Berechnungskosten addiert werden, um die erwarteten (Gesamt-)Kosten für die Durchführung der jeweiligen numerischen Operation unter den aktuellen Umständen auf jeder der geeigneten Ausführungseinheiten zu berechnen.
-
Für komplexere numerische Operationen, wie das Invertieren einer Matrix, das Berechnen von Matrix-Vektor-Produkten, das Lösen eines Gleichungssystems oder das Bestimmen von Eigenwerten und/oder Eigenvektoren einer Matrix (eig), das Berechnen der schnellen Fourier-Transformation (fft) einer Matrix, können die zu erwartenden Rechenkosten auch von der Größe der Ausgabe der numerischen Operation, der Struktur oder Form der Ausgabe oder eines Wertes, der Struktur und/oder der Form der Laufzeitinstanz der Eingabe abhängen. Je nach Eigenschaften der Eingabedaten (spärlich besetzte Matrix vs. dicht besetzte Matrix, einfache vs. doppelte Gleitkommapräzision, Matrixsymmetrie, Größe usw.) können z.B. unterschiedliche numerische Algorithmen verwendet werden. Wie weiter unten näher erläutert wird, können Informationen zur Ausgabe der numerischen Operation durch einen Compiler aus Metadaten des Programmsegments abgeleitet werden.
-
Dieser Ansatz überwindet die Grenzen der bisher verwendeten Ansätze zur Ermittlung der Ausführungskosten.
-
Beispielsweise ist es eine besondere Herausforderung, die Anzahl und Art der ausführbaren Anweisungen in einer Zwischendarstellung oder im ausführbaren Binär-/Objektcode auf einer Plattform/ Ausführungseinheit als Maß für „Kosten“ zu verwenden, da dies den Programmablauf im Programmsegment berücksichtigen muss. Besonders wenn das Segment bedingte Anweisungen enthält (was oft der Fall ist, wenn es sich um Schleifenkonstrukte handelt), ist eine genaue Analyse zumindest mühsam und damit teuer. Wenn der Wert solcher Bedingungen dynamisch von den vom Programmsegment behandelten Eingabedatengrößen abhängt, muss diese Analyse zur Laufzeit durchgeführt werden, wenn solche Datengrößen bekannt sind. Das Problem wird dadurch verschärft, dass die meisten Ansätze versuchen, mit dynamischen Segmentgrößen umzugehen, die durch das Zusammenführen einer unterschiedlichen Anzahl von Befehlen für eine bessere Effizienz gewonnen werden. Jedes zusammengeführte Segment muss erneut mit dem spezifischen Compiler für jede Zielplattform kompiliert werden, der wiederum zur Laufzeit ausgeführt werden muss.
-
Weiterhin, wenn die für die Ausführung eines Programmsegments benötigte Zeit als Maß für die „Kosten“ verwendet wird, ist ein großer Overhead für die Entscheidung, wo der Code ausgeführt werden soll, erforderlich. Darüber hinaus kann eine große Anzahl von Durchläufen mit unterschiedlich großen und/oder geformten Eingangsdaten erforderlich sein, um ein zuverlässiges Kostenmodell abzuleiten. Beispielsweise müssen sowohl die Dimension eines Eingabefeldes und die Länge des Feldes in jeder Dimension als auch der Datentyp der Elemente auf den Ausführungseinheiten variiert werden, um ein zuverlässiges Kostenmodell für die eigentliche Berechnung zu erstellen. Dementsprechend ist dieser Ansatz von Natur aus ineffizient, wenn er dynamisch für kleine Eingabedatengrößen durchgeführt wird.
-
Die Programmsegmente können weiter eingerichtet sein, eine zweite Datenstruktur zu empfangen, die mehrere Elemente des gemeinsamen Datentyps oder eines anderen gemeinsamen Datentyps speichern kann.
-
Ein Programmsegment zur Berechnung eines Matrix-Vektor-Produkts kann beispielsweise eingerichtet sein, eine Matrix und einen Vektor zu empfangen. Ebenso kann ein Programmsegment zum Addieren zweier Matrizen eingerichtet sein, zwei Matrizen zu empfangen.
-
Gemäß einer Ausführungsform eines computerlesbaren Mediums enthält das computerlesbare Medium Anweisungen (Instruktionen), die, wenn sie von einem Computer, eine erste Ausführungseinheit und eine zweite Ausführungseinheit aufweisend, ausgeführt werden, den Computer veranlassen, die hierin beanspruchten Verfahren auszuführen.
-
Der Rechner ist typischerweise ein heterogener Rechner mit zwei (d.h. der ersten und zweiten Ausführungseinheit) verschiedenen Ausführungseinheiten wie z.B. einer CPU und einer GPU oder mehr als zwei verschiedenen Ausführungseinheiten. Typischerweise unterscheiden sich die verschiedenen Ausführungseinheiten in Bezug auf rechnerische Eigenschaften und/oder Fähigkeiten, die sich typischerweise aus unterschiedlichen physikalischen Eigenschaften ergeben. Allerdings können auch zwei verschiedene Ausführungseinheiten die gleichen Berechnungseigenschaften und/oder -fähigkeiten haben, werden aber zum Zeitpunkt der Ausführung eines Programmsegments unterschiedlich genutzt (haben eine unterschiedliche Arbeitsbelastung).
-
Das computerlesbare Medium ist typischerweise ein nicht flüchtiges Speichermedium.
-
Gemäß einer Ausführungsform eines heterogenen Rechensystems (im Folgenden auch als Rechnersystem und Computersystem bezeichnet) enthält das heterogene Rechensystem mindestens zwei verschiedene Ausführungseinheiten und einen Speicher, der ein Laufzeitsegment eines Programmsegments speichert, das eingerichtet ist eine erste Datenstruktur, die mehrere Elemente eines gemeinsamen Datentyps speichern kann, zu empfangen. Das Programmsegment stellt eine Funktionsmetainformation bereit. Die Funktionsmetainformation enthält Daten, die sich auf eine Größe einer Ausgabe einer numerischen Operation des Programmsegments, eine Struktur der Ausgabe und/oder einen numerischen Aufwand zum Erzeugen der Ausgabe beziehen. Jede der mindestens zwei verschiedenen Ausführungseinheiten hat Zugang zu und/oder bildet einen Teil des Speichers, der einen jeweiligen Rechenkern des Laufzeitsegments für jede der mindestens zwei Ausführungseinheiten speichert. Jeder Rechenkern implementiert die numerische Operation. Das Laufzeitsegment enthält oder referenziert auf ausführbaren Code zum Bestimmen einer Datenmetainformation einer Laufzeitinstanz der ersten Datenstruktur. Die Datenmetainformation enthält eine Laufzeitgrößeninformation der Laufzeitinstanz, eine Laufzeitortinformation der Laufzeitinstanz, eine Laufzeitsynchronisationsinformation der Laufzeitinstanz und/oder eine Laufzeittypinformation der Laufzeitinstanz. Das Laufzeitsegment enthält weiter ausführbaren Code, der eingerichtet ist, die Funktionsmetainformation(en) und die Datenmetainformation(en) zum Berechnen jeweiliger erwarteter Kosten des Ausführens der jeweiligen Rechenkerne zum Ausführen der numerischen Operation mit der Laufzeitinstanz für die mindestens zwei Ausführungseinheiten zu verwenden. Weiter enthält oder referenziert das Laufzeitsegment (auf) ausführbaren Code zum Auswählen einer der mindestens zwei verschiedenen Ausführungseinheiten zum Ausführen des jeweiligen Rechnerkerns, sodass die bestimmten erwarteten Kosten der ausgewählten der zwei verschiedenen Ausführungseinheiten einem niedrigsten Wert der bestimmten erwarteten Kosten entsprechen.
-
Eine Sequenz von Laufzeitsegmenten kann im Speicher abgelegt werden.
-
Der ausführbare Code zur Berechnung der zu erwartenden Kosten und der ausführbare Code zur Auswahl der mindestens zwei verschiedenen Ausführungseinheiten können in einen Kontrollkern des Laufzeitsegments enthalten sein.
-
Typischerweise umfasst das heterogene Rechensystem einen mit der ersten Ausführungseinheit und der zweiten Ausführungseinheit gekoppelten Basisprozessor. Der Basisprozessor ist typischerweise so konfiguriert, dass er Steuerungsfunktionen ausführt, insbesondere Ausführungseinheiten des heterogenen Rechensystems ermittelt, die für die Durchführung der numerischen Operation geeignet sind, die jeweiligen Rechenkerne in die Ausführungseinheiten lädt, die erwarteten Kosten ermittelt, die (gewünschte) Ausführungseinheit anhand der ermittelten erwarteten Kosten auswählt und/oder die Ausführung des jeweiligen Kerns auf der ausgewählten Ausführungseinheit einleitet.
-
Beispielsweise kann der Basisprozessor einen Steuerkern hosten und ausführen. Im Folgenden wird der Basisprozessor auch als Steuerprozessor bezeichnet. In anderen Ausführungen können eine, mehrere oder sogar alle Steuerungsfunktionen von einer der Ausführungseinheiten ausgeführt werden. Die Steuerungsfunktionen können aber auch auf die Ausführungseinheit und den Basisprozessor verteilt werden.
-
Außerdem kann der Basisprozessor als eine der Ausführungseinheiten fungieren bzw. eine der Ausführungseinheiten sein, auch wenn die Rechenleistung im Vergleich zu den (anderen) Ausführungseinheiten des heterogenen Rechensystems gering ist. Beispielsweise kann der Basisprozessor die numerische Operation durchführen, wenn die Anzahl der in der Laufzeitinstanz der (ersten) Datenstruktur gespeicherten Elemente vergleichsweise gering ist.
-
Jede der Ausführungseinheiten kann aus einer Einzelkern-CPU (zentrale Ausführungseinheit), einer Multikern-CPU, einer CPU mit paralleler Befehlssatzerweiterung (wie SSE/AVX), einer GPU (Grafik-Ausführungseinheit), einer CPU mit mindestens einer GPU, einem Coprozessor für numerische Berechnungen, einem SIMD-Beschleuniger (von engl. single instruction multiple data accelerator), einem Mikroprozessor, einem Mikrocontroller wie einem DSP (Digitaler Signalprozessor) und einem FPGA (Field-programmable Gate Array) oder durch eine Kombination aus einem oder mehreren dieser Geräte, die typischerweise einen sogenannten virtuellen Prozessor bilden, bestehen.
-
Das heterogene Rechensystem ist typischerweise ein heterogener Rechner (Computer) mit einem Hostcontroller, der einen Hostprozessor hat, der typischerweise den Basisprozessor bildet.
-
In einem Beispiel umfasst der heterogene Rechner weiterhin eine CPU und eine oder mehrere GPUs, die jeweils eine Ausführungseinheit bilden.
-
Das heterogene Rechensystem kann auch ein Arbeitsplatzrechner sein, d.h. ein Computer, der speziell für technische, medizinische und/oder wissenschaftliche Anwendungen und/oder für numerische und/oder array-intensive Berechnungen entwickelt wurde. Der Arbeitsplatzrechner kann beispielsweise zur Analyse von Messdaten, simulierten Daten und/oder Bildern, insbesondere von Simulationsergebnissen, wissenschaftlichen Daten, wissenschaftlichen Bildern und/oder medizinischen Bildern verwendet werden.
-
Das heterogene Rechensystem kann aber auch als Grid- oder Cloud-Computing-System mit mehreren miteinander verbundenen Knoten als Ausführungseinheit oder sogar als Embedded-Computing-System realisiert werden.
-
Der Fachmann erkennt beim Lesen der folgenden detaillierten Beschreibung und beim Betrachten der beiliegenden Zeichnungen weitere Merkmale und Vorteile.
-
Figurenliste
-
Die Komponenten in den Figuren sind nicht unbedingt maßstabsgerecht; stattdessen wurde Wert auf die Veranschaulichung der Prinzipien der Erfindung gelegt. Darüber hinaus bezeichnen in den Figuren gleiche Bezugszahlen entsprechende Teile. In den Zeichnungen ist Folgendes dargestellt:
- 1 veranschaulicht ein computerimplementiertes Verfahren gemäß einer Ausführungsform;
- 2 veranschaulicht ein computerimplementiertes Verfahren gemäß einer Ausführungsform;
- 3 veranschaulicht ein computerimplementiertes Verfahren gemäß einer Ausführungsform;
- 4 veranschaulicht Verfahrensschritte eines computerimplementierten Verfahrens und ein heterogenes Rechensystem gemäß einer Ausführungsform;
- 5 veranschaulicht Verfahrensschritte eines computerimplementierten Verfahrens gemäß einer Ausführungsform;
- 6 veranschaulicht Verfahrensschritte eines computerimplementierten Verfahrens gemäß einer Ausführungsform;
- 7 veranschaulicht Verfahrensschritte eines computerimplementierten Verfahrens gemäß einer Ausführungsform;
- 8 veranschaulicht Verfahrensschritte eines computerimplementierten Verfahrens gemäß einer Ausführungsform;
- 9 veranschaulicht Verfahrensschritte eines computerimplementierten Verfahrens gemäß einer Ausführungsform; und
- 10 veranschaulicht Verfahrensschritte eines computerimplementierten Verfahrens gemäß einer Ausführungsform
-
DETAILLIERTE BESCHREIBUNG
-
In der folgenden detaillierten Beschreibung wird auf die beiliegenden Zeichnungen Bezug genommen, die einen Teil der vorliegenden Schrift bilden und in denen zum Zweck der Veranschaulichung konkrete Ausführungsformen gezeigt sind, wie die Erfindung praktiziert werden kann. In diesem Zusammenhang werden Richtungsbegriffe wie zum Beispiel „oben“, „unten“, „Vorderseite“, „Rückseite“, „vorn“, „hinten“ usw. bezüglich der Ausrichtung der beschriebenen Figuren verwendet. Weil die Komponenten von Ausführungsformen in einer Reihe unterschiedlicher Ausrichtungen positioniert werden können, dienen die Richtungsbegriffe lediglich der Veranschaulichung und sind in keiner Weise einschränkend. Es versteht sich, dass andere Ausführungsformen verwendet werden können und dass strukturelle oder logische Änderungen vorgenommen werden können, ohne den Schutzumfang der vorliegenden Erfindung zu verlassen. Die folgende detaillierte Beschreibung ist darum nicht in einem einschränkenden Sinne zu verstehen, und der Schutzumfang der vorliegenden Erfindung ist durch die beiliegenden Ansprüche definiert.
-
Wir wenden uns nun ausführlich verschiedenen Ausführungsformen zu, von denen ein oder mehrere Beispiele in den Figuren veranschaulicht sind. Jedes Beispiel dient lediglich der Erläuterung und soll die Erfindung nicht einschränken. Zum Beispiel können Merkmale, die als Teil einer Ausführungsform veranschaulicht oder beschrieben sind, in - oder in Verbindung mit - anderen Ausführungsformen verwendet werden, um eine weitere Ausführungsform zu erhalten. Es ist beabsichtigt, dass die vorliegende Erfindung solche Modifizierungen und Variationen enthält. Die Beispiele werden unter Verwendung bestimmter Formulierungen beschrieben, die nicht so verstanden werden dürfen, als würden sie den Schutzumfang der beiliegenden Ansprüche einschränken. Die Zeichnungen sind nicht nach Maßstab angefertigt und dienen lediglich veranschaulichenden Zwecken. Zur besseren Klarheit wurden - wenn nicht anders angegeben - die gleichen Elemente oder Herstellungsschritte mit den gleichen Bezugszahlen versehen in den verschiedenen Zeichnungen.
-
Mit Bezug zur 1 werden Verfahrensschritte eines computerimplementierten Verfahrens 1000 erläutert.
-
In einem Block 1300 einer Startphase II werden Ausführungseinheiten eines heterogenen Rechensystems mit entsprechenden Rechenkernen eines Laufzeitsegments initialisiert, das aus einem (übergeordneten) Programmsegment kompiliert wurde, das eingerichtet ist, eine erste Datenstruktur zu erhalten, die mehrere Elemente eines gemeinsamen Datentyps speichern kann. Dies kann das Kopieren des Rechenkerns in einen Speicher der jeweiligen Ausführungseinheit oder einen geteilten Speicher des heterogenen Rechensystems und/oder die Zuweisung des Rechenkerns auf die jeweilige Ausführungseinheit beinhalten.
-
Jeder Rechenkern kann so konfiguriert sein, dass er eine vom Programmsegment abgeleitete numerische Operation ausführt.
-
Das Laufzeitsegment enthält oder hat Zugriff auf eine Funktionsmetainformation mit Daten, die sich auf die Größe einer Ausgabe der numerischen Operation, eine Struktur der Ausgabe und/oder einen Aufwand zur Erzeugung der Ausgabe beziehen.
-
In einem nachfolgenden Block 1500 einer Laufzeitphase III werden die Funktionsmetainformationen und eine Datenmetainformation einer Laufzeitinstanz der ersten Datenstruktur verwendet, um die jeweils zu erwartenden Kosten für die Ausführung der Rechenkerne auf den Ausführungseinheiten zu ermitteln. Die Datenmetainformation kann eine Laufzeitgrößeninformation der Laufzeitinstanz, eine Laufzeitspeicherortinformation der Laufzeitinstanz und/oder eine Laufzeittypinformation der Laufzeitinstanz enthalten.
-
Zu diesem Zweck kann auf eine Laufzeitinstanz der Datenstruktur, die in einem Speicher einer der Ausführungseinheiten oder einem gemeinsamen Speicher des heterogenen Rechensystems abgelegt ist, über den Steuercode des Laufzeitsegments zugegriffen werden.
-
In einem nachfolgenden Block 1600 der Laufzeitphase III kann anhand der zu erwartenden Kosten entschieden werden, auf welcher der Ausführungseinheiten der jeweilige Rechenkern ausgeführt wird, um die numerische Operation mit der Laufzeitinstanz durchzuführen.
-
Danach kann die Laufzeitinstanz in die zur Ausführung ausgewählte Ausführungseinheit kopiert werden, wenn die ausgewählte Ausführungseinheit noch keinen Zugriff auf die Laufzeitinstanz hat.
-
Danach kann der Rechnerkern auf der ausgewählten Ausführungseinheit ausgeführt werden.
-
Mit Bezug zur 2 werden Verfahrensschritte eines computerimplementierten Verfahrens 2000 erläutert. Das Verfahren 2000 ist ähnlich zum oben mit Bezug zur erläuterten 1 erläuterten Verfahren 1000.
-
Wie in in 2 dargestellt wird, kann eine der Startphase II vorausgehende Kompilierungsphase I in einem Block 2100 zum Identifizieren von Programmsegmenten in einem Programmkode und zum Kompilieren von Laufzeitsegmenten für die identifizierten Programmsegmente genutzt werden.
-
Weiterhin werden Ausführungseinheiten, die für die Durchführung der aus den identifizierten Programmsegmenten abgeleiteten numerischen Operation geeignet sind, typischerweise in einem Block 2200 der Startphase II des Verfahrens 2000 ermittelt.
-
In einem nachfolgenden Block 2300 der Startphase II werden die ermittelten Ausführungseinheiten mit entsprechenden Rechenkernen der Laufzeitsegmente initialisiert.
-
In einem nachfolgenden Block 2400 der Laufzeitphase III wird die Datenmetainformation(en) einer Laufzeitinstanz der ersten Datenstruktur ermittelt. Dies kann das Bestimmen einer Dimension der ersten Datenstruktur, das Bestimmen einer Anzahl gespeicherter Elemente in jeder der Dimensionen der ersten Datenstruktur, das Bestimmen einer Anzahl gespeicherter Elemente in der ersten Datenstruktur, das Bestimmen des Datentyps der Elemente, das Bestimmen einer Typinformation der Laufzeitinstanz und/oder das Bestimmen einer Laufzeitortinformation der Laufzeitinstanz beinhalten.
-
Letzteres kann erleichtert werden, wenn die Laufzeitinformationen innerhalb der Laufzeitinstanz der ersten Datenstruktur gespeichert werden.
-
In Ausführungsformen, in denen die numerische Operation zwei oder mehr Datenstrukturen als Eingaben (Input) nutzt, können die Datenmetainformationen für alle Laufzeitinstanzen in Block 2400 bestimmt werden.
-
In einem nachfolgenden Block 2500, werden die Funktionsmetainformation(en) und die Datenmetainformation(en) der Laufzeitinstanz(en) genutzt, um jeweilige erwartete Kosten des Ausführens der Rechenkerne auf den Ausführungseinheiten zu bestimmen.
-
In einem nachfolgenden Block 2600 der Laufzeitphase III, können die erwarteten Kosten dazu genutzt werden, eine Ausführungseinheit zum Ausführen des jeweiligen Kerns zum Ausführen der numerischen Operation mit der Laufzeitinstanz(en) auszuwählen.
-
Danach, kann in einem Block 2700 der Rechenkern auf der ausgewählten Ausführungseinheit ausgeführt werden.
-
Wie in 2 durch den gestrichelten Pfeil angezeigt wird, können die Blöcke 2400-2700 in einer Schleife durchlaufen werden, wenn mehrere Laufzeitsegmente auszuführen sind.
-
Optional können Out-of-Order-Ausführungspläne implementiert werden.
-
Mit Bezug zur 3 werden Verfahrensschritte eines computerimplementierten Verfahrens 3000 erläutert. Das Verfahren 3000 ist ähnlich zum oben mit Bezug zur 2 erläuterten Verfahren 2000.
-
Das Verfahren 3000 beginnt mit einem Block 3110 zum Identifizieren einer Sequenz mit Programmsegmenten in einem Programmkode. Zu diesem Zweck kann ein Parser verwendet werden, um nach numerischen Operationen im Programmkode zu suchen.
-
In einem nachfolgenden Block 3150 können mehrere nachfolgende Laufzeitsegmente erzeugt werden. Jedes Laufzeitsegment kann so konfiguriert werden, dass es ein oder mehrere Datenstrukturen empfängt, die mehrere Elemente eines jeweiligen gemeinsamen Datentyps speichern, die gleich oder verschieden sein können. Weiterhin kann jedes Laufzeitsegment eine entsprechende Funktionsmetainformation enthalten, die Daten zu einer Größe von einer oder mehreren Ausgaben der numerischen Operation, einer Struktur oder Form der Ausgabe(n) und/oder einem Aufwand zur Erzeugung der Ausgabe(n) enthält.
-
In einem nachfolgenden Block 3170 der Kompilierungsphase I, kann jedes Programmsegment in eine Zwischenrepräsentation übersetzt werden. Dies kann mittels einer Compiler-Infrastruktur erfolgen.
-
Jedes Laufzeitsegment kann die jeweilige Zwischendarstellung und entsprechende Funktionsmetainformation(en) enthalten, die sich auf eine jeweilige numerische Operation beziehen, die mit der/den Laufzeitinstanz(en) einer oder mehrerer Datenstrukturen durchzuführen ist.
-
Die Zwischendarstellung kann auch aus Metadaten erstellt werden, die (zusammen) mit der Sequenz oder den entsprechenden Programmsegmenten gespeichert sind oder sich auf diese beziehen.
-
In einem nachfolgenden Block 3200 einer Startphase II des Verfahrens 3000 werden typischerweise Ausführungseinheiten ermittelt, die zur Durchführung der aus den identifizierten Programmsegmenten abgeleiteten numerischen Operation geeignet sind.
-
In einem nachfolgenden Block 3270 der Startphase II können die Zwischendarstellungen in entsprechende Maschinensprachendarstellungen (Rechenkerne) übersetzt werden. Dies kann bspw. durch einen Just-in-time-Compiler erreicht werden, der auf die vorhandenen jeweiligen Ausführungseinheiten spezialisiert sein kann (oder auch nicht) und eine bessere Anpassung der Maschinensprache und/oder der Zwischendarstellung an die Eigenschaften der vorhandenen Ausführungseinheiten ermöglichen kann.
-
In einem nachfolgenden Block 3300 der Startphase II, werden die bestimmten Ausführungseinheiten mit den im Block 3270 gebildeten jeweiligen Rechenkernen initialisiert.
-
In einem nachfolgenden Block 3400 der Laufzeitphase III, kann eine Datenmetainformation einer Laufzeitinstanz von Datenstrukturen bestimmt werden.
-
In einem nachfolgenden Block 3500 können die Funktionsmetainformation(en) und die Datenmetainformation(en) der Laufzeitinstanz(en) zur Ermittlung der jeweils zu erwartenden Kosten für die Ausführung der Kerne auf den Ausführungseinheiten verwendet werden..
-
In einem nachfolgenden Block 3600 der Laufzeitphase III können die erwarteten Kosten verwendet werden, um eine Ausführungseinheit für die Ausführung des jeweiligen Kernels zur Durchführung der numerischen Operation mit der Laufzeitinstanz(en) auszuwählen.
-
Danach kann in einem Block 3700 auf der ausgewählten Ausführungseinheit der jeweilige Rechenkern ausgeführt werden.
-
Der gestrichelte Pfeil in 3 zeigt an, dass die Blöcke 3400 bis 3700 als Schleife durchlaufen werden können, wenn mehrere Laufzeitsegmente auszuführen sind.
-
In einer anderen Ausführungsform wird der Block 3600 durch einen Block ersetzt, in dem die zu erwartenden Kosten dazu verwendet werden, die Arbeitslast der Ausführung des Rechenkerns auf mindestens zwei Ausführungseinheiten zu verteilen, die in einem Ersatzblock für Block 3700 die jeweiligen numerischen Operationen für entsprechende Anteile der Laufzeitinstanz durchführen können.
-
4 zeigt exemplarische Komponenten eines heterogenen Rechensystems, das als heterogener Rechner (Computer) 70 implementiert ist, und typische Prozesse zur Ausführung einer Sequenz von Laufzeitsegmenten in einer Laufzeitphase III eines computerimplementierten Verfahrens 4000. Typischerweise gehen, ähnlich wie oben mit Bezug zu den 1 bis 3 erläutert wurde, der Laufzeitphase III des Verfahrens 4000 eine Kompilierungsphase und eine Startphase voraus.
-
In dem exemplarischen Ausführungsbeispiel ist der heterogene Rechner 70 mit einer CPU 71, die auch einen Haupt- bzw. Hostprozessor eines Hostcontrollers 73 bildet und einer GPU 72 ausgerüstet.
-
Ein komplexes Nutzerprogramm wird in Sequenzen zerteilt, die in Segmente segmentiert werden. Die Segmente können in der aus der Sequenz abgeleiteten Reihenfolge fortlaufend aneinandergereiht werden.
-
In 4 sind Bezugszeichen nur für die aufeinanderfolgenden Laufzeitsegmente 30 bis 34 vorgesehen, die einen kontinuierlichen Teil der ursprünglichen Sequenz darstellen.
-
In der Startphase (wenn das Programm startet) wurde jede Ausführungseinheit 71, 72, 73, die eine Sequenz verarbeiten kann, für die Behandlung aller Laufzeitsegmente 30 bis 34 der Sequenz initialisiert. Im Folgenden werden die Ausführungseinheiten auch als Geräte (Gerät 3, Gerät 2 bzw. Gerät 1) bezeichnet.
-
Daher werden auf jedem Gerät jeweilige Rechenkerne entsprechend den Laufzeitsegmenten 30-34 der Sequenz kompiliert und zugeordnet. Die Rechenkerne 140 bis 144 wurden für die CPU 71 kompiliert und zugewiesen. Die kompilierten Kerne 140 bis 144 bilden eine ausführbare Repräsentation 111 der ursprünglichen Sequenz auf der CPU.
-
Genauso wurden die Kernel 150 bis 154 der gleichen Laufzeitsegmente für die GPU 72 kompiliert sowie der GPU 72 zugewiesen und bilden eine ausführbare Repräsentation 112 der ursprünglichen Sequenz auf der GPU 72.
-
Zusätzlich können für jedes Laufzeitsegment skalare Rechenkerne 130 bis 134 vorbereitet werden, die eine skalare Version des Codes der Segmente darstellen. Die skalaren Rechenkerne 130-134 laufen meist auf der CPU und verwenden die gleiche Hostsprache, in der das Programm bereitgestellt wurde. Die skalaren Codekerne 130 bis 134 bilden eine Repräsentation 100 der ursprünglichen Sequenz auf der CPU, sind aber angepasst, typischerweise optimiert für skalare Eingangsdaten und/oder sequentielle Verarbeitung von - typischerweise relativ kleinen - Instanzen von Eingangsdatenstrukturen.
-
Jedes der Laufzeitsegmente 30-34 kann einen Verweis auf die entsprechenden kompilierten und zugeordneten Rechenkerne 130 -154 - jeweils mindestens einen für jedes der ermittelten Geräte - speichern, mit dem seine Ausführung jederzeit ausgelöst werden kann.
-
Am Ende der Startphase können alle Segmente einer Sequenz eine Referenz auf eine ausführbare Version der Programmsegmente für jedes unterstützte Gerät speichern.
-
In der beispielhaften Ausführungsform kann jedes Segment seinen Code auf drei verschiedene Arten mit sehr geringem Overhead ausführen: eine Version für skalare Daten (für Gerät 1 bzw. den Host-Controller 73), eine Array-Version für den Betrieb auf der CPU (71 und Gerät 2) und eine Version für den Betrieb auf der GPU (72 und Gerät 3).
-
Jedes Segment 30 - 31 kann einen Steuercode 50 implementieren, mit dem zur Laufzeit der Ausführungspfad umgeschaltet wird, um einen der vorbereiteten Ausführungspfade auszuwählen. Der Ausführungspfad ist in 4 als dicker gestrichelter Pfeil dargestellt.
-
Die Entscheidung, welcher Pfad für die Ausführung verwendet werden soll, basiert auf mehreren Faktoren, wie der Datengröße, Datenform und Lokalität der Daten (Datenlokalität), den Berechnungskosten der einzelnen Ausführungspfade der Segmente, den Eigenschaften der Geräte, der aktuellen Auslastung der Geräte, Heuristiken oder Messdaten über den mit jedem Gerät verbundenen zusätzlichen Overhead, Heuristiken über feste oder dynamische Schwellenwerte, die mit Datengrößen verbunden sind. Die Entscheidung, welcher Pfad verwendet werden soll, kann weiterhin von vorherigen und/oder aufeinanderfolgenden Segmenten und der Konfiguration der Datenlokalität abhängen, welche der kürzesten Ausführungszeit für diese Segmente entspricht. Zugehörige Informationen können auch durch historische Nutzungsdaten des Segments und/oder durch vorausschauende Analysen (eng.: speculative processing) und/oder Bewertungen gewonnen werden.
-
Wie oben beschrieben, kann der beste Ausführungspfad derjenige sein, der die Ausführung des Segmentcodes am frühesten beendet oder der während der Ausführung die geringste Energie verbraucht. Dies wird im Folgenden näher erläutert.
-
Wenn die Ausführung der Sequenz durch die Segmente in Richtung der Zeit 42 verläuft, kann der Ausführungspfad der Sequenz schnell zwischen den vorbereiteten Geräten wechseln.
-
Jedem Segment sind zur Durchführung allfällige Input-Array-Argumente 10 zur Verfügung zu stellen.
-
Jedes Array-Objekt (Laufzeitinstanz) A kann die Orte (Gerätespeicher) verfolgen, in die seine Daten kopiert wurden. Wenn das Array A zum ersten Mal auf einem Gerät 71, 72 verwendet wurde, werden dessen Daten typischerweise in den Gerätespeicher kopiert und eine Referenz 20 - 22 auf das Array A wird typischerweise mit dem Array A gespeichert.
-
Wenn ein Array als Ergebnisspeicher für ein Segment auf einem Gerät verwendet wird, wird dieses Gerät im Array als das einzige Gerät markiert, auf dem das Array momentan gespeichert ist. Alle anderen Gerätepositionen, auf die das Array kopiert wurde, werden aus einer Liste von Arraypositionen des Arrays gelöscht.
-
Mit Bezug zur 5 werden weitere Prozesse einer Kompilierungsphase I erläutert, die in jedem der oben mit Bezug zu den 2 und 3 erläuterten Verfahren verwendet werden können.
-
In einem ersten Prozess wird ein Anwenderprogramm 200 analysiert. Es werden Sequenzen des Programms 200 identifiziert, die eine Ermittlung der Ausführungskosten zur Laufzeit ermöglichen. Aus Gründen der Übersichtlichkeit hat das beispielhafte Anwenderprogramm nur eine Sequenz 210.
-
Sequenzen können einfache Anweisungen zum Umgang mit skalaren Daten 205 enthalten oder sogar daraus bestehen. In diesem Fall kann ein Compiler Kosteninformationen ableiten, die ausschließlich auf dem für die Anweisung generierten ausführbaren Code basieren.
-
Alternativ oder zusätzlich kann die Sequenz 210 komplexere Befehle enthalten, die möglicherweise komplexere numerische Operationen behandeln, und/oder eine oder mehrere Datenstrukturen 10 enthalten, die mehrere Elemente des jeweiligen gemeinsamen Datentyps speichern können.
-
Solche Datenstrukturen können beliebige vordefinierte Array-Datentypen sein, die den Benutzern der Sprache, in der das Programm geschrieben wurde, zur Verfügung stehen. Alternativ kann die Datenstruktur aus einer Bibliothek oder einem eigenständigen Modul oder einem externen Modul (dynamic link library/statically linked library) stammen, das in das Programm importiert wird. Solche Datenstrukturen sind typischerweise in der Lage, mehrere Elemente des gleichen oder ähnlichen Datentyps und begleitende Informationen über die Datenstruktur zu speichern, wie die Anzahl der Elemente, die Anzahl und Länge der einzelnen Dimensionen in der Datenstruktur (als „Form“ für Arrays und rechtwinklige Arrays), die Art der Elemente für solche Sprachen, die Typen und Ortsinformationen (Speicherort(e)) unterstützen.
-
Weiterhin kann die Datenstruktur 10 eine allgemeinere Datenstruktur kapseln, um notwendige Informationen über die allgemeine Datenstruktur hinzuzufügen und als gemeinsame Datenstruktur 10 zur Verfügung zu stellen, wenn die allgemeine Datenstruktur diese Informationen nicht natürlich bereitstellt.
-
Mindestens ein Teil des Anwenderprogramms 200 besteht aus Anweisungen, die numerische Operationen auf Datenstruktur(en) 10 ausführen. Solche Anweisungen sind dem Compiler entweder bekannt oder bieten eine Möglichkeit, vom Compiler automatisch erkannt zu werden
-
Ein Beispiel für solche Anweisungen ist der Funktionsumfang in technischen Sprachen wie Matlab (The MathWorks Inc.), numpy (numpy.org), ILNumerics (ILNumerics GmbH), Julia (julialang.org), Fortran, Scilab (scilab.org), Octave, FreeMat (freemat.sf.net) und Mathematica (Wolfram Research), die mit Array-Daten arbeiten. Solche Sprachen können ihre eigene Compiler- und Entwicklungsinfrastruktur bereitstellen. Oder die Sprache kann als domänenspezifische Spracherweiterung (DSL) einer anderen, typischerweise allgemeineren Sprache (GPL) realisiert werden, die den Compiler und die Entwicklungswerkzeuge der GPL vollständig oder teilweise ausnutzt. Hier kann die Datenstruktur 10 als rechtwinkliges Array beliebiger Größe und Dimensionalität realisiert werden. Einige dieser Funktionen nehmen ein oder mehrere Arrays von Elementen (mehrwertige Eingabeparameter) und führen ihre Operation mit vielen oder sogar allen Elementen der Parameter durch, als Map (Ausführen einer Funktion auf den Elementen), Sinus (Berechnen des Sinus der Elemente), Add (Hinzufügen von Elementen zweier Arrays). Andere Funktionen können ein oder mehrere neue Array-Daten basierend auf null oder mehr Array- oder skalaren Eingabeparametern erzeugen, wie z.B. ‚zero‘ (Erstellen eines Arrays von 0-wertigen Elementen), ‚eins‘ (Erstellen von Arrays von 1-wertigen Elementen), ‚rand‘ (Erstellen von Arrays von zufällig bewerteten Elementen), ‚clone‘ (Erstellen einer Kopie eines anderen Arrays). Andere typische Funktionen führen die Aggregation durch, wie z.B. „reduce“ (Ausführen einer Aggregationsfunktion für eine Dimension eines Arrays), „sum“ (Summieren der Elemente entlang einer bestimmten Dimension), „max“ (Berechnen des Maximalwerts entlang einer Dimension). Andere Funktionen können Subarrays aus Datenstrukturen erzeugen oder Verkettungen durchführen, wie z.B. ‚vertcat‘ und ‚concat‘ (Verkettung von Arrays entlang einer bestimmten Dimension). Weiter, noch komplexere Operationen können genutzt werden,z.B. ‚matmult‘ (Matrixmultiplikation), ‚linsolve‘ (Lösung linearer Gleichungssysteme), ‚svd‘ (Singulärwertzerlegung), ‚qr‘ (QR-Matrixzerlegung), ‚fft‘ (Durchführung einer schnellen Fourier-Transformation), eig' (Eigenwert / Eigenvektorzerlegung), ‚interpolate‘ (Interpolation neuer Werte nach bekannten diskreten Werten), ‚fmin‘ (Finden des Minimums einer Funktion).
-
Funktionen können zudem Zusatzparameter verwenden, z.B. zur Steuerung interner Vorgänge. Ein Beispiel ist die Funktion ‚sum(A,1)‘, wobei ‚1‘ ein zusätzlicher skalarer Parameter ist, der den Index der Dimension des Eingabefeldes A bestimmt, um die enthaltenen Elemente zu summieren. Einige Funktionen erzeugen ein einziges Ausgabefeld des gleichen Typs und der gleichen Größe wie das/die Eingabefeld(er). Andere Funktionen können mehrere Ausgaben erzeugen, einige der Ausgabeargumente können sich in Größe und Typ unterscheiden, z.B. die Broadcast-Binärfunktion ‚add‘, ‚find‘ (Auffinden von Elementen ungleich 0), ‚max(A,1,l)‘ (Auffinden von Elementen mit dem Maximalwert entlang der Dimension ‚1‘ in A und auch die Indizes solcher Elemente). Andere Funktionen können neue Arrays von Grund auf oder durch Extrahieren von Teilen und/oder Modifizieren von Teilen eines oder mehrerer Eingabefelder erzeugen. Ein Beispiel für letztere Funktionskategorie ist ein modifizierender (linker) Subarray-Operatorausdruck wie A[2;:] = B, wobei die Werte von B den entsprechenden Elementen von A zugeordnet werden, die entlang der Zeile mit dem Index ‚2‘ gespeichert sind.
-
Ein weiteres Beispiel für identifizierbare Anweisungen im Anwenderprogramm 200 ist der Satz von Funktionen, die ähnlich als Laufzeitergänzung für Sprachen wie FORTRAN, C/C++, Perl, C#, Visual Basic, F#, Python, Javascript bereitgestellt werden. Ein solcher beispielhafter Satz von Funktionen wird durch die statischen Funktionselemente der .NET CLI-Klasse System.Math beschrieben. Er enthält, ist aber nicht darauf beschränkt, den Funktionsumfang, dessen Namen mit Abs, Acos, Asin, Atan, Atan2, Ceiling, Cos, Cosh beginnen und mit Sin, Sinh, Sqrt, Tan, Tanh, Truncate enden. Aufgrund des skalaren Charakters solcher Funktionen kann der Compiler der beschriebenen Methode gemeinsame Sprachschleifenkonstruktmuster im Benutzercode (for-, while-, goto-Schleifen) erkennen, um Iterationen über Elemente von Arrays in einzelne Array-Funktionsaufrufe umzuwandeln, die entsprechend der beschriebenen Methode in Programmsegmente verarbeitet werden können.
-
Eine Ausführungsform des offenbarten Verfahrens kann in einer dynamisch typisierten Sprache (z.B. Duck Typing) wie Python, Julia oder MATLAB implementiert werden. Es ist jedoch zu erwarten, dass es mit einer Implementierung, die eine statisch typisierte Sprache wie C# oder Java verwendet, eine bessere Laufzeit-Performance erreichbar ist. Sowohl Ausführungsformen, die in einer statisch typisierten Sprache implementiert sind, als auch solche, die in einer dynamisch typisierten Sprache implementiert sind, gelten für beide Kategorien von Programmen: statisch typisierte Programme und dynamisch typisierte Programme. Wenn das Programm statisch typisiert ist und das beschriebene Verfahren in einer dynamisch typisierten Sprache implementiert ist, können Informationen über die Daten und Funktionen des Programms weggelassen werden. Entsprechend, wenn das Programm in einer dynamisch typisierten Sprache bereitgestellt wird und das beschriebene Verfahren in einer statisch typisierten Sprache implementiert ist, werden fehlende Typinformationen vom Compiler oder von einem Laufzeit-Framework, das die Ausführung zur Laufzeit unterstützt, aus dem Programm abgeleitet. Daher kann der gleiche Mechanismus verwendet werden, der die Bestimmung der endgültigen Typen (wie sie direkt für die Ausführung verwendet werden) im Sprachcompiler und/oder Runtime Execution Framework erleichtert und die Sprache des ursprünglichen dynamisch typisierten Programms ergänzt. Alternativ kann ein Compiler den Programmkode inspizieren und die Art der Datenstrukturelemente und Programmsegmente aus Programmkodeausdrücken (z.B. Konstanten), Markup (Attribute, Typdeklarationen) im Code oder bei kompilierten binären Darstellungen der Programmsegmente oder in ergänzenden Materialien, aus einer Liste der für den Compiler verfügbaren Standardtypen und/oder aus aufeinanderfolgenden Programmsegmenten oder anderen Programmkontexten ableiten.
-
Die Sequenz 210 der Anweisungen kann vom Benutzer erstellt worden sein. Der Compiler kann auch einzelne Befehle der Sequenz 210 zu Programmsegmenten 220 - 222 bekannter Befehle zusammenfassen. Das Hauptziel einer solchen Gruppierung (Kombination) ist die Verbesserung der Ausführungsleistung. In diesem Fall können die numerischen Kosten der Programmsegmente 220 automatisch vom Compiler aus den Kostenfunktionen der einzelnen Anweisungen abgeleitet werden, die im Programmsegment gruppiert sind, wie nachfolgend beschrieben wird.
-
Alternativ kann ein Programmsegment 220 nur einen einzigen Array-Befehl enthalten.
-
In der beispielhaften Ausführungsform von Figure 5, besteht die Sequenz 210 aus drei Programmsegmenten 220 - 222.
-
Wenn ein Teil des Anwenderprogramms 200 vom Compiler nicht als bekannter Befehl erkannt wird, kann die aktuelle Programmsequenz 210 beendet und eine neue Programmsequenz mit dem nächsten identifizierbaren Befehl gestartet werden.
-
In der beispielhaften Ausführungsform besteht die Sequenz
210 aus der vollen Programmzeile des Anwenderprogramms
200, mit zwei Eingangsarrays
A,
B und einer zusätzlichen skalaren Eingangsgröße (input) i:
-
Da dem Compiler alle Instruktionen bzw. Befehle (sum, abs, minus, subarray) bekannt sind, wird aus der gesamten Programmzeile eine einzige Sequenz erzeugt. In diesem Fall wird die Zuweisung ‚=‘ in die generierte Sequenz aufgenommen.
-
In anderen Ausführungsformen können Zuweisungen ausgeschlossen oder Mehrfachzuweisungen in Programmsegmente aufgenommen werden. Dadurch können die Programmsegmente eine beliebige Anzahl von Befehlen, Befehlszeilen und Befehlsabschnitten überspannen.
-
Weiterhin können die Programmsegmente Kontrollstrukturen und/oder Schleifenkonstrukte enthalten oder nicht. In solchen Fällen kann eine Sequenzgrenze erstellt werden, sobald der Compiler eines der eingeschränkten Sprachkonstrukte identifiziert.
-
Darüber hinaus können Markierungen im Anwenderprogramm 200 auch Grenzen von Sequenzen oder Programmsegmenten anzeigen. Solche Markierungen können vom Benutzer oder durch einen automatisierten Prozess erstellt werden. Beispielsweise kann ein Präprozessor den Programmkode 200 analysieren und mit dem Programm Markierungen erstellen und speichern, um Hinweise für die nachfolgenden Kompilierungsschritte zu geben.
-
Alternativ und/oder zusätzlich kann der Benutzer Markierungen bearbeiten, erstellen, verwenden und/oder speichern, z.B. in Form von Sprachattributen, um dem Compiler Informationen zur Unterstützung oder Steuerung der Identifikation, Segmentierung und/oder Extraktion von benötigten Metadaten über eine vom Benutzer bereitgestellte Funktion oder ein Programm zur Verfügung zu stellen. Der Compiler kann aber auch einige oder sogar alle Markierungen ignorieren.
-
Die Aufteilung des Programms 200 in Sequenzen 210 und die Aufteilung von Sequenzen 210 in Programmsegmente 220-222 kann die Erstellung einer graphischen Datenstruktur beinhalten, die die zu identifizierende Sequenz darstellt.
-
Der Baum kann als abstrakter Syntaxbaum (AST) angelegt werden, der dann alle Informationen enthält, die notwendig sind, um die Datenabhängigkeiten und den Programmablauf der Anweisungen und der entsprechenden Daten in der Sequenz zu identifizieren. Diese Informationen in der Grafik können zusammen mit begleitenden Informationen wie Metadaten der Funktionen auch zur Entscheidung für Programmsegmentgrenzen und/oder zur Berechnung von Programmsegmentkosten, Programmsegmentparametersätzen und/oder Programmsegmentausgabegrößen verwendet werden.
-
Um eine hohe Effizienz bei der Ausführung zu erreichen, versucht der Compiler typischerweise, die Anzahl der in einem Programmsegment zusammengeführten Befehle zu maximieren. Dies erhöht die Komplexität und den Arbeitsaufwand der Segmente und führt häufig zu einer besseren Auslastung der Ausführungseinheiten durch Minimierung des Overheads für den Übergang zwischen den Segmenten und der Speicher- und Transferkosten der Zwischenergebnisse.
-
Beispielsweise kann der Compiler versuchen, die Größe von Programmsegmenten zu erhöhen, indem er benachbarte Befehle im Programmkode iterativ kombiniert, bis mindestens eine bestimmte Grenze erreicht ist und das Hinzufügen weiterer Befehle zum neuen Segment gestoppt wird. Eine solche Grenze kann durch eine compilerspezifische Regel (heuristisch) oder eine compilerspezifische Grenze festgelegt werden, die sich auf die Komplexität der kombinierten Operation für die neuen Segmente, die Anzahl und/oder den Typ oder die Struktur der Ein-/Ausgabeparameter des neuen Segments bezieht, die Fähigkeit der Compilerlogik, benötigte Funktionsmetainformationen (als Funktionsgröße und Funktionsaufwand) als Funktion der Eigenschaften von Ein-/Ausgabedatenstrukturen des Segments auf Basis von Metainformationen der kombinierten Anweisungen zu erzeugen / abzuleiten, und/oder die Fähigkeit, aus den einzelnen Anweisungen eine Zwischendarstellung eines Rechenkerns zu erzeugen, der die kombinierte numerische Operation darstellt.
-
Eine weitere Möglichkeit, Programmsegmentgrenzen mit kombinierten Befehlen zu definieren, besteht darin, den Satz bekannter Befehle in Befehle zu kategorisieren, die keine Größenänderung für einen Ausgabeparameter in Bezug auf die Eingabeparametergrößen bewirken (map-artige Befehle) und weitere Befehle, die möglicherweise eine Änderung der Größe mindestens eines Ausgabeparameters in Bezug auf die Eingabeparametergrößen bewirken (reduce-type Befehle, Subarray und (Array-Erstellungsbefehle). Gemäß einer Ausführungsform enthält ein Programmsegment maximal einen Größenänderungsbefehl mit beliebig vielen bekannten map-artigen Befehlen, die von direkten Nachbarn des Größenänderungsbefehls im Benutzerprogrammkode abgeleitet sind.
-
Eine Methode zur Unterstützung der Entscheidung für die Programmsegmentgrenzenfestlegung, die ein Fachmann besonders hilfreich finden wird, ist die Begrenzung der Komplexität (Kosten) der Ermittlung des Aufwands zur Ausführung der Rechnerkerne eines Segments mit Instanzen von Input/Output-Datenstrukturen zur Laufzeit für jede Ausführungseinheit. Gemäß einer Ausführungsform wird dieser Aufwand mittels aus Funktionsmetainformationen des Segmentes abgeleiteten Funktionalen berechnet, die beim Hinzufügen neuer Anweisungen zu einem Programmsegment zur Kompilierzeit während der Segmentierung erzeugt bzw. übernommen werden (siehe unten). Die Komplexität solcher Aufwandsfunktionen wird in der Regel nicht erhöht, wenn dem Segment mapartige Rechenanweisungen hinzugefügt werden. Andere Arten von Anweisungen können zu einer komplexeren Kostenfunktion führen, die einen höheren Aufwand für die Auswertung zur Laufzeit erfordert. Um den Overhead der Entscheidung für eine Ausführungseinheit für ein Segment klein zu halten, kann der Compiler eine Segmentgrenze setzen, wenn die Hinzufügung einer neuen Anweisung die Komplexität oder den Aufwand für die Bewertung der Kosten eines Programmsegments erhöhen würde.
-
Um unterstützte Anweisungen zu finden, kann eine semantische Analyse des Programms oder von Teilen davon durchgeführt werden. Ziel einer solchen Analyse kann es sein, den Typ oder andere Informationen der Anweisung und/oder der Daten, die die Anweisung behandelt, zu bestimmen..
-
Alternativ und/oder zusätzlich kann ein einfacher Textabgleich auf Symbole (Token) des Programms 200 durchgeführt werden.
-
Eine semantische Analyse kann auch verwendet werden, um Datentypen abzuleiten, wenn das Programm 200 nicht direkt genügend Typinformationen liefert. Ein Beispiel bezieht sich auf die Sammlung von dynamisch oder schwach typisierten Sprachen und/oder Strukturen, die für technische Prototyping-Sprachen üblich sind.
-
Während der Kompilierungsphase I wird typischerweise für jedes identifizierte Programmsegment ein Laufzeitsegment 240 erzeugt. Die Laufzeit-Segmente 240 speichern typischerweise alle Informationen, die zur Ausführung der numerischen Operation des Programmsegments 220 auf den Ausführungseinheiten (71, 72 ,73) zur Laufzeit benötigt werden.
-
In einer Ausführungsform ist das Laufzeitsegment 240 als vom Compiler generierte Klassendefinition 249 realisiert, die zum Startzeitpunkt II instanziiert wird. Typischerweise speichert die Klasse die Zwischendarstellung der numerischen Operation für unterstützte Gerätekategorien (Kerne 241, 242, 243), Argumentadapter 245, die die Bestimmung, das Caching und das Laden von Arrayargumenten für die Segmente Rechenkerne 241 - 243 erleichtern, die Funktionsmetainformationen 248 für die (kombinierten) Anweisungen des Segments und einen Geräteauswahlcode 247, der die Ermittlung der Verarbeitungskosten und die Auswahl der Ausführungseinheit zur Laufzeit enthält. Der Gerätewähler 247 kann bei der Programmausführung als Einstiegspunkt für das Laufzeitsegment 240, 249 dienen. Daher kann der Gerätewähler 247 eine Funktionsschnittstelle zur Verfügung stellen, die die gleiche Anzahl und die gleichen Arten von Eingaben und Ausgabeargumenten unterstützt wie das Programmsegment 220. Damit kann der Compiler den Gerätewähler 247 als direkten Ersatz für die Befehle verwenden, aus denen das Programmsegment 220 besteht.
-
Das Ersetzen kann mittels Code-Injektion durch Modifikation des Programmkodes 200 oder durch Modifikation einer aus dem Anwenderprogramm 200 abgeleiteten kompilierten Assemblierung (ausführbare Binärdatei) erfolgen.
-
Die Implementierung der Laufzeitsegmente kann als separates Modul, Baugruppe, Bibliothek, Remote Service oder lokaler Service realisiert werden. Alternativ kann der Code der Laufzeitsegmente dem Anwenderprogramm 200 hinzugefügt werden.
-
In einer Ausführungsform kann die Startphase I durch Instanziierung des Laufzeitsegments 249 als statische Kontextvariable im Programm implementiert und ausgelöst werden. Während der Startphase I werden Informationen über die verfügbaren Ausführungseinheiten (73, 71, 72) gesammelt, die Zwischendarstellungen 241, 242, 243 des Programmsegments 220 für jede unterstützte Ausführungseinheit (73, 71, 72) angepasst, die Rechnerkerne kompiliert und in die Ausführungseinheiten (73, 71, 72) geladen.
-
Alternativ oder zusätzlich können einige der Initialisierungsschritte für einige der oben beschriebenen Ressourcen auf den Zeitpunkt verschoben werden, zu dem die Verwendung der entsprechenden Ressourcen zum ersten Mal während der Ausführung erforderlich ist.
-
Wie oben erläutert, sind die Kosten für die Ausführung jedes einzelnen Segments zur Laufzeit bestimmbar. Im Gegensatz zu anderen Ansätzen muss der Compiler nicht versuchen, die tatsächlichen Kosten einer Anweisung zum Zeitpunkt der Kompilierung oder vor der Zuweisung des Segments auf Geräte (Ausführungseinheiten) des heterogenen Rechensystems zu ermitteln. Stattdessen sind alle Geräte mit ausführbaren Laufzeitsegmenten ausgestattet und die zu erwartenden Kosten werden zur Laufzeit, typischerweise unmittelbar vor der Ausführung, ermittelt.
-
Die zu erwartenden Kosten eines Laufzeitsegments werden typischerweise anhand einer Kostenfunktion unter Berücksichtigung der intrinsischen Kosten der ausführbaren Instruktionen auf einer bestimmten Ausführungseinheit und der konkreten Daten, die durch die aus den Instruktionen des Programmsegments abgeleitete numerische Operation behandelt werden, bewertet.
-
Die intrinsischen Segmentkosten können aus den Kosten jeder einzelnen Anweisung im Programmsegment abgeleitet werden, indem man den Anweisungsgraphen der entsprechenden Sequenz 210 verwendet. Die Kosten einzelner Instruktionen können in geeigneter Weise kombiniert werden, um die intrinsischen Kosten des gesamten Segments so darzustellen, dass eine effiziente Auswertung zur Laufzeit möglich ist.
-
Eine solche Repräsentation 248 kann mit einem Laufzeitsegment 240 gespeichert und zur Laufzeit verwendet werden, um die tatsächlichen Kosten für die Ausführung des aus dem Programmsegment 220 kompilierten Laufzeitsegments für die konkrete Eingabedatenstruktur 10 zu ermitteln.
-
Daher sind die Berechnungskosten eines Laufzeitsegments eine Funktion solcher Eingabeargumente und können auf verschiedene Weise implementiert werden. Zum Beispiel kann der Compiler mehrere Möglichkeiten nutzen, um die Kostenfunktion dem Laufzeitsegment zur Verfügung zu stellen, von denen einige im Folgenden beschrieben werden.
-
Normalerweise wählt der Compiler 250 eine Kostenfunktionsimplementierung, die zur Laufzeit die beste Ausführungsleistung oder den geringsten Stromverbrauch zur Folge haben soll. Diese Auswahl entspricht oft der geringsten Anzahl von Informationen, die zur Laufzeit benötigt werden.
-
Für viele Anweisungen kann die Kostenfunktion anhand der Anzahl der Operationen bestimmt werden, die durchgeführt werden, um ein einzelnes Element C in der Ausgabe zu erzeugen. Die Gesamtkosten können dann das Ergebnis der Multiplikation der Kosten eines einzelnen Elements C mit einem Faktor N sein, wobei N der Anzahl der produzierten Ausgangselemente oder einer bestimmten Leistung davon entspricht, je nach Aufwand der Anweisung.
-
Für einfache Operationen wie sin(A) wäre N gleich der Anzahl der Elemente im Eingabefeld A. Für einen Sortierbefehl eines Feldes/Arrays A wäre der Aufwand O(n2) und N gleich dem Quadrat der Anzahl der Elemente in A. Andere Befehle können einen höheren oder niedrigeren Aufwand haben, was zu anderen Potenzen führt, einschließlich nichtganzzahliger Potenzen.
-
Die Berechnungskosten für ein einzelnes Element C können aus Lookup-Tabellen oder Caches ermittelt werden, z.B. durch Messungen an der konkreten Hardware, mit der der Befehl ausgeführt wird, oder durch Analyse des Rechenkerns eines Laufzeitsegments, das aus einer Zusammenstellung des Befehls für eine bestimmte Ausführungseinheit (CPU, GPU etc.) eines heterogenen Rechensystems resultiert.
-
Andere Anweisungen können komplexere Kostenfunktionen haben. Beispielsweise kann die Kostenfunktion nicht nur von der Anzahl der Elemente im Ausgabe-Ergebnis abhängen, sondern auch von der Anzahl der Dimensionen und/oder der Struktur (d.h. den Längen der einzelnen Dimensionen) mindestens eines Eingabeargumentes (z.B. Laufzeitinstanz(en) der jeweiligen Datenstrukturen), dem Wert von Elementen in mindestens einem Eingabeargument, der Existenz, Struktur, Art, Wert anderer Eingabeparameter und/oder Ausgabeparameter und/oder der Existenz und der Werte von Zusatzparametern.
-
Die Informationen der Kostenfunktion können vom Compiler 250 auch automatisch aus dem Befehl abgeleitet werden. Die automatische Ableitung der Kosten kann eine Analyse der Funktion beinhalten und ist besonders für einfache Anweisungen nützlich. Der Compiler 250 kann mindestens einen Code, den Namen, die variierende Anzahl von Parametern und/oder die ausführbare Binärdarstellung der Anweisung verwenden, um die Kosten der Funktion automatisch abzuleiten.
-
Alternativ und/oder zusätzlich können vom Compiler zusätzliche Metadaten verwendet werden, die für die Auswertung der Berechnungskosten zur Laufzeit geeignet sind. Solche Metadaten können während der Erstellung der Funktion erstellt worden sein. Durch die Vereinbarung eines bestimmten Satzes und/oder Formats solcher Metadaten kann der Programmierer des Programms 200 die Sammlung der unterstützten Anweisungen um eigene Funktionen und / oder Operationen erweitern.
-
Der Compiler 250 kann auch einen vordefinierten Satz von Anweisungen und zugehörige Kostenfunktionen kennen und/oder in der Lage sein, Anweisungen im Programmkode mit der Kostenfunktion der bekannten Anweisungen abzugleichen.
-
Der Compiler 250 kann auch entscheiden, die Kostenfunktionen der entsprechenden Anweisungen in einer einzigen Segmentkostenfunktion zusammenzufassen oder die Kostenfunktion für einzelne oder alle kombinierten Anweisungen durch eine Ersatzkostenfunktion zu ersetzen, die den Aufwand des Segments für eine bestimmte Instanz der eingegebenen Datenstrukturen darstellt.
-
Alternativ kann die Segmentkostenfunktion unter Berücksichtigung der Kostenfunktionen aller Einzelinstruktionen im Segment implementiert und/oder bewertet werden. Dies kann erreicht werden, indem die Knoten des Sequenzgraphen 210 durchlaufen werden und die Kostenfunktion jedes Knotens, der eine Anweisung darstellt, ausgewertet wird.
-
Weiterhin kann der Compiler 250 Optimierungen vornehmen, um die resultierende(n) Kostenfunktion(en) zu vereinfachen. Solche Optimierungen können z.B. das Einbinden der Instruktionskostenfunktion, das Weglassen solcher Parameter, die nicht (wesentlich) zur Verbesserung der Qualität / Genauigkeit des Kostenwertes für ein Segment beitragen, die teilweise oder vollständige Aggregation von Instruktionen in einem Segment und andere Methoden, die den Rechenaufwand bei der Auswertung einer Segmentkostenfunktion zur Laufzeit reduzieren, umfassen. Wenn der Compiler 250 beispielsweise für ein Programmsegment feststellt, dass die Kosten ausschließlich von der Anzahl der Elemente in seinem Eingabeparameter 10 abhängen, können andere Informationen wie z.B. die Anzahl der Dimensionen und/oder die Struktur des Eingabeparameters 10 in der resultierenden Kostenfunktion weggelassen werden.
-
Gemäß einer Ausführungsform werden Instruktionsinformationen durch Metadatenattribute bereitgestellt. Jede Anweisung kann eine Möglichkeit bieten, Metadaten für die Anweisung an den Compiler abzufragen. Beispielsweise ist jede geeignete ILNumerics-Funktion mit einem bestimmten Attribut versehen, das den Compiler bei der Identifizierung der Funktion als Anweisung unterstützt. Dementsprechend ist der Attributtyp dem Compiler bekannt und kann verwendet werden, um umfassende Metadaten der Funktion über eine vom Attributtyp implementierte gemeinsame Schnittstelle bereitzustellen.
-
Die Funktionsmetadaten enthalten typischerweise Daten zur Bestimmung der Größe und Struktur jeder erzeugten Instanz von Ausgabedatenstrukturen, die ein Array sein können, Daten zur Bestimmung des „Aufwands“, den die Anweisung (numerische Operation) zur Erzeugung einer Instanz einer Ausgabedatenstruktur aufwendet, und optionale Daten zur Ableitung von Rechenkerncode für eine bestimmte Ausführungseinheit und/oder einen generischen Ausführungseinheitstyp, wobei die Ausführungseinheiten zur Durchführung der numerischen Operation mit einem Satz von Instanzen von Eingabe-Datenstrukturen geeignet sind.
-
Die obigen unterstützenden Funktionsmetadaten werden typischerweise für jede Ausgabe, die die Anweisung erzeugen kann und für jede Art von Ausführungseinheit, die vom Framework unterstützt wird, einzeln bereitgestellt.
-
Beispielsweise ist die High-Level ILNumerics-Funktion abs(In0) zur Berechnung von Absolut- oder Größenelementwerten von Instanzen von Eingangsdaten In0 mit einem ILAcceleratorMetadata-Attribut versehen, das einen Link auf eine Klasse ILMetadata abs001 enthält. Beide werden vom Autor der abs-Funktion des ILNumerics-Frameworks zur Verfügung gestellt. Der Compiler kann die Klasse instanziieren und ihre Schnittstelle abfragen, um folgende Informationen zu sammeln: Größeninformationen von Instanzen der Ausgabedatenstrukturen Out0, die von der Anweisung abs(In0) in Bezug auf Instanzen von Eingangsdaten In0 erzeugt werden. Individuelle Größeninformationen können bereitgestellt werden, einschließlich, aber nicht beschränkt auf (
1) die Gesamtzahl der in Out0 gespeicherten Elemente (Numel(In0)), (
2) die Anzahl der Dimensionen, die zur Beschreibung der Struktur von Out0 (Numdim(In0)) verwendet werden, und (
3) die Länge (Anzahl der Elemente) für jede Dimension von Out0 (Size(In0, 0) bis Size(In0, 2)), wie in Tabelle I dargestellt, die typischerweise einen Segmentgrößenvektor oder eine Liste darstellt:
Numdim(In0) |
Numel(In0) |
Size(In0, 0) |
Size(In0, 1) |
Size(In0, 2) |
-
Diese Informationen werden typischerweise in Form einer Funktion bereitgestellt, die die Instanz der eingegebenen Datenstruktur In0 empfängt und einen Vektor (Size Descriptor) mit den als aufeinanderfolgende Elemente gespeicherten Größeninformationen erzeugt. Da es nur eine Ausgabe von abs(In0) gibt, liefert die beispielhafte Klasse ILMetadata_abs001 nur einen einzigen Größenbeschreiber. Abhängig von der Art der Anweisung kann die Funktion „size descriptor“ weitere Argumente benötigen, die der Anzahl der für die Anweisung definierten Argumente entsprechen. Zum Beispiel erfordert die Anweisung add(In0, In1) zum Addieren der Elemente von zwei Eingabefeldern zwei Array-Argumente, die für die Funktion „size descriptor“ benötigt, bereitgestellt und verwendet werden können..
-
Als Funktionsmetainformationen der abs(In0)-Anweisung kann weiterhin der ‚Aufwand‘ der Anweisung in Form von Instanzen ihrer Eingangsdaten In0, auf einer (verallgemeinerten) CPU-Plattform, bezogen auf (1) die Anzahl der Elemente in In0, (2) die Anzahl der Dimensionen in In0 und (3) die Länge jeder Dimension von In0 sein.
-
Solche Aufwandsinformationen können als normierte Werte oder Konstanten angegeben werden, die einen Aufwandsvektor aus Fließkomma- oder Ganzzahlwerten bilden. Jedes Element des Aufwandsvektors entspricht der Information der Elemente eines Größenbeschreibungsvektors, so dass bei der Berechnung des Vektorprodukts (d.h.: Berechnung des Skalarprodukts) mit dem Aufwandsinformationsvektor, der typischerweise den normalisierten Aufwand für die Anweisung beschreibt (i.e. Aufwand pro Element der Eingabedatenstruktur) und dem Größenbeschreibungsvektor, der der Instanz der Eingabedatenstruktur entspricht, der resultierende Wert dem Aufwand der Ausführung der Anweisung mit der Instanz der Eingabedatenstruktur entspricht.
-
Ebenso kann der ‚Aufwand‘ der Funktion abs(In0) in Bezug auf ihre Eingangsdaten In0 auf einer (generalisierten) GPU und/oder jeder anderen Ausführungseinheit angegeben werden.
-
Andere Ausführungsformen können andere Verfahren verwenden, um die Aufwandsinformationen der Anweisung zur Laufzeit bereitzustellen. Solche Verfahren können einfacher sein als die oben beschriebenen. Sie können Informationen aus der Berechnung auslassen oder einige Informationen durch geeignete Schätzungen ersetzen. Oder die Verfahren sind komplexer und berücksichtigen die gleichen und/oder andere Eigenschaften von verwandten Einheiten. Beispielsweise kann die Aufwandsinformation als Konstante statt als Funktion angegeben werden. Oder die Berechnung kann die Werte und/oder die Art der Elemente der eingegebenen Datenstrukturinstanzen, Zustandsinformationen der Ausführungseinheit, des Host-Controllers und/oder anderer Systemkomponenten, weitere Informationen der Ein-/Ausgabedatenstrukturinstanzen, der Funktionsmetainformationen und/oder der Rechenkerne berücksichtigen.
-
Die Funktionsmetainformationen können außerdem einen Vorlage-/Template-Kerncode (Rechenkern) enthalten, der als Vorlage für einen speziellen Rechenkern-Funktionskörper auf einer konkreten CPU-Plattform verwendet werden kann. Das Template wird typischerweise später im Programmlauf vervollständigt, wenn die konkreten Ausführungseinheiten bekannt sind. Die Vorlage kann Platzhalter enthalten, die später, z.B. beim Start, durch Eigenschaften der Ausführungseinheit ersetzt werden. Solche Eigenschaften können die Anzahl der Prozessoren, Kerne, Cache-Größe, Cache-Zeilengröße, Speichergröße und Geschwindigkeit, Bitrate, Features und/oder Befehlssatz, die von der Ausführungseinheit unterstützt werden, Stromverbrauch und/oder Frequenz der Ausführungseinheit sein.
-
Ebenso kann im Rahmen der Funktionsmetainformationen Kerncode (Rechenkern) zur Verfügung gestellt werden, der als Vorlage für einen speziellen Kern-Funktionskörper auf einer konkreten GPU und/oder jeder anderen Ausführungseinheit einschließlich eines Host-Controllers geeignet ist. Ein solcher Template-Kerncode kann mit Platzhaltern versehen werden, die beim Start durch Eigenschaften der konkreten Ausführungseinheit ersetzt werden, wie Anzahl der Prozessoren, Anzahl der Kerne, Cache-Größe, Cache-Zeilengröße, Speichergröße und Geschwindigkeit, Bitrate, Features und/oder Befehlssatz, der von der Ausführungseinheit unterstützt wird, Stromverbrauch und/oder Frequenz der Ausführungseinheit.
-
Der Compiler 250 verwendet die gesammelten Informationen, um ein Laufzeitsegment zu erzeugen. Die Laufzeitsegmente können ausführbaren Code in der gleichen Sprache wie das Anwenderprogramm enthalten. Der Compiler 250 kann den Programmkode auch so modifizieren, dass das Laufzeitsegment anstelle des ursprünglichen Programmteils, von dem das Segment abgeleitet wurde, ausgeführt wird.
-
Alternativ kann der Compiler ausführbare Anweisungen erstellen, die mit dem Ausführungssystem kompatibel sind und/oder eine aus dem Anwenderprogramm 200 erzeugte ausführbare Ressource modifizieren, um die Ausführung des Laufzeitsegments anstelle des entsprechenden Teils des Anwenderprogramms 200 zur Laufzeit zu bewirken.
-
Ein Laufzeitsegment kann sich über eine einzelne identifizierte Anweisung erstrecken. In diesem Fall entspricht der Segmentaufwand (siehe unten) dem Instruktionsaufwand und die Segmentgröße (siehe unten) der Instruktionsgröße. In anderen Fällen kann ein Segment mehrere Instruktionen umfassen und die Segmentgröße und der Aufwand werden aus dem Satz von mehreren Instruktionen und -größen berechnet. Siehe unten für eine Beschreibung der Erstellung von Segmentaufwand und -größe.
-
Das Laufzeitsegment kann mehrere Ausführungspfade für die ursprüngliche Absicht der Anweisungen implementieren. Ein Pfad führt das Laufzeitsegment auf der CPU aus, wobei möglicherweise das gleiche Laufzeitsystem verwendet wird, für das der Programmkode gemacht wurde. Andere Pfade können Low-Level-Beschleunigungsschnittstellen wie OpenCL oder CUDA oder OpenGL usw. verwenden, um das Laufzeitsegment auf einem beschleunigenden Gerät, wie einem Grafikprozessor oder ähnlichem, auszuführen. Optional ist für skalare oder kleine Eingabestrukturen ein schneller Pfad („Fast Track“) vorgesehen. Alle Pfade werden fertig initialisiert, indem entsprechende Rechenkerne auf die entsprechenden Ausführungseinheiten vorgeladen werden, auch wenn das Laden zu dem Zeitpunkt, zu dem ein Rechenkern zum ersten Mal ausgeführt werden soll, verzögert erfolgen kann.
-
In einer Ausführungsform hängt die Entscheidung zur Laufzeit, welcher Pfad (welches Gerät) für die schnellste / effizienteste Ergebniserstellung verwendet werden soll, oft nur von dynamischen Laufzeitdaten ab, z.B. der Datenmetainformation(en), insbesondere der Größe, Struktur und/oder den Werten von Laufzeitinstanzen der eingegebenen Datenstrukturen. Alle anderen bedingten Informationen wurden vom Compiler bereits zur Kompilierzeit bzw. zur Startzeit in den vorbereiteten Segmentaufwand ‚eingebrannt‘. Dies gilt insbesondere für solche Informationen, die zur Kompilier- oder Startzeit bekannt sind und nicht von dynamischen Änderungen zur Laufzeit abhängen.
-
Typischerweise implementiert jedes Laufzeitsegment einen schnellen Schalter für die dynamische Entscheidung des Ausführungspfades, basierend auf der Segmentgröße und dem Segmentaufwand.
-
Der Segmentaufwand entspricht der Anzahl der Operationen, die mit der Berechnung eines einzelnen Ergebniselements durch das Laufzeitsegment verbunden sind. Für ein Laufzeitsegment kann es mehrere Aufwände geben, wenn das Laufzeitsegment in der Lage ist, mehrere Ausgabeergebnisse zu liefern.
-
Segmentaufwände werden typischerweise als Funktionen der Größe der eingegebenen Datenstrukturinstanz realisiert.
-
Der Segmentaufwand kann ein Maß für die Kosten der im Laufzeitsegment implementierten und aus den Anweisungen, die das Segment zusammensetzen, kumulierten Anweisungen aufweisen. Da die Art und Weise, wie die Anweisung auf einzelnen Plattformen implementiert wird, unterschiedlich sein kann, wird ein Segmentaufwand typischerweise aus den Zusammensetzungsanweisungen für jede unterstützte Plattform einzeln berechnet.
-
Gemäß einer Ausführungsform wird der Segmentaufwand als Segmentaufwandvektor der Länge n entsprechend der Größenbeschreibung eines mehrdimensionalen Arrays implementiert, wobei n der Anzahl der vom System maximal unterstützten Dimensionen entspricht, die um die Zahl 2 erhöht wird..
-
Jeder Segment-Aufwandsvektor SEVO, SEV1 kann folgende Elemente enthalten oder aus ihnen bestehen:
- - Element #0: Aufwand der Funktion durch die Anzahl der Eingabedimensionen,
- - Element #1: Aufwand durch die Anzahl der Eingabeelemente,
- - ...
- - Element #2...(n-3): Aufwand durch die Länge der entsprechenden Dimension.
-
Elemente des Segmentaufwandsvektors können numerische Konstanten oder skalare numerische Funktionen sein. Der Compiler kann so konfiguriert werden, dass er alle Elemente des Segmentaufwandsvektors füllt und pflegt oder die Performance verbessert und/oder sogar optimiert, indem er einige Elemente aus den Berechnungen weglässt, abhängig von den Eigenschaften (Komplexität) der Funktionen im Segment.
-
Mindestens ein Element des Segmentaufwandsvektors ist jedoch ungleich Null.
-
Die Segmentgröße entspricht der Größenbeschreibung eines n-dimensionalen Arrays und kann als Segmentgrößenvektor der Länge (n + 2) implementiert werden, wobei n der Anzahl der vom System maximal unterstützten Dimensionen entspricht.
-
Typischerweise haben der Segmentgrößenvektor und der Segmentaufwand die gleiche Länge.
-
Der Segmentgrößenvektor kann folgende Elemente enthalten oder aus diesen bestehen:
- - Element #0: Anzahl der Dimensionen (n),
- - Element #1: Anzahl der Elemente,
- - ...
- - Element #2...(n+1): Länge der Dimension 1...n.
-
Die Elemente des Segmentgrößenvektors können numerische Konstanten oder skalare numerische Funktionen sein. Typischerweise umfassen die Elemente skalare Funktionen von Instanzen der eingegebenen Datenstrukturen. Auch hier kann der Compiler entscheiden, den Segmentgrößenvektor für eine bessere Performance anzupassen und/oder sogar zu optimieren.
-
Mit Bezug zur 6 wird die Ermittlung der zur Laufzeit III zu erwartenden Kosten für die Ausführung der jeweiligen Rechenkerne eines Laufzeitsegments, das ein beispielhaftes Programmsegment mit der Funktion 3+sin(A) für eine Array-Instanz A von 200 x 300 Elementen implementiert, erläutert. In der Beispielausführung werden die zu erwartenden Kosten für ein heterogenes Rechensystem mit zwei Ausführungseinheiten, nämlich einer CPU (Gerät0) und einer GPU (Gerät1), ermittelt.
-
Zur Laufzeit III und vor der Ausführung eines der Rechenkerne wird die gewünschte Ausführungseinheit für die Ausführung des Laufzeitsegments durch Berechnung der erwarteten Kosten für die Ausführung der jeweiligen Rechenkerne auf jedem verfügbaren Gerät ausgewählt.
-
Zunächst kann der Segmentgrößenvektor SSV und die Segment-Aufwandsvektoren SEV0, SEV1 bestimmt werden. In der Beispielausführung wird der Segmentgrößenvektor SSV wie oben in Bezug auf die Tabelle I implementiert und gemäß der Array-Instanz A ermittelt.
-
Für jedes Element des Segmentgrößenvektors SSV, das sich auf eine Funktion bezieht, wird die Funktion ausgewertet, um das Element als Zahl zu erhalten. Dies ist nur einmal durchzuführen, da sich die Größe des Ergebnisses des Segmentergebnisses zwischen den Geräten nicht ändert. Siehe unten, wie dies erreicht werden kann.
-
Der Segmentaufwandvektor SEV0, SEV1 wird typischerweise für jede Ausführungseinheit (CPU - Index 0, GPU - Index 1) ermittelt. Alle zugehörigen Elementfunktionen können ausgewertet werden (siehe unten).
-
Danach können die Berechnungskosten für jedes Gerät durch Berechnung eines skalaren Produkts zwischen dem Segmentgrößenvektor und dem Segmentaufwandvektor bestimmt werden..
-
Danach können die erwarteten (Gesamt-)Kosten ermittelt werden, indem die Berechnungskosten und Kosten für einen möglichen Datentransfer zu jedem Gerät auf der Grundlage des Segmentgrößenvektors und einer aktuellen Datenlokalitätsinformation (Datenortsinformation), wie sie im Array-Objekt A gepflegt sind, addiert werden.
-
Danach kann das Gerät, das den niedrigsten erwarteten Kosten entspricht, zur Ausführung ausgewählt werden.
-
Zusätzlich können weitere Informationen über die zu erwartenden Kosten berücksichtigt werden. Beispielsweise können zusätzliche Geräteeigenschaften (Cachegrößen, SIMD-Spurlänge auf CPUs mit paralleler Befehlssatzerweiterungen etc.), ein Faktor, der einem Overhead für die Einleitung von Datenübertragungen entspricht (neben den tatsächlichen Datenübertragungskosten), ein Overhead für die Auslösung der Ausführung von vorinstallierten Rechnerkernen, Prozessorfrequenz, Nutzung von Ausführungseinheiten etc. berücksichtigt werden..
-
Alternativ kann die Geräteauswahl für die Ausführung auf weniger Informationen als oben beschrieben basieren.
-
Eine Transferkostentabelle TCT wird typischerweise während der Startphase erstellt, sobald alle für die Durchführung numerischer Operationen geeigneten Geräte (z.B. auf Basis einer vordefinierten Version von OpenCL) identifiziert sind. Die Tabelle TCT kann zwischengespeichert werden, um den Startaufwand zu verringern.
-
Gemäß einer Ausführungsform können Array-Daten auf jedem Gerät (nicht nur auf dem Host) leben. Um den erforderlichen Aufwand für das Kopieren von Daten von einem aktuellen Gerät auf ein anderes Gerät zur Ausführung abzuschätzen, können die Kosten der Übertragung auf der Grundlage eines Übertragungsfaktors geschätzt werden, der den normalisierten Übertragungskosten vom aktuellen Gerät auf das andere Gerät entspricht.
-
Der Transferfaktor kann durch Messung bei der Inbetriebnahme oder auf der Grundlage bestimmter, mit den Systemen ausgelieferter Heuristiken ermittelt werden.
-
Der Transferfaktor kann zur Laufzeit mit der tatsächlichen Arraygröße multipliziert werden.
-
Weiterhin kann der Transfer während der Ausführung des Systems auf Basis der tatsächlichen Messdaten verfeinert werden. Dies kann helfen, Änderungen des Ressourcenzustands des Laufzeitsystems zu berücksichtigen.
-
Gemäß einer Ausführungsform werden Ortsvektoren (Lokalitätsvektoren) LV verwendet. Laufzeitinstanzen von Datenstrukturen (Arrays) können parallel auf mehreren Speicherplätzen existieren. Ein Ortsvektor von Verweisen auf solche Orte wird typischerweise von jeder Array-Operation gepflegt.
-
Die Länge des Ortsvektors LV kann der Anzahl der unterstützten Geräte entsprechen und ist typischerweise während der Programmausführung konstant, kann aber zwischen einzelnen Ausführungsläufen und/oder Rechnersystemen variieren.
-
Jedes der Elemente des Ortsvektors LV entspricht typischerweise einem (festen) Geräteindex 0, 1 für alle Laufzeitsegmente. Geräteindizes können beim Start des Programms im Rahmen der Ermittlung der Ausführungseinheiten vergeben werden.
-
Die Elementwerte des Ortsvektors LV können auf eine Referenz auf den Datenpuffer des jeweiligen Gerätes verweisen.
-
Die Elemente des Ortsvektors LV können ungleich Null sein, wenn die Array-Daten (aktuell) auf dem Gerät entsprechend dem Elementindex vorhanden sind, ansonsten sind sie Null.
-
In der in 6 dargestellten Beispielausführung existiert das Eingabefeld A auf nur dem CPU-Gerät (Index 0), wie es exemplarisch durch den Ortsvektor LV = (1, 0) angegeben ist, der einen Wert ungleich Null (1) nur im Ortsvektor LV-Element mit Index Null für die CPU speichert. Da das Array A nicht auf dem GPU-Speicher vorhanden ist, sind für die GPU beispielhafte Übertragungskosten von 0,2 pro Element des Eingangsarrays A zu berücksichtigen, wie in 6 durch den Übertragungskostenvektor TCV1 dargestellt ist, während für die CPU keine Übertragungskosten berücksichtigt werden müssen, so dass ein entsprechender Übertragungskostenvektor TCV0 mit Nullen gefüllt wird.
-
Für jede Ausführungseinheit können die zu erwartenden Kosten als Skalarprodukt SSV*(SEVi+TCVi) mit Geräteindex i (i=0, 1) ermittelt werden.
-
In der exemplarischen Ausführungsform sind die erwarteten Kosten für die Ausführung des Laufzeitsegments auf der CPU deutlich höher als die erwarteten Kosten für die Ausführung des Laufzeitsegments auf der GPU (90000 gegenüber nur 16980). Daher kann die GPU zur Ausführung ausgewählt werden.
-
6 zeigt zur Verdeutlichung nur die Kostenermittlung zur Laufzeit für eine numerische Operation (3+sin(A)), die elementweise (für die beispielhaften Ausführungseinheiten) in den Segmentaufwandsvektoren SEV0, SEV1 mit jeweils nur einem Element ungleich Null (nämlich dem zweiten, das sich auf Kosten in Abhängigkeit von der Gesamtzahl der gespeicherten Elemente bezieht) bezieht.
-
Weiteren Ausführungsformen der Berechnung der Segment-Aufwandsvektoren und des Segmentgrößenvektors werden im Folgenden anhand der bis erläutert, die die beispielhafte Erstellung der Funktionsmetainformationen an einem Segment, das die Operation 3+sin(A) durchführt, beschreiben
-
Für eine gute Performance kann es vorzuziehen sein, mehrere Befehle zu einem einzigen zusammengeführten Segment zusammenzuführen. Es gibt mehrere Optionen, um die Zusammenführung durchzuführen und die gesamte Segmentgröße und den Segmentaufwand zur Laufzeit zur Verfügung zu stellen. Die Verfahren unterscheiden sich in der Komplexität der von ihnen unterstützten Segmente und in den Kosten für die Berechnung ihrer Informationen.
-
Wenn der Compiler zur Kompilierzeit mit dem Aufbau des Segments beginnt, kann er zunächst den Codebaum oder den abstrakten Syntaxbaum (AST) des rechts in 7 gezeigten Segments identifizieren.
-
In der exemplarischen Ausführungsform ist der Baum relativ einfach. Auch komplexere Bäume werden unterstützt. Typischerweise entspricht jeder Knoten des Baumes einer Anweisung des Programms oder Datenargumenten (Konstanten, Arrays). Der Compiler beginnt, indem er an den Knoten des Baumes entlanggeht und die Metadaten aller beteiligten Anweisungen abfragt.
-
Während des Ablaufens der Knoten kann der Compiler Folgendes erstellen
- - Segmentaufwandsvektoren,
- - Segmentgrößenvektoren, und
- - Segmentkernel-Templates
für jede unterstützte Ausführungseinheit wie z.B. generische GPU, generische CPU, CPU-Host, generischer DSP, skalares Gerät etc. Dabei ist zu beachten, dass die für die einzelnen Kategorien der Ausführungseinheit erstellten Aufwandsvektoren noch keine gerätespezifischen Informationen enthalten. Vielmehr berücksichtigen die einzelnen Aufwandsvektoren individuelle Wege zur Umsetzung und Ausführung der Anweisungen in den verschiedenen Kategorien der Ausführungseinheit. Beispielsweise benötigen einige Gerätekategorien den Kerncode, um auch Schleifenstrukturen und/oder Thread-Verwaltungs-Overheads zu implementieren, um einige Anweisungen effizient auszuführen, während andere Gerätekategorien dies nicht tun.
-
Der Segmentgrößenvektor muss nur einmal aufgebaut werden, da er sich auf einzelnen Ausführungseinheiten nicht ändert.
-
Jedem Ergebnis (im Folgenden auch als Ausgangslot bezeichnet, von engl.: output slot) des Segments kann ein eigener Segmentgrößenvektor zugewiesen werden. Abhängig von der Komplexität der in den AST-Ergebnissen behandelten Anweisungen können die einzelnen Ausgabeslots des Segments den individuellen Berechnungskosten entsprechen. Einige Ausführungsformen berücksichtigen solche Unterschiede, indem sie individuelle Aufwandsvektoren für einzelne Ausgaben eines Segments pflegen. Aus Gründen der Übersichtlichkeit geht diese Beschreibung jedoch davon aus, dass die Segmente so einfach sind, dass alle Ausgaben für jede Ausführung des Segments auf einmal erstellt werden, so dass ein einziger Aufwandsvektor für jede Ausführungseinheit (Kategorie) die tatsächlichen Berechnungskosten für alle Ausgaben darstellen kann.
-
Anweisungen in einem Segment, die die Größe seiner Eingaben nicht verändern, können zu einer einzigen Größenfunktion zusammengefasst werden. Segmente, die solche Befehle enthalten, die die Größe ihrer Eingänge nicht verändern, erhalten typischerweise nur einen Größenfunktionsvektor, der den Größenbezeichner des entsprechenden Eingangs, der dem Ausgabeslots zugeordnet ist, widerspiegelt.
-
Andernfalls kann die endgültige Segmentgröße jedes Ausgabeslots als Baum von „Größe-ändernden Knoten“ („size-changing nodes“) entsprechend dem Segment-Syntaxbaum realisiert werden. Jede Knotengrößenfunktion kann die Größe ihrer Eingabe(n) entsprechend ändern und ‚erzeugt‘ eine neue Größe, die als Eingabe für die Größenfunktion ihres übergeordneten Knotens dient.
-
Der oben beschriebene Größenbaum wird typischerweise zur späteren Auswertung im Segment gespeichert. Die Größeninformationen können vom Compiler angepasst und/oder sogar optimiert und/oder in einem speziellen Format gespeichert werden, um eine effizientere Auswertung der Segmentgröße zur Laufzeit zu ermöglichen.
-
Zum Beispiel implementiert das Segment 3+sin(A) zwei Befehle: Add() und Sin(). Add() ist ein binärer Operator, der die Elemente von zwei Eingangsarrays addiert. Da eines der beiden Argumente die skalare Konstante ‚3‘ ist, hängt die Größe der von Add() erzeugten Ausgabe ausschließlich von der Größe des zweiten Eingabe-(Array)-Arguments zu Add() ab, entsprechend den üblichen Übertragungsregeln. Daher wird Add() hier als eine nicht größenändernde Anweisung betrachtet.
-
Ebenso berechnet die Anweisung Sin() den Sinus aller Elemente im Eingabefeld. Die Größe der von Sin() erzeugten Ausgabe entspricht der Größe des Input-Arrays. Daher sind beide Befehle im Segment nicht größenveränderlich und die Segmentgröße entspricht der Eingabegröße.
-
Dies wird auch in 8 dargestellt, in der der Segmentgrößenvektor SSV des erzeugten Segments Segment_0001 für den (einzigen) Ausgabeslot Out0 dem Größenbeschreiber des (einzigen) Input-Array-Arguments In0 entspricht.
-
Viele Größenbäume sind Einzelknotenbäume, da viele Segmente nur nichtgrößenändernde Operationen durchführen (wie im obigen Beispiel). Aufgrund der Dünn-Besetzheit (engl. „sparsity“) der Segment-Aufwandsvektoren SEV0 bis SEV1, die sich auf die jeweiligen Ausführungseinheiten PU0 bis PU2 und nichtgrößenändernde Operatoren beziehen (siehe unten), ist oft nur eine Einzelwertauswertung zur Laufzeit erforderlich, um den endgültigen Aufwand des Segments für eine bestimmte Ausführungseinheit zu berechnen, oft nur auf Basis der Gesamtzahl der Elemente im Eingabearray.
-
Für solche Segmente, die mehrere aggregierende Funktionen enthalten (z.B. Sum()), die die Größe von Eingabeargumenten verändern, müssen die Größenfunktionen basierend auf dem Befehlsbaum und den entsprechenden Metadaten zur Laufzeit jedoch rekursiv ausgewertet werden. Der Compiler kann entscheiden, den Funktionsumfang für ein Segment entsprechend einzuschränken, um die Auswertung der Segmentgröße einfach zu halten.
-
Die Segmentaufwandsvektoren SEV0 bis SEV1 können auf ähnliche Weise erzeugt werden. Metadaten jeder Anweisung werden vom Compiler für jede spezifische Ausführungseinheit einzeln abgefragt. Der Aufwand für jede Anweisung kann als einfacher Vektor von Skalaren oder von skalaren Funktionen angegeben werden. Typischerweise entsprechen die Segmentaufwandvektoren SEV0 bis SEV1 dem jeweiligen normierten Befehlsaufwand der Berechnung eines einzelnen Elements der entsprechenden Ausgabe. Zu beachten ist, dass die einzelnen Ausführungseinheiten PU0 bis PU2 unterschiedliche Aufwände zur Berechnung eines einzelnen Ausgabeelements beinhalten können, je nachdem, ob z.B. unterschiedliche Befehlssätze von der Ausführungseinheit, unterschiedliche Implementierungen der beabsichtigten numerischen Operation auf den Ausführungseinheiten und/oder unterschiedliche Unterstützung für den erforderlichen numerischen Elementdatentyp oder die numerische Genauigkeit unterstützt werden .
-
Die Aufwandsdatenvektoren für nicht größenändernde Array-Befehle werden typischerweise durch Addition der Aufwandsvektoren von nicht größenändernden Befehlen zu einem einzigen Aufwandsvektor aggregiert. 8 zeigt eine Ausführungsform, in der der Aufwand für die Berechnung eines einzelnen Elements der Ausgabe des Segments 3+sin(A) dargestellt ist. Beide Anweisungen sind nicht-größenveränderliche Anweisungen. Jedes Element der Ausgabe erfordert daher die Berechnung einer Addition und einer Auswertung der (intrinsischen) Sinusfunktion. Der Aufwand für beide Operationen wird vom Autor der Instruktion in Form von Instruktionsmetadaten bereitgestellt und zum Segmentaufwandvektor addiert.
-
Oft wird der Aufwand für die Berechnung eines einzelnen Elements der Ausgabe mit der Gesamtzahl der zu berechnenden Elemente assoziiert. In diesem Fall und zur Laufzeit führt eine einzige Multiplikationsoperation mit den Faktoren ‚Einzelelementaufwand‘ und ‚Anzahl der Ausgabeelemente‘ zu einer Schätzung des gesamten Segmentausführungsaufwands für eine bestimmte Eingangsdatenstruktur. Oft ist der Segmentaufwandvektor dünn besetzt, da nur ein einziges Element ungleich Null ist, nämlich das Element, das dem Index der Information über die Anzahl der Elemente in einem Größen-Deskriptor entspricht.
-
In der in 8 dargestellten exemplarischen Ausführungsform wird der Aufwand zur Erzeugung eines einzelnen Elements in den Segment-Aufwandsvektoren SEV0 bis SEV2 bei Index 1 (2. Indexwert) gespeichert, entsprechend der Position, an der die Anzahl der Elemente eines Arrays in einem Arraygrößen-Deskriptor bzw. dem SSV gespeichert ist. Hier ist der gespeicherte Wert 6 das Ergebnis der Addition des Einzelelementaufwands 1 der Add() Anweisung und des Einzelelementaufwands 5 der Sin() Anweisung. Andere Elemente des Segmentgrößenvektors sind 0, was bedeutet, dass der Segmentaufwand ausschließlich über die Gesamtzahl der Elemente in der eingegebenen Datenstruktur zur Laufzeit berechnet werden kann.
-
Andere Segmente können andere Befehle implementieren, z.B. die Aggregationsanweisung Sum(). Wenn solche Anweisungen in einem Segment enthalten sind, kann der entsprechende Instruktionsaufwand aus anderen Größeninformationen berechnet werden, z.B. aus der Anzahl der Dimensionen oder der Länge der einzelnen Dimensionen der eingegebenen Datenstrukturen..
-
Für Größenänderungsbefehle, d.h. Befehle, die Ausgabefelder mit einer anderen Größe als die Eingabeargumente erzeugen, ist eine Aggregation der Befehlsaufwandsinformationen im Voraus nicht einfach möglich. Stattdessen kann der Aufwand für die Erstellung eines einzelnen Ausgabeelements von Größeninformationen abhängen, die den Input/Output-Datenstrukturinstanzen entsprechen, da sie zur Laufzeit einzelnen Anweisungen des Segments zur Verfügung gestellt werden. Daher kann für diese Segmente ein Aufwandsauswertungsbaum angelegt werden. Knoten des Aufwandsbewertungsbaums speichern die Aufwandsinformationen der beteiligten Anweisungen für die spätere Auswertung.
-
Der Aufwandsauswertungsbaum kann zur Laufzeit verwendet werden, um den Segmentaufwandsvektor des Segments für bestimmte Eingangsdatenstrukturinstanzen zu erhalten. Daher werden die Befehlsaufwandsfunktionen und die in den Knoten des Baums gespeicherten Befehlsgrößenfunktionen entsprechend ausgewertet (z.B. rekursiv oder durch Abgehen der Knoten des Baums), wobei die konkreten Größen der Datenstrukturinstanzen (Eingabe- und Zwischenargumentgrößen) berücksichtigt werden.
-
Auf diese Weise ermöglicht der Aufwandsauswertungsbaum die Voraussage des Aufwands, der erforderlich ist, um die numerische Operation des Segments auf einer bestimmten Ausführungseinheit mit bestimmten Datenstrukturinstanzen auf der Basis zugehöriger Metadaten zu berechnen - ohne die Operation auf der Ausführungseinheit tatsächlich durchführen zu müssen.
-
Typischerweise werden die Befehlsaufwandsvektoren vom Autor der Befehle für jede Gerätekategorie einzeln gepflegt. Dabei wird berücksichtigt, dass einzelne Geräte die gleiche Operation unterschiedlich implementieren können bzw. dass die gleiche Operation unterschiedliche Ausführungskosten für einzelne Gerätekategorien verursachen kann.
-
Zur Verbesserung der Genauigkeit des Aufwandsabschätzungsergebnisses können weitere Daten berücksichtigt werden, wie z.B. Daten zu Speichertransferkosten und Frequenzen der Ausführungseinheiten.
-
Während die Segmentaufwandsvektoren nach der Kompilierungsphase den Aufwand der Erstellung eines einzelnen Elements darstellen können, können die Segmentaufwandsvektoren durch Berücksichtigung weiterer Daten weiter verfeinert werden. 9 zeigt die Anpassung des Segmentaufwandsvektors mit Informationen über die Anzahl der verfügbaren Kerne für zwei Ausführungseinheiten, eine CPU und eine GPU.
-
Zum Startzeitpunkt II kann die Anzahl der Kerne der verfügbaren Ausführungseinheiten ermittelt und die vorgegebenen Segmentaufwandvektoren SEV0 bis SEV2 entsprechend aktualisiert werden. In der beispielhaften Ausführung erlauben die erkannten 4 Kerne für die CPU die gleichzeitige Ausführung von 4 Befehlen. Daher werden die Elemente des vorgegebenen entsprechenden Segmentaufwandsvektors SEV1 (Werte 0,6,0,0,0,0, siehe Abb.(8)) durch 4 geteilt. Entsprechend kann die GPU 96 Kerne haben und der Segmentaufwandvektor SEV2 für die GPU wird durch 96 geteilt (oder mit -0,01 multipliziert). Entsprechend kann der vorgegebene Segmentaufwandvektor SEV0 (aus Gründen der Übersichtlichkeit nicht in 9 dargestellt), der den Aufwand zur sequentiellen / skalaren Ausführung des Segments auf der CPU darstellt, hier aktualisiert werden oder auch nicht.
-
Entsprechend von Ausführungsformen werden weitere und/oder andere Daten berücksichtigt, um die Segmentaufwandsinformationen zu erstellen und/oder zu aktualisieren und/oder weitere und/oder andere Faktoren als die Anzahl der Elemente in der Größenbeschreibung von Datenstruktur-Instanzen zu verwenden. Beispielsweise können weitere gerätespezifische Eigenschaften wie zusätzlicher Aufwand zum Initiieren einer Kernausführung, Länge der parallelen Befehlssatzerweiterung einer entsprechenden CPU, Speicherbusübertragungsraten zwischen den Geräten, Informationen zur Kerngruppierung, tatsächliche oder beabsichtigte Geräteauslastung berücksichtigt werden.
-
Außerdem können weitere Größeninformationen verwendet werden, um Aufwandsinformationen mit den tatsächlichen Datenstrukturen zu verknüpfen, die für die Ausführung eines Segments instanziiert wurden. Beispielsweise können konstante Faktoren in einen Größenbeschreiber aufgenommen oder dem berechneten Aufwand hinzugefügt werden. Oder zusätzlich zu den oben beschriebenen Größeninformationen wie Anzahl der Dimensionen, Anzahl der Elemente, Länge der Dimensionen, können ganzzahlige oder gebrochene Potenzen dieser Daten oder weitere Zahlen zur Beschreibung der Größe einer Datenstruktur-Instanz verwendet werden. Alle Informationen, die sich auf den Einfluss der Datenstruktur-Instanz auf die Ausführung des Segments beziehen, können innerhalb einer Segmentaufwandsauswertung verwendet werden, z.B. Werte von Elementen der Datenstruktur, Symmetrieinformationen, Datenbereichsinformationen, Boolesche oder Integer-Flags, die weitere Eigenschaften der Datenstruktur beschreiben.
-
Bei der Kompilierung wird typischerweise ein Rechenkern-Template (-Vorlage) für jede Gerätekategorie erstellt. Die Kernel-Templates implementieren die numerische Operation, die durch die Befehle des Segments beabsichtigt sind.
-
Solche Templates können verwendet werden, um den Rechenkerncode beim Start effizient zu implementieren und anzupassen, wenn die eigentlichen Ausführungseinheiten bekannt sind. Beispiele für die Anpassung sind die Berücksichtigung von Geräteeigenschaften wie Cache-Größen, Cache-Reihenlängen, Längen paralleler SIMD Befehlssatzerweiterungen, unterstützter Funktionsumfang, vom Gerät unterstützte Genauigkeit, Frequenz, Anzahl der Kerne, etc. Diese Informationen können verwendet werden, um den Kern und/oder die Kern-Templates für eine schnellere Ausführung auf den Ausführungseinheiten anzupassen.
-
Um die Methode für eine Vielzahl von Anwendungen numerischer Algorithmen nutzbar zu machen, sollte der Overhead der Methode so gering wie möglich gehalten werden. Dies verschiebt den Break-Even-Punkt nach unten, insbesondere die Größe der Array-Dateninstanzen, ab der die Vorteile der Methode durch die Ausführung auf Zusatzgeräten den durch die Methode selbst hinzugefügten Overhead übersteigen.
-
Die hierin beschriebenen Verfahren werden bereits bei einer vernünftigen geringen Anzahl rentabel. So kann z.B. das Parallelpotenzial auch bei relativ kleinen ArrayGrößen genutzt werden.
-
Wie in anderen Bereichen der Informatik sind Speichertransfers einer der größten Engpässe in modernen (von Neumann) Rechnerarchitekturen. Daher ist der Datenpufferaustausch zwischen den Geräten sorgfältig zu handhaben, die Speicherverwaltung auf den Geräten muss besonders effizient sein und die Eingabedatenstruktur kann die Erfindung mit den nachfolgend beschriebenen konstruktiven Vorteilen unterstützen.
-
Insofern ist es vorteilhaft, wenn Arrays (Datenstrukturinstanzen) ihre aktuellen Speicherorte verfolgen, z.B. können die Arrays die Speicherorte der Gerätepuffer speichern, in die ihre Elemente kopiert wurden. Auf diese Weise können die in einem Array gespeicherten Informationen auf mehreren Gerätespeichern gleichzeitig vorhanden sein. Alle diese Geräte können mit wenig Aufwand auf das Array zugreifen.
-
Der Compiler kann beschließen, die Speicherverwaltung auf den Ausführungseinheiten zu unterstützen, indem er die Segmentimplementierungen hinsichtlich ihrer Argumente optimiert. „Hinsichtlich“' bezieht sich dabei auf die Lebenszeit und/oder die Veränderbarkeit eines Arguments. Daher kann eine Unterscheidung zwischen den Intentionen von Segmentargumenten in die Sprache eingebaut werden (verschiedene InArray, OutArray, LocalArray und ReturnArray -Typen). Die Verwendung von Unveränderbarkeiten (Read-Only/immutable Input Arrays) und Volatilitäten (selbstzerstörende Rückgabe Array-Typen) spart Array-Kopien und ermöglicht eine frühzeitige, deterministische Freigabe von Arrayspeicher - auch für Sprachen, die keine deterministische Objektzerstörung von selbst unterstützen.
-
Der Gerätepufferspeicher kann bei Bedarf für Input-Array-Argumente zugewiesen werden und erhält die gleiche Lebensdauer wie der Speicher des Arrays selbst. Einmal in das Gerät kopiert, bleibt der Puffer dort, bis das Array entsorgt oder geändert wird. Auf diese Weise wird eine kostengünstige Wiederverwendung des Arrays (Puffers) in einem möglichen späteren Segmentaufruf ermöglicht.
-
Typischerweise annullieren (entsorgen / freigeben) Ausgabeargumente der Segmente alle anderen Speicherplätze für das Array und ein neuer Puffer wird der einzige Speicherplatz für das Array.
-
Freigegebene Puffer können auch zur späteren Wiederverwendung (Pooling) auf dem Gerät verbleiben.
-
Die Datenspeicherung von Arrays wird typischerweise getrennt von den Arraygrößen-Deskriptoren verwaltet. Dies ermöglicht die gemeinsame Nutzung von (großen) Datenspeichern zwischen Unterarrays desselben Quellarrays, die sich nur durch individuelle Größenbeschreibungen (manchmal auch als „Views“ bezeichnet) unterscheiden..
-
Arrays können auch nur verzögert Kopien ihrer Speicher erstellen (‚lazy copy on write‘).
-
Außerdem können Laufzeitsegmente auf Geräten, die sie unterstützen, asynchron ausgeführt werden. Typischerweise ist jeweils ein Gerät (oft der Host-Controller) dafür zuständig, die am besten geeignete Ausführungseinheit für ein Laufzeitsegment zu ermitteln und/oder das Laufzeitsegment asynchron zur ermittelten Ausführungseinheit zu verwalten. D.h,. das steuernde Gerät wartet nicht bis die Ausführung des Laufzeitsegments beendet ist, bis das Verfahren mit dem nächsten Laufzeitsegment weiterläuft.
-
10 veranschaulicht Verfahrensschritte, die mit der asynchronen Ausführung mehrerer Laufzeitsegmente und Anweisungen verbunden sind, die die Erstellung, den Zugriff und die Freigabe von und die Berechnung mit Datenstrukturen beinhalten, die mehrere Elemente eines gemeinsamen Datentyps wie z.B. Array-Objekte speichern können.
-
Gemäß einer Ausführungsform werden für jedes unterstützte Gerät zwei zusätzliche Objekte in den Array-Objekten gespeichert und gepflegt, nämlich eine Puffer-Referenz (engl.: buffer-handle) und eine Synchronisationsreferenz (engl. sync-handle).
-
Der Puffer-Handle speichert ggf. eine Referenz auf den Puffer der Array-Daten (Datenstruktur-Instanz) im gerätespezifischen Speicher. Der Typ des Puffer-Handle kann je nach Gerätetyp und / oder der Schnittstelle, über die auf die Geräte zugegriffen wird, variieren.
-
Der Synchronisationshandle (Synch-Handle) kann verwendet werden, um den Zugriff auf den Pufferspeicher für ein bestimmtes Gerät zu synchronisieren.
-
Synch-Handles können entweder von gerätespezifischen Schnittstellen (z.B. OpenCL, MPI) bezogen oder von den Host-Applikationscodes erstellt und gepflegt werden.
-
In der in 10 exemplarisch dargestellten Ausführungsform existieren zwei Ausführungseinheiten: ein CPU-Host-Gerät, das Speicher als „Managed Heap2 (Gerät 0) implementiert und eine GPU (Gerät 2) mit dediziertem GPU-Speicher.
-
Dabei ist zu beachten, dass der tatsächliche Gerätetyp (Ausführungseinheiten) unbedeutend sein kann, da alle Geräte über eine gemeinsame Schnittstelle der Geräte genutzt werden können. OpenCL erlaubt z.B. den Umgang mit dem CPU-Speicher wie mit dem GPU-Speicher. Ebenso können einzelne Shared-Memory-Geräte auf den gleichen HW-Speicher zugreifen. Dennoch ist die Gesamtfunktion gleich, außer dass einige Aufrufe in nicht erforderlich wären und aus Performance-Gründen weggelassen werden können.
-
Ebenso können die den Geräten zugeordneten Ortsindizes unterschiedlich gewählt werden. Während der CPU in 10 der Index 0 und der GPU der Index 2 zugewiesen wurde, können die Geräteortsindizes in der Menge der unterstützten Geräte unterschiedlich sein, ohne dieses Schema zu beeinflussen..
-
Beim Starten werden die unterstützten Geräte identifiziert und jedem Gerät wird typischerweise ein fester Index zugeordnet. Dieser Index wird während der gesamten Programmausführung verwendet, um das Gerät im Orts-Array zu lokalisieren.
-
Man beachte weiter, dass in der Figur nur die Aufnahmeplätze für Gerät 0 und Gerät 2 dargestellt sind. Andere Slots, die anderen Geräten im System entsprechen, werden aus Gründen der Übersichtlichkeit weggelassen..
-
10 zeigt, mit dem Fokus auf Speicherverwaltung und Synchronisation, eine schematische Darstellung der Aktionen zur asynchronen Berechnung des folgenden Array-Befehls für ein Eingangs bzw. Eingabe-Array A:
- [1] {
- [2] ILArray<double> A = rand(1000,2000);
- [3] ...
- [4] B = Abs(Sin(A * A));
- [5] var c = A.GetArrayForRead();
- [6] ...
- [7] }
-
Die Zeilen [1] und [7] definiern einen Codeblock, der dem Array A einen Lebenszeitbereich zuordnet. Der Array-Bereich wird in die folgende Betrachtung einbezogen, da er eine deterministische Zerstörung, Freigabe und Wiederverwendung der Arrayspeicher ermöglicht. Dies dürfte in den meisten Fällen die nicht-deterministische Speicherverwaltung durch z.B. Garbage-Kollektoren (GC) übertreffen, da es die zeitnahe, deterministische Freigabe und Wiederverwendung von Speicher ermöglicht und den Overhead der Garbage-Kollektoren und die damit verbundenen Kosten einspart. Außerdem vereinfacht das explizite Behandeln (engl.: scoping) die Speicherverwaltung, indem es dem Host erlaubt, im Single-Thread-Modus zu arbeiten, wodurch der Synchronisationsaufwand, der sonst durch Sperren oder andere Synchronisationsschemata entsteht, eingespart wird. Einige GC-Implementierungen können hier aufgrund ihrer Multithread-orientierten, nichtdeterministischen Objektzerstörung und/oder Finalisierung stören. In dieser Beschreibung wird davon ausgegangen, dass die verwendete Sprache in der Lage ist, Informationen über den Umfang bzw. die Lebensdauer ihrer Objekte zu liefern.
-
Sobald das Array A den Bereich des aktuellen Blocks verlässt, gilt es als entsorgungsbereit.
-
In Zeile 2 wird das Array A als Matrix mit beispielhaften 1000 Zeilen und 2000 Spalten mit Zufallszahlen auf dem Host-Controller erzeugt. Der Compiler kann diesen Array-Befehl als (Einzelbefehl-) Segment identifizieren und zur Laufzeit kann entschieden werden, die Zufallszahl auf der GPU zu berechnen (Gerät 2).
-
Zu diesem Zeitpunkt wird noch kein Speicher der GPU für die Elemente der großen Matrix A allokiert. Stattdessen wird das GPU-Gerät angewiesen, Speicher für die Matrix A auf seinem dedizierten Gerätespeicher zu allokieren. Diese Anweisung erfolgt asynchron: Der Host wartet nicht darauf, dass das Gerät den Puffer tatsächlich erzeugt, sondern erhält vom Gerät ein Synch-Handle, mit dem später der asynchrone Betrieb identifiziert wird. Während der Host-Controller sofort mit den nachfolgenden Operationen fortfahren kann, wird bei allen zukünftigen Versuchen, auf den Puffer auf dem Gerät zuzugreifen, das Synchronisationshandle verwendet. Solche Zugriffsversuche warten auf den Synch-Handle, um in den „Bereitschaftszustand“ zu wechseln, bevor sie den Zugriff auf den Puffer ausführen. Der Synchro-Handle wird im entsprechenden Geräte-Slot der Array-Speicherplätze abgelegt.
-
Der Synchronisationshandle kann ein einfacher gemeinsamer, universeller Zähler sein. Alternativ kann es sich auch um ein komplexeres Synchronisationsobjekt handeln, das vom Host-Framework bereitgestellt wird und/oder vom Betriebssystem oder anderen verwandten Ausführungsschichten des Systems unterstützt wird.
-
Weiterhin kann ein Pointer (Zeiger) auf den Puffer zur Verfügung gestellt und in dem gleichen Geräteschacht des Arrays A gespeichert werden, der dem GPU-Gerät entspricht. Der Puffer-Handle dient dem Zugriff auf den eigentlichen Speicher des Gerätes. Sein Typ kann von der Technologie abhängen, mit der auf das Gerät zugegriffen wird (z.B. OpenCL, MPI, OpenMP). Das Puffer-Handle kann im gleichen Aufruf wie das Synch-Handle bereitgestellt werden. Oder es kann in einem späteren Aufruf mit dem Synch-Handle bereitgestellt werden, um den Pufferzeiger vom Gerät abzufragen. Oder die Erstellung des Puffers kann insgesamt synchron erfolgen.
-
Sobald der Puffer angelegt ist (auch wenn noch nicht bekannt ist, ob die Puffererstellung abgeschlossen ist), wird die Ausführung des Rechenkerns ausgelöst. Diese Auslösung kann durch Einfügen eines entsprechenden Befehls in eine Befehlswarteschlange o.ä. erfolgen, so wie es die Low-Level-Schnittstelle für den Zugriff auf die Ausführungseinheit vorsieht.
-
Sowohl das Synch-Handle als auch das Puffer-Handle werden typischerweise dem Kerncode zur Verfügung gestellt. Der Aufruf erfolgt wiederum asynchron. Er kehrt zurück, ohne auf den Abschluss einer (möglicherweise zeitaufwendigen) Aktion zu warten, die mit dem Aufruf verbunden ist. Der Rechenkern wartet auf das Synch-Handle, falls die letzte Operation nicht beendet wurde und der Puffer noch nicht erreichbar ist. Dieses Warten und eventuelle teure und/oder zeitraubende Berechnungen im Rechenkern werden ohne Verzögerung des Host-Prozessor-Threads durchgeführt.
-
Der Aufruf, der die asynchrone Ausführung der Rechenkerne auslöst, stellt dem Host ein zweites Synchronisationshandle zur Verfügung. Der zweite Synch-Handle wird verwendet, um die Kern-Operation später zu identifizieren. Auf die Verkettung der beiden asynchronen Operationen sei hingewiesen: Die Rechenkernausführung wartet auf die Puffererzeugung. Daher ist der zweite zurückgegebene Synch-Handle geeignet, einen Bereitschaftszustand des Gerätepuffers nach Abschluss aller vorherigen Befehle zu erkennen. Daher reicht es aus, den zweiten Synchronisationshandle zu speichern, indem man den ersten ersetzt, der an dieser Stelle im Programm nicht mehr benötigt wird.
-
In anderen Ausführungsformen sind die Synch-Handle möglicherweise nicht verkettbar. Für jede Operation kann es erforderlich sein, ein neues Handle zu erzeugen. Die Synchronisation kann ohne Unterstützung durch die Low-Level-Geräteschnittstelle durchgeführt werden. In diesen Ausführungsformen kann es erforderlich sein, eine Queue oder eine Datenstruktur ähnlich einer Queue zu implementieren, um die mit den obigen Befehlen verbundenen Synchronisationsinformationen zu speichern und zu verwalten. Danach kann der einzelne Speicherplatz für ein Synchronisationshandle für jedes Gerät zu einer Referenz auf eine Warteschlange oder eine andere Datensammlung werden. In diesem Fall wird jedoch ein höherer Managementaufwand erwartet.
-
Nachdem die Berechnung von rand(1000,2000) (seg_001 in ) ausgelöst wurde, speichert der Host das durch den Aufruf zurückgegebene Synch-Handle im Array-Speicherplatz-Slot für Gerät 2 und fährt sofort mit nachfolgenden Operationen fort. Das Vorhandensein des Synch-Handles im Speicherplatz-Slot informiert andere Verbraucher über das Array, dass eine Operation, die den Speicher des Arrays nutzt, noch im Gange ist. Operationen, die ggf. die Existenz von Sync-Handles berücksichtigen müssen, schliessen den Zugriff auf den Puffer als Ausgabepuffer (d.h. Modifizieren des Puffers durch einen Kernel) und die Freigabe des Puffers ein.
-
Gemäß Zeile [4] der obigen Codeliste wird bei der nächsten Operation das gleiche Array A verwendet. Sobald der Host-Befehlszähler das entsprechende Segment erreicht (nehmen wir an, dies ist Segment seg_042), ist der Status der Fertigstellung von seg_001 nicht bestimmt.
-
Daher synchronisiert der Host die Ausführung der neuen Segmente mit der vorherigen Ausführung, indem er den Puffer-Handle und den Synch-Handle im Array-Speicherplatz für Gerät 2 bereitstellt. Der Rechenkern gibt sofort ein neues Synch-Handle zurück und wartet auf das angegebene Synch-Handle (falls vorhanden), bevor er die Ausführung startet. Es ist zu beachten, dass Synch-Handle generell optional sind. Das gesamte Schema oder beliebige Kombinationen einzelner Geräte können auch synchron arbeiten oder andere Schemata zur Synchronisation implementieren. Zu beachten ist weiterhin, dass die Ausgabe von seg_0042 hier aus Gründen der Übersichtlichkeit weggelassen wird.
-
Gemäß Zeile [5] der obigen Codeliste fordert die nächste Operation ein lokales Host-Array an, das die Array-Elemente als 1D-System-Array-Objekt darstellt. Da die Daten von A bisher nur auf dem GPU-Gerät vorhanden sind, müssen 2 Elemente von A auf das Host-Gerät 0 kopiert werden. Diese Kopie wird wiederum asynchron durchgeführt. Ein neuer Synchronisationshandle wird sofort vom Kopierbefehl an das Gerät 0 zurückgegeben - diesmal entsprechend einem neuen Puffer auf Gerät 0. Ähnlich wie bei der Array-Puffererstellung oben, kann ein Puffer-Handle sofort zurückgegeben oder später angefordert / gespeichert werden. Alternativ kann die Kopie der Daten auch durch einen expliziten Puffererstellungsbefehl vorangestellt werden.
-
Zu beachten ist, dass per Puffer-Handle nur auf Gerät 2 aus dem Puffer gelesen wird. Da keine Änderungen am Puffer auf Gerät 2 vorgenommen werden, ist in diesem Schritt kein neuer Synchronisationshandle für Geräteplatz 2 erforderlich.
-
Es wird angenommen, dass der folgende Befehl auf dem Host ausgeführt wird, der auf das von A.GetArrayForRead() zurückgegebene System-Array zugreift. Daher ist zu gewährleisten, dass alle Elemente bereits in den Puffer des Gerätes 0 kopiert wurden.
-
Zu diesem Zweck kann eine Speicherschranke eingeführt werden, die es erlaubt, mit Hilfe des Synchronisationshandles im Gerätespeicherplatz 0 auf den Abschluss des Kopiervorgangs zu warten. Diese Speicherschranke funktioniert in der gleichen Weise wie bei herkömmlichen Synchronisationsmethoden und kann verwendet werden, um synchron auf den Abschluss von Operationen auf einem beliebigen Gerät zu warten. Ähnliche Wartemechanismen werden z.B. beim Versuch, einen Puffer freizugeben, verwendet.
-
Schließlich verlässt A den Bereich des aktuellen Codeblocks. Da A danach nicht mehr referenziert wird, können die mit A verbundenen Speicher so schnell wie möglich entsorgt werden, um die auf dem (begrenzten) Gerätespeicher verbrauchten Speicherbereiche freizugeben. Dabei wird das Vorhandensein von Synch-Handles berücksichtigt. Das Verfahren kann implizite Speicherschranken verwenden (wie bei der WarteSync-Handle() (engl. WaitSynchHandle()) Methode in 10), bevor der Speicher freigegeben wird. Oder es können asynchrone Freigabevorgänge verwendet werden, wenn die Geräteschnittstelle diese unterstützt.
-
Zu beachten ist, dass das Verfahren eine 1:1-Beziehung zwischen Array-Objekten und Gerätepuffern implementieren kann oder nicht. Für eine möglichst effiziente Speicherverwaltung kann es wünschenswert sein, dass mehrere Arrays denselben Pufferspeicher verwenden / referenzieren können, z.B. in einem „Lazy Copy on Write“-Schema. Dementsprechend darf die Entsorgung eines Arrays nicht sofort zur Freigabe seiner Puffer führen. Stattdessen kann Referenzzählung verwendet werden, um einen Referenzzähler des gemeinsamen Speicherobjekts zu dekrementieren. Die Puffer werden freigegeben, sobald der Referenzzähler anzeigt, dass keine weiteren Arrays existieren, die auf diesen Speicher verweisen.
-
Eine asynchrone Ausführung kann helfen, die Geräte eines heterogenen Rechensystems auszulasten und die vorhandenen Rechenressourcen effizienter zu nutzen und somit die Ausführung der Sequenz früher abzuschließen oder weniger Energie zu verbrauchen.
-
Beim Hinzufügen eines Laufzeitsegmentes zur Ausführung auf einem Gerät wird ohnehin ein Maß für die Kosten des Segments berechnet. Dieses Maß kann auch als Repräsentation der Anzahl an ,wartenden' Operationen in der Warteschlange verwendet werden. Das kumulierte Maß aller Segmente, die derzeit in der Warteschlange auf die Ausführung warten, gibt einen Hinweis auf die anstehenden Operationen in der Gerätewarteschlange.
-
Dementsprechend kann auch die Tatsache berücksichtigt werden, dass jeder Eintrag einer individuellen Anzahl von Operationen entsprechen kann.
-
Die Entscheidung für das „optimale“ Gerät kann auch auf der Grundlage dieser Kosten „im Voraus“ getroffen werden. Wenn ein Gerät viele Segmente in der Warteschlange hat, kann es besser sein, die Kosten für eine Datenpufferkopie auf ein anderes Gerät zu investieren und stattdessen das Segment auf dem anderen Gerät auszuführen bzw. in dessen Warteschlange zu stellen.
-
Weiterhin kann eine Low-Level-Schnittstelle (z.B. OpenCL) konfiguriert werden, um die Laufzeitsegmente in beliebiger Reihenfolge auszuführen. Wenn die Low-Level-Geräteschnittstelle oder die übergeordnete Management-Schicht entscheidet, dass eine andere Reihenfolge als die Reihenfolge, in der die Segmente ursprünglich in der Sequenz angeordnet waren, in Bezug auf die Performance oder andere Aspekte vorteilhaft ist und dass der Austausch der Reihenfolge keine negativen Nebeneffekte für die Sequenz-Ergebnisse mit sich bringt, kann sie die Reihenfolge der Ausführung ändern.
-
Eine Neuanordnung der Ausführungsreihenfolge kann sogar die gleichzeitige Ausführung einiger Laufzeitsegmente auf mehreren Geräten ermöglichen. Beispielsweise kann die Neuanordnung einer Sequenz von Segmenten solche Segmente zusammenfassen, die von den Ergebnissen der anderen abhängen. Es kann auch solche Segmente zusammenfassen, die die gleichen oder meist die gleichen Argumente / Datenpuffer benötigen. Die Gruppierung kann auch auf Basis der aktuellen Datenlokalität-Puffer erfolgen. All dies kann die Chance erhöhen, dass solche Gruppen von Segmenten gleichzeitig und effizient auf mehreren Geräten ausgeführt werden können. Beispielsweise kann eine erste Gruppe von neu angeordneten Laufzeitsegmenten auf dem ersten Gerät asynchron ausgeführt werden, während nachfolgende Laufzeitsegmente, die eine zweite Gruppe bilden, zur asynchronen Ausführung in die Warteschlange des zweiten Geräts gestellt werden können. Da die Datenabhängigkeiten zwischen den Gruppen gering gehalten werden, können beide Laufzeitsegmentgruppen mit geringem Synchronisationsaufwand ausgeführt werden.
-
Dementsprechend ist das Verfahren in der Lage, viele oder sogar alle Geräte vorteilhaft beschäftigt zu halten. Dadurch werden die Rechnerressourcen besser ausgenutzt.
-
Gemäß einer Ausführungsform, ein computerimplementiertes Verfahren beinhaltet die Identifizierung eines Programmsegments in einem Programmkode, typischerweise einer Sequenz von Programmsegmenten. Das Programmsegment enthält einen Rechenbefehl, typischerweise eine numerische Operation, für eine erste Datenstruktur, die mehrere Elemente eines ersten gemeinsamen Datentyps speichern kann und Funktionsmetainformationen, die Daten enthalten, die sich auf eine Ausgabe des Rechenbefehls beziehen. Es werden eine erste Ausführungseinheit und eine zweite Ausführungseinheit eines Rechensystems ermittelt, die zur Ausführung einer jeweils kompilierten Darstellung (typischerweise eines Rechenkerns) des Programmsegments geeignet sind. Die erste Ausführungseinheit unterscheidet sich von der zweiten Ausführungseinheit. Die erste Ausführungseinheit wird mit einer ersten kompilierten Darstellung (Repräsentation) des Programmsegments und die zweite Ausführungseinheit mit einer zweiten kompilierten Darstellung des Programmsegments initialisiert. Die Funktionsmetainformationen und eine Datenmetainformation einer Laufzeitinstanz der ersten Datenstruktur werden verwendet, um erste erwartete Kosten für die Ausführung der ersten kompilierten Darstellung auf der ersten Ausführungseinheit und zweite erwartete Kosten für die Ausführung der zweiten kompilierten Darstellung auf der zweiten Ausführungseinheit zu ermitteln. Die ersten erwarteten Kosten und die zweiten erwarteten Kosten werden verwendet, um entweder die erste Ausführungseinheit oder die zweite Ausführungseinheit für die Ausführung der jeweils erstellten Darstellung auszuwählen, typischerweise die Ausführungseinheit mit geringeren erwarteten Kosten.
-
Alternativ dazu werden die ersten erwarteten Kosten und die zweiten erwarteten Kosten verwendet, um entweder nur eine der ersten Ausführungseinheit und der zweiten Ausführungseinheit für die Ausführung der jeweiligen kompilierten Darstellung auszuwählen oder um entsprechende Anteile eines Arbeitsaufwands für die Ausführung der jeweiligen kompilierten Darstellung auf der ersten Ausführungseinheit und auf der zweiten Ausführungseinheit zu bestimmen.
-
Die Verteilung der Arbeitslast kann ausgelöst werden, wenn der Rechenbefehl für die Ausführung auf/mit Teilen der Laufzeitinstanz geeignet ist. Dies gilt insbesondere für Rechenbefehle, die elementweise oder auf trennbaren Bereichen der Daten ausgeführt werden können. Beispiele sind numerische Operationen wie das Addieren von Matrizen, das Berechnen einer Funktion einer Matrix A wie sin(A) und das Glätten von Daten.
-
Die Lastverteilung erfolgt typischerweise durch das Zuordnen eines ersten Teils der Laufzeitinstanz zur ersten Ausführungseinheit und eines zweiten Teils der Laufzeitinstanz zur zweiten Ausführungseinheit. Die Arbeitslast kann nach den erwarteten Kosten aufgeteilt werden, z.B. umgekehrt bezogen auf die erwarteten Kosten und/oder die erwartete Ausführungszeit. Die erwartete Ausführungszeit ergibt sich aus den erwarteten Kosten und der tatsächlichen Verfügbarkeit bzw. Auslastung der Ausführungseinheiten.
-
Typischerweise wird die Arbeitslast verteilt, wenn eine Laufzeitinstanz ausreichend groß ist und/oder wenn festgestellt oder erwartet wird, dass die Auslastung mindestens einer auf dem System vorhandenen Ausführungseinheit im Vergleich zu anderen Ausführungseinheiten gering ist. Im letzteren Fall kann das Laufzeitsegment, der Compiler zur Laufzeit oder ein Scheduling-Modul entscheiden, die nicht ausgelastete(n) Ausführungseinheit(en) mit einem oder mehreren Geräten mit höherer Auslastung zu teilen, um zumindest einen Teil der Arbeitslast von dem/den besetzten Gerät(en) auf das/die nicht ausgelastete(n) Gerät(e) zu verteilen.
-
Geteilte Geräte (shared devices) können so genannte ‚virtuelle Geräte‘ bilden und sind besonders nützlich, wenn die Ausführungseinheit nicht asynchron ausgeführt werden kann. Dies kann der Fall sein, wenn die zugrundeliegende Low-Level-Schnittstelle für den Zugriff auf die Ausführungseinheiten solche Funktionen nicht unterstützt und/oder wenn die Anweisungen zu viele Abhängigkeiten aufweisen, so dass ein Umordnen von Anweisungen/Segmenten und/oder ein Verteilen von Segmenten auf mehrere Geräte nicht möglich oder nicht praktikabel ist.
-
Wenn Instanzen von Datenstrukturen den Speicher mehrerer Geräte (verteilter Speicher) überspannen, ermöglichen virtuelle Geräte außerdem, dass die an der Ausführung von Segmenten mit solchen verteilten Instanzen beteiligten Geräte entsprechend dieser Erfindung verwendet werden können. Das virtuelle Gerät wird dazu als Ausführungseinheit betrachtet und wie oben beschrieben behandelt.
-
Ein weiteres Szenario, in dem virtuelle Geräte für die Performance der Ausführung von Sequenzen vorteilhaft sind, sind Geräte mit gemeinsam verwendeten Speicher (shared-memory), z.B. CPUs, die sich Speicher mit GPU-Geräten teilen. Hier kann ein virtuelles Gerät, das alle oder Teile der CPU-Kerne und alle oder Teile der GPU-Kerne umfasst, eine höhere Rechenfähigkeit aufweisen als die zugrunde liegenden separaten Geräte. Da keine Transferkosten (Übertragungskosten) anfallen (aufgrund des gemeinsamen Speichers), führt das Ersetzen eines der zugrunde liegenden Geräte durch das virtuelle Gerät zu einer höheren Ausführungsleistung.
-
Gemäß einer Ausführungsform speichert ein computerlesbares Medium Instruktionen (Anweisungen, Befehle), die bei Ausführung durch einen Computer (Rechner) den Computer veranlassen, eines der oben beschriebenen Verfahren und/oder mindestens eines der folgenden Verfahren auszuführen, die zumindest teilweise als API (Application Programming Interface) implementiert sein können: Abrufen von Softwarecode, typischerweise ein in einer GPL oder einer wissenschaftlichen Rechensprache wie MATLAB oder NumPy geschriebener Benutzercode, Identifizieren eines Programmsegments in dem Softwarecode, das eine numerische Operation für eine erste Datenstruktur umfasst, die mehrere Elemente eines gemeinsamen Datentyps speichern kann, Bestimmen von Funktionsmetainformation(en) bezüglich einer Ausgabe der numerischen Operation, insbesondere einer Größe der Ausgabe, einer Struktur der Ausgabe, und/oder eines Aufwands zur Erzeugung der Ausgabe, Bestimmen der Ausführungseinheit des Computers oder eines anderen heterogenen Rechensystems als zur Durchführung der numerischen Operation geeignet, und Übersetzung des Programmsegments in eine Zwischendarstellung, insbesondere eine Bytecode-Darstellung, oder in eine auf dem Computer oder dem anderen heterogenen Rechensystem ausführbare Darstellung, insbesondere ein Laufzeitsegment des Programmsegments.
-
Typischerweise enthält das Laufzeitsegment die Funktionsmetainformation(en), einen Rechenkern für jede der ermittelten Ausführungseinheiten und ausführbaren Code zur Ermittlung der zu erwarteten Kosten für die Ausführung der Rechenkerne mit einer Laufzeitinstanz der ersten Datenstruktur unter Verwendung der Funktionsmetainformation(en) und einer Datenmetainformation der Laufzeitinstanz der ersten Datenstruktur. Die Datenmetainformation kann eine Laufzeitgrößeninformation der Laufzeitinstanz, eine Laufzeitspeicherortinformation der Laufzeitinstanz und eine Laufzeittypinformation der Laufzeitinstanz enthalten.
-
Das Ermitteln der Funktionsmetainformation(en) kann ein Lesen der Funktionsmetainformationen, wenn diese bereits im Softwarecode gespeichert sind, die Suche in einer Look-up-Tabelle (Nachschlagetabelle), die Funktionsmetainformationen (bereits) bekannter numerischer Operationen wie Addieren, Sinus, FFT (schnelle FourierTransformation) etc. speichert und/oder sogar die Ableitung der Funktionsmetainformationen auf Basis von Testläufen beinhalten.
-
Obgleich verschiedene beispielhafte Ausführungsformen der Erfindung offenbart wurden, leuchtet dem Fachmann ein, dass verschiedene Änderungen und Modifizierungen vorgenommen werden können, die einige der Vorteile der Erfindung erreichen, ohne vom Geist und Schutzumfang der Erfindung abzuweichen. Dem Durchschnittsfachmann ist klar, dass andere Komponenten, die die gleichen Funktionen erfüllen, ebenso an ihre Stelle treten können. Es soll erwähnt werden, dass Merkmale, die mit Bezug auf eine konkrete Figur erläutert wurden, mit Merkmalen aus anderen Figuren kombiniert werden können, selbst in Fällen, in denen es nicht ausdrücklich erwähnt wurde. Solche Modifizierungen am erfinderischen Konzept sollen ebenfalls durch die beiliegenden Ansprüche erfasst werden.
-
Im Sinne des vorliegenden Textes sind die Begriffe „haben“, „aufweisen“, „enthalten“, „umfassen“ und dergleichen offene Begriffe, die das Vorhandensein genannter Elemente oder Merkmale angeben, aber keine weiteren Elemente oder Merkmale ausschließen. Die Artikel „ein/einer/eine“ und „der/die/das“ beinhalten sowohl die Einzahlbedeutung als auch die Mehrzahlbedeutung, sofern der Kontext nicht eindeutig ein anderes Verständnis verlangt.
-
Mit der obigen Bandbreite an Variationen und Anwendungen vor Augen versteht es sich, dass die vorliegende Erfindung weder durch die obige Beschreibung noch durch die beiliegenden Zeichnungen eingeschränkt wird. Vielmehr wird die vorliegende Erfindung ausschließlich durch die folgenden Ansprüche und ihre rechtlichen Äquivalente beschränkt.