2 Stimmen

Wie man benutzerdefinierte Anweisungen/Erweiterungen in Standard-C/C++-Code einschließt und übersetzt, ohne die Leistung zu beeinträchtigen

Ich entwickle einen allgemeinen Bildverarbeitungskern für FPGAs und ASICs. Die Idee ist, ihn mit einem Standardprozessor zu verbinden. Eines der Probleme, die ich habe, ist, wie man ihn "programmiert". Lassen Sie mich erklären: Der Kern hat einen Befehlsdecoder für meine "benutzerdefinierten" Erweiterungen. Zum Beispiel:

vector_addition $vector[0], $vector[1], $vector[2]    // (z.B. v2 = v0+v1) 

und viele weitere ähnliche Operationen. Dieser Befehl wird vom Prozessor über den Bus an den Kern gesendet, wobei der Prozessor für Schleifen, nicht-vektorisierte Operationen usw. verwendet wird, wie zum Beispiel:

for (i=0; i<15;i++)           // auf dem Prozessor auszuführen
     vector_add(v0, v1, v2)   // auf meinem benutzerdefinierten Kern auszuführen

Das Programm ist in C/C++ geschrieben. Der Kern benötigt nur den Befehl selbst, in Maschinencode

  1. opcode = vector_add = 0x12h
  2. register_src_1 = v0 = 0x00h
  3. register_src_2 = v1 = 0x01h
  4. register_dst = v2 = 0x02h

    maschinencode = opcore | v0 | v1 | v2 = 0x7606E600h

(oder was auch immer, einfach eine Verkettung verschiedener Felder, um die Anweisung im Binärformat zu erstellen)

Nach dem Senden über den Bus an den Kern kann der Kern alle Daten aus dem Speicher über dedizierte Busse anfordern und alles ohne Verwendung des Prozessors handhaben. Die große Frage ist: wie kann ich die vorherige Anweisung in ihre hexadezimale Darstellung übersetzen? (das Senden über den Bus ist kein Problem). Einige Möglichkeiten, die mir einfallen, sind:

  • Interpretierten Code ausführen (Übersetzung in Maschinencode zur Laufzeit im Prozessor) --> sehr langsam, selbst bei Verwendung einer Art Inline-Makro
  • Die benutzerdefinierten Abschnitte mit einem externen benutzerdefinierten Compiler kompilieren, die Binärdatei aus dem externen Speicher laden und mit einer eindeutigen Anweisung an den Kern übertragen --> schwer zu lesender/verstehender Quellcode, schlechte SDK-Integration, zu viele Abschnitte, wenn der Code sehr segmentiert ist
  • JIT-Kompilierung --> zu komplex nur für diese Aufgabe?
  • Den Compiler erweitern --> ein Alptraum!
  • Einen benutzerdefinierten Prozessor an den benutzerdefinierten Kern anschließen, um alles zu handhaben: Schleifen, Zeiger, Speicherzuweisung, Variablen... --> zu viel Arbeit

Das Problem betrifft Software/Compiler, aber für diejenigen, die über tiefgehendes Wissen zu diesem Thema verfügen, handelt es sich um ein SoC in einem FPGA, wobei der Hauptprozessor ein MicroBlaze ist und der IP-Kern AXI4-Busse verwendet.

Ich hoffe, ich habe es richtig erklärt... Vielen Dank im Voraus!

1voto

edA-qa mort-ora-y Punkte 27791

Ich bin mir nicht sicher, ob ich das vollständig verstehe, aber ich glaube, ich habe etwas Ähnliches schon einmal erlebt. Basierend auf dem Kommentar zur Antwort von Rodrigo hört es sich so an, als ob Sie kleine Anweisungsstücke in Ihrem Code verstreut haben. Sie erwähnen auch, dass ein externer Compiler möglich ist, aber nur mühsam. Wenn Sie den externen Compiler mit einem C-Makro kombinieren, können Sie etwas Vernünftiges bekommen.

Betrachten Sie diesen Code:

für (i=0; i<15;i++)
     CORE_EXEC(vector_add(v0, v1, v2), ref1)

Das CORE_EXEC-Makro wird zwei Zwecke erfüllen:

  1. Sie können ein externes Tool verwenden, um Ihre Quelldateien nach diesen Einträgen zu durchsuchen und den Kerncode zu kompilieren. Dieser Code wird mit C verknüpft (erstellt einfach eine C-Datei mit binären Bits) und verwendet den Namen "ref1" als Variable.
  2. In C definieren Sie das CORE_EXEC-Makro, um den String "ref1" an den Kern zur Verarbeitung zu übergeben.

Also wird Stufe 1 eine Datei mit kompilierten binären Kernanweisungen erzeugen, zum Beispiel könnte das obige einen String wie diesen haben:

const char * const cx_ref1[] = { 0x12, 0x00, 0x01, 0x02 };

Und Sie könnten CORE_EXEC so definieren:

#define CORE_EXEC( code, name ) send_core_exec( cx_##name )

Sie können natürlich die Präfixe nach Belieben wählen, obwohl Sie in C++ möglicherweise lieber einen Namespace verwenden möchten.

Hinsichtlich des Toolchains könnten Sie eine Datei für all Ihre Teile erstellen oder eine Datei pro C++ Datei erstellen - was möglicherweise einfacher für die Dirty-Erkennung sein könnte. Dann können Sie die generierten Dateien einfach in Ihren Quellcode einbinden.

0voto

rodrigo Punkte 87935

Könnten Sie nicht alle Abschnitte Ihres Codes zu Maschinencode übersetzen, am Anfang des Programms (nur einmal), sie im Binärformat in Speicherblöcken speichern und dann diese Binärdateien bei Bedarf verwenden?

Das ist im Grunde genommen, wie die OpenGL-Shaders funktionieren, und ich finde das ziemlich einfach zu verwalten.

Der Hauptnachteil ist der Speicherverbrauch, da sowohl der Text als auch die binäre Darstellung der gleichen Skripte im Speicher vorhanden sind. Ich weiß nicht, ob das ein Problem für Sie ist. Falls doch, gibt es teilweise Lösungen, wie das Entladen der Quelltexte, sobald sie kompiliert sind.

0voto

old_timer Punkte 65318

Sagen wir, ich würde einen Arm-Kern modifizieren, um einige benutzerdefinierte Anweisungen hinzuzufügen, und die Operationen, die ich ausführen wollte, waren zur Kompilierzeit bekannt (kommen gleich zur Laufzeit).

Ich würde zum Beispiel Assembly verwenden:

.globl vecabc
vecabc:
   .word 0x7606E600 ;@ spezielle Anweisung
   bx lr

Oder inline damit, was auch immer die Inline-Syntax für deinen Compiler ist, es wird schwieriger, wenn du z. B. Prozessorregister verwenden musst, wo der C-Compiler die Register im Inline-Assembler ausfüllt und der Assembler diese Anweisungen zusammenbaut. Ich finde es einfacher, tatsächlich Assembler zu schreiben und die Wörter in den Anweisungsstrom einzufügen wie oben, nur der Compiler unterscheidet einige Bytes als Daten und einige Bytes als Anweisungen, der Kern wird sie in der Reihenfolge sehen, wie sie geschrieben sind.

Wenn du Dinge in Echtzeit erledigen musst, kannst du selbstmodifizierenden Code verwenden, auch hier verwende ich gerne Assembler, um zu trampen. Baue die Anweisungen, die du irgendwo im RAM ausführen möchtest, z. B. an der Adresse 0x20000000, und lass einen Trampolin sie aufrufen:

.globl tramp
tramp:
    bx r0 ;@ unter der Annahme, dass du ein Return in deinen Anweisungen codiert hast

Rufe es auf mit

tramp(0x20000000);

Ein anderer, damit zusammenhängender Weg oben besteht darin, den Assembler zu modifizieren, um die neuen Anweisungen hinzuzufügen, eine Syntax für diese Anweisungen zu erstellen. Dann kannst du einfach Assemblersprache oder Inline-Assemblersprache nach Belieben verwenden, du wirst den Compiler nicht dazu bringen, sie zu verwenden, ohne den Compiler zu modifizieren, was ein anderer Weg ist, nachdem der Assembler modifiziert wurde.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X