Ich habe versucht, die Leistung von Inline-Assemblersprache und C ++ - Code zu vergleichen, also habe ich eine Funktion geschrieben, die zwei Arrays der Größe 2000 100000 Mal hinzufügt. Hier ist der Code:
#define TIMES 100000
void calcuC(int *x,int *y,int length)
{
for(int i = 0; i < TIMES; i++)
{
for(int j = 0; j < length; j++)
x[j] += y[j];
}
}
void calcuAsm(int *x,int *y,int lengthOfArray)
{
__asm
{
mov edi,TIMES
start:
mov esi,0
mov ecx,lengthOfArray
label:
mov edx,x
push edx
mov eax,DWORD PTR [edx + esi*4]
mov edx,y
mov ebx,DWORD PTR [edx + esi*4]
add eax,ebx
pop edx
mov [edx + esi*4],eax
inc esi
loop label
dec edi
cmp edi,0
jnz start
};
}
Hier ist main()
:
int main() {
bool errorOccured = false;
setbuf(stdout,NULL);
int *xC,*xAsm,*yC,*yAsm;
xC = new int[2000];
xAsm = new int[2000];
yC = new int[2000];
yAsm = new int[2000];
for(int i = 0; i < 2000; i++)
{
xC[i] = 0;
xAsm[i] = 0;
yC[i] = i;
yAsm[i] = i;
}
time_t start = clock();
calcuC(xC,yC,2000);
// calcuAsm(xAsm,yAsm,2000);
// for(int i = 0; i < 2000; i++)
// {
// if(xC[i] != xAsm[i])
// {
// cout<<"xC["<<i<<"]="<<xC[i]<<" "<<"xAsm["<<i<<"]="<<xAsm[i]<<endl;
// errorOccured = true;
// break;
// }
// }
// if(errorOccured)
// cout<<"Error occurs!"<<endl;
// else
// cout<<"Works fine!"<<endl;
time_t end = clock();
// cout<<"time = "<<(float)(end - start) / CLOCKS_PER_SEC<<"\n";
cout<<"time = "<<end - start<<endl;
return 0;
}
Dann führe ich das Programm fünfmal aus, um die Zyklen des Prozessors zu erhalten, die als Zeit angesehen werden könnten. Jedes Mal rufe ich nur eine der oben genannten Funktionen auf.
Und hier kommt das Ergebnis.
Funktion der Baugruppenversion:
Debug Release
---------------
732 668
733 680
659 672
667 675
684 694
Average: 677
Funktion der C ++ - Version:
Debug Release
-----------------
1068 168
999 166
1072 231
1002 166
1114 183
Average: 182
Der C ++ - Code im Release-Modus ist fast 3,7-mal schneller als der Assembly-Code. Warum?
Ich denke, dass der Assembler-Code, den ich geschrieben habe, nicht so effektiv ist wie der von GCC generierte. Für einen gewöhnlichen Programmierer wie mich ist es schwierig, Code schneller zu schreiben als sein Gegner, der von einem Compiler generiert wurde. Bedeutet das, dass ich der Leistung der von meinen Händen geschriebenen Assemblersprache nicht vertrauen, mich auf C ++ konzentrieren und die Assemblersprache vergessen sollte?
quelle
Antworten:
Ja, meistens.
Zunächst gehen Sie von der falschen Annahme aus, dass eine einfache Sprache (in diesem Fall Assembly) immer schnelleren Code erzeugt als eine höhere Sprache (in diesem Fall C ++ und C). Es ist nicht wahr. Ist C-Code immer schneller als Java-Code? Nein, weil es eine andere Variable gibt: Programmierer. Die Art und Weise, wie Sie Code schreiben, und die Kenntnis der Architekturdetails haben großen Einfluss auf die Leistung (wie Sie in diesem Fall gesehen haben).
Sie können immer ein Beispiel erstellen, in dem handgefertigter Assembler-Code besser ist als kompilierter Code, aber normalerweise ist es ein fiktives Beispiel oder eine einzelne Routine (kein echtes Programm mit mehr als 500.000 Zeilen C ++ - Code). Ich denke, Compiler produzieren 95% -igen besseren Assembler-Code, und manchmal, nur in seltenen Fällen, müssen Sie Assembler-Code für wenige, kurze, häufig verwendete , leistungskritische Routinen schreiben oder wenn Sie auf Funktionen Ihrer bevorzugten Hochsprache zugreifen müssen nicht aussetzen. Möchten Sie einen Hauch dieser Komplexität? Lesen Sie diese großartige Antwort hier auf SO.
Warum das?
Erstens, weil Compiler Optimierungen vornehmen können, die wir uns nicht einmal vorstellen können (siehe diese kurze Liste ), und sie werden sie in Sekunden ausführen (wenn wir Tage brauchen ).
Wenn Sie in Assembly codieren, müssen Sie genau definierte Funktionen mit einer genau definierten Aufrufschnittstelle erstellen. Sie können jedoch die Optimierung des gesamten Programms und die Optimierung zwischen den Prozeduren berücksichtigen, wie z. B. Registerzuweisung , konstante Ausbreitung , Eliminierung gemeinsamer Unterausdrücke , Befehlsplanung und andere komplexe, nicht offensichtliche Optimierungen ( z. B. Polytopemodell ). In Bezug auf die RISC- Architektur haben sich die Leute vor vielen Jahren keine Gedanken mehr darüber gemacht (die Befehlsplanung ist beispielsweise von Hand sehr schwer abzustimmen ), und moderne CISC- CPUs haben sehr lange Pipelines auch.
Bei einigen komplexen Mikrocontrollern werden sogar Systembibliotheken in C anstatt in Assembly geschrieben, da ihre Compiler einen besseren (und einfach zu wartenden) endgültigen Code erzeugen.
Compiler können manchmal einige MMX / SIMDx-Anweisungen automatisch selbst verwenden, und wenn Sie sie nicht verwenden, können Sie sie einfach nicht vergleichen (andere Antworten haben Ihren Assembler-Code bereits sehr gut überprüft). Nur für Schleifen ist dies eine kurze Liste von Schleifenoptimierungen dessen, was üblicherweise von einem Compiler überprüft wird (glauben Sie, Sie könnten es selbst tun, wenn Ihr Zeitplan für ein C # -Programm festgelegt wurde?) Wenn Sie etwas in Assembly schreiben, ich Ich denke, Sie müssen zumindest einige einfache Optimierungen berücksichtigen . Das Schulbuchbeispiel für Arrays ist das Abrollen des Zyklus (seine Größe ist zur Kompilierungszeit bekannt). Mach es und führe deinen Test erneut aus.
Heutzutage ist es auch sehr ungewöhnlich, Assemblersprache aus einem anderen Grund zu verwenden: der Fülle verschiedener CPUs . Willst du sie alle unterstützen? Jedes hat eine spezifische Mikroarchitektur und einige spezifische Befehlssätze . Sie haben eine unterschiedliche Anzahl von Funktionseinheiten und Montageanweisungen sollten so angeordnet werden, dass sie alle beschäftigt sind . Wenn Sie in C schreiben, können Sie PGO verwenden, aber in der Assembly benötigen Sie ein umfassendes Wissen über diese spezifische Architektur (und überdenken und wiederholen Sie alles für eine andere Architektur ). Bei kleinen Aufgaben erledigt der Compiler dies normalerweise besser, und bei komplexen Aufgaben wird die Arbeit normalerweise nicht zurückgezahlt (undCompiler kann es sowieso besser machen).
Wenn Sie sich hinsetzen und sich Ihren Code ansehen, werden Sie wahrscheinlich feststellen, dass Sie mehr für die Neugestaltung Ihres Algorithmus als für die Übersetzung in Assembly gewinnen (lesen Sie diesen großartigen Beitrag hier auf SO ). Es gibt Optimierungen auf hoher Ebene (und Hinweise für den Compiler) können Sie effektiv anwenden, bevor Sie auf die Assemblersprache zurückgreifen müssen. Es ist wahrscheinlich erwähnenswert, dass Sie bei Verwendung von Intrinsics häufig einen Leistungsgewinn erzielen, den Sie suchen, und der Compiler die meisten seiner Optimierungen weiterhin ausführen kann.
Selbst wenn Sie einen 5- bis 10-mal schnelleren Assembler-Code erstellen können, sollten Sie Ihre Kunden fragen, ob sie lieber eine Woche Ihrer Zeit bezahlen oder eine 50-Dollar-schnellere CPU kaufen möchten . Meistens ist eine extreme Optimierung (und insbesondere in LOB-Anwendungen) für die meisten von uns einfach nicht erforderlich.
quelle
Ihr Assembler-Code ist nicht optimal und kann verbessert werden:
loop
Anweisung, von der bekannt ist, dass sie auf den meisten modernen CPUs sehr langsam ist (möglicherweise aufgrund der Verwendung eines alten Montagebuchs *).Wenn Sie also Ihre Fähigkeiten in Bezug auf Assembler nicht wesentlich verbessern, ist es für Sie nicht sinnvoll, Assembler-Code für die Leistung zu schreiben.
* Natürlich weiß ich nicht, ob Sie wirklich die
loop
Anweisung aus einem alten Versammlungsbuch erhalten haben. Aber Sie sehen es fast nie im Code der realen Welt, da jeder Compiler da draußen klug genug ist, um nicht zu emittierenloop
. Sie sehen es nur in IMHO schlechten und veralteten Büchern.quelle
loop
(und viele "veraltete" Anweisungen) ausgeben, wenn Sie für die Größe optimierenNoch bevor wir uns mit Assembly befassen, gibt es Code-Transformationen, die auf einer höheren Ebene existieren.
kann über Loop Rotation umgewandelt werden :
Das ist viel besser, was die Speicherlokalität betrifft.
Dies könnte weiter optimiert werden.
a += b
X-mal zu machen ist gleichbedeutenda += X * b
damit:Es scheint jedoch, dass mein Lieblingsoptimierer (LLVM) diese Transformation nicht durchführt.
[Bearbeiten] Ich habe festgestellt, dass die Transformation durchgeführt wird, wenn wir das
restrict
Qualifikationsmerkmal fürx
und hatteny
. In der Tat ohne diese Einschränkung,x[j]
undy[j]
könnte Alias auf den gleichen Ort, der diese Transformation fehlerhaft macht. [Bearbeitung beenden]Jedenfalls ist dies meiner Meinung nach die optimierte C-Version. Schon ist es viel einfacher. Basierend darauf ist hier mein Riss bei ASM (ich lasse Clang ihn generieren, ich bin nutzlos darin):
Ich fürchte, ich verstehe nicht, woher all diese Anweisungen kommen, aber Sie können immer Spaß haben und versuchen, zu sehen, wie sie verglichen werden ... aber ich würde immer noch die optimierte C-Version anstelle der Assembly-Version im Code verwenden. viel tragbarer.
quelle
x
undy
. Das heißt, der Compiler kann nicht sicher sein, dass für allei,j
in[0, length)
uns habenx + i != y + j
. Bei Überlappungen ist eine Optimierung nicht möglich. Die C-Sprache führte dasrestrict
Schlüsselwort ein, um dem Compiler mitzuteilen, dass zwei Zeiger keinen Alias haben können. Dies funktioniert jedoch nicht für Arrays, da sie sich auch dann überlappen können, wenn sie nicht genau Alias sind.__restrict
). SSE2 ist die Basis für x86-64 und mit dem Mischen kann SSE2 2x 32-Bit-Multiplikationen gleichzeitig ausführen (64-Bit-Produkte werden erzeugt, daher das Mischen, um die Ergebnisse wieder zusammenzusetzen). godbolt.org/z/r7F_uo . (SSE4.1 wird benötigt fürpmulld
: gepackte 32x32 => 32-Bit-Multiplikation). GCC hat einen tollen Trick, konstante ganzzahlige Multiplikatoren in Shift / Add (und / oder Subtrahieren) umzuwandeln, was für Multiplikatoren mit wenigen gesetzten Bits gut ist. Clangs Shuffle-lastiger Code wird einen Engpass beim Shuffle-Durchsatz auf Intel-CPUs verursachen.Kurze Antwort: ja.
Lange Antwort: Ja, es sei denn, Sie wissen wirklich, was Sie tun, und haben einen Grund dafür.
quelle
Ich habe meinen ASM-Code korrigiert:
Ergebnisse für Release-Version:
Der Assemblycode im Release-Modus ist fast zweimal schneller als in C ++.
quelle
xmm0
anstelle vonmm0
), erhalten Sie eine weitere Beschleunigung um den Faktor zwei ;-)paddd xmm
(nachdem er auf Überlappungen zwischenx
und geprüft haty
, weil Sie diese nicht verwendet habenint *__restrict x
). Zum Beispiel macht gcc das: godbolt.org/z/c2JG0- . Oder nach dem Inliningmain
sollte es nicht auf Überlappung prüfen müssen, da es die Zuordnung sehen und beweisen kann, dass sie sich nicht überlappen. (Bei einigen x86-64-Implementierungen wird auch eine 16-Byte-Ausrichtung angenommen, was bei der eigenständigen Definition nicht der Fall ist.) Und wenn Sie mit kompilierengcc -O3 -march=native
, können Sie 256-Bit oder 512-Bit erhalten Vektorisierung.Ja, genau das bedeutet es und es gilt für jede Sprache. Wenn Sie nicht wissen, wie man effizienten Code in Sprache X schreibt, sollten Sie Ihrer Fähigkeit, effizienten Code in X zu schreiben, nicht vertrauen. Wenn Sie also effizienten Code wünschen, sollten Sie eine andere Sprache verwenden.
Die Montage reagiert besonders empfindlich darauf, denn was Sie sehen, ist das, was Sie bekommen. Sie schreiben die spezifischen Anweisungen, die die CPU ausführen soll. Bei Hochsprachen gibt es zwischendurch einen Compiler, der Ihren Code transformieren und viele Ineffizienzen beseitigen kann. Mit der Montage sind Sie auf sich allein gestellt.
quelle
Der einzige Grund, heutzutage Assemblersprache zu verwenden, besteht darin, einige Funktionen zu verwenden, auf die die Sprache nicht zugreifen kann.
Dies gilt für:
Aktuelle Compiler sind jedoch ziemlich schlau. Sie können sogar zwei separate Anweisungen wie
d = a / b; r = a % b;
durch eine einzige Anweisung ersetzen , die die Division und den Rest auf einmal berechnet, wenn sie verfügbar sind, selbst wenn C keinen solchen Operator hat.quelle
Es ist wahr, dass ein moderner Compiler bei der Codeoptimierung hervorragende Arbeit leistet, aber ich möchte Sie trotzdem ermutigen, weiterhin Assembler zu lernen.
Zunächst einmal lassen Sie sich davon eindeutig nicht einschüchtern , das ist ein großartiges Plus. Als Nächstes sind Sie auf dem richtigen Weg, indem Sie ein Profil erstellen, um Ihre Geschwindigkeitsannahmen zu validieren oder zu verwerfen . Sie bitten erfahrene Personen und Sie um Input haben das größte Optimierungswerkzeug, das der Menschheit bekannt ist: ein Gehirn .
Mit zunehmender Erfahrung erfahren Sie, wann und wo Sie es verwenden müssen (normalerweise die engsten, innersten Schleifen in Ihrem Code, nachdem Sie auf algorithmischer Ebene tief optimiert haben).
Als Inspiration würde ich Ihnen empfehlen , die Artikel von Michael Abrash nachzuschlagen (wenn Sie nichts von ihm gehört haben, ist er ein Optimierungsguru; er hat sogar mit John Carmack bei der Optimierung des Quake-Software-Renderers zusammengearbeitet!)
quelle
Ich habe den ASM-Code geändert:
Ergebnisse für Release-Version:
Der Assemblycode im Release-Modus ist fast viermal schneller als in C ++. IMHo hängt die Geschwindigkeit des Assembler-Codes vom Programmierer ab
quelle
shr ecx,2
ist überflüssig, weil die Array-Länge bereits inint
und nicht in Byte angegeben ist. Sie erreichen also im Grunde die gleiche Geschwindigkeit. Sie könnten diepaddd
Antwort von Harold versuchen , dies wird wirklich schneller sein.Es ist ein sehr interessantes Thema!
Ich habe das MMX von SSE in Sashas Code geändert.
Hier sind meine Ergebnisse:
Der Assemblycode mit SSE ist fünfmal schneller als mit C ++
quelle
Die meisten Hochsprachen-Compiler sind sehr optimiert und wissen, was sie tun. Sie können versuchen, den Disassemble-Code zu sichern und ihn mit Ihrer nativen Assembly zu vergleichen. Ich glaube, Sie werden einige nette Tricks sehen, die Ihr Compiler verwendet.
Nur zum Beispiel, auch wenn ich nicht mehr sicher bin, ob es richtig ist :):
Tun:
kosten mehr Zyklen als
das macht das gleiche.
Der Compiler kennt alle diese Tricks und verwendet sie.
quelle
Der Compiler hat dich geschlagen. Ich werde es versuchen, aber ich werde keine Garantien geben. Ich gehe davon aus, dass die „Multiplikation“ von TIMES gemeint ist es ein relevanter Performance - Test zu machen, dass
y
undx
bin 16 ausgerichtet, und daslength
ist ein nicht-Null Vielfachen von 4. Das ist wahrscheinlich alles wahr sowieso.Wie gesagt, ich gebe keine Garantie. Aber ich bin überrascht, wenn es viel schneller geht - der Engpass hier ist der Speicherdurchsatz, auch wenn alles ein L1-Treffer ist.
quelle
mov ecx, length, lea ecx,[ecx*4], mov eax,16... add ecx,eax
[esi + ecx] ändern und dann überall verwenden, vermeiden Sie 1 Zyklusstillstand pro Befehl, der die Schleifenlose beschleunigt. (Wenn Sie den neuesten Skylake haben, gilt dies nicht). Das Hinzufügen von reg, reg macht die Schleife nur enger, was möglicherweise hilft oder nicht.Die blinde Implementierung des exakt gleichen Algorithmus Befehl für Befehl in der Assembly ist garantiert langsamer als das, was der Compiler tun kann.
Dies liegt daran, dass selbst die kleinste Optimierung, die der Compiler vornimmt, besser ist als Ihr starrer Code, ohne dass eine Optimierung erfolgt.
Natürlich ist es möglich, den Compiler zu schlagen, besonders wenn es sich um einen kleinen, lokalisierten Teil des Codes handelt. Ich musste es sogar selbst tun, um eine Ca. 4x beschleunigen, aber in diesem Fall müssen wir uns stark auf gute Kenntnisse der Hardware und zahlreiche, scheinbar kontraintuitive Tricks verlassen.
quelle
Als Compiler würde ich eine Schleife mit einer festen Größe für viele Ausführungsaufgaben ersetzen.
wird herstellen
und irgendwann wird es wissen, dass "a = a + 0;" ist nutzlos, so dass diese Zeile entfernt wird. Hoffentlich etwas in Ihrem Kopf, das jetzt bereit ist, einige Optimierungsoptionen als Kommentar beizufügen. All diese sehr effektiven Optimierungen beschleunigen die kompilierte Sprache.
quelle
a
es nicht volatil ist, besteht eine gute Chance, dass der Compiler diesint a = 13;
von Anfang an tut .Genau das bedeutet es. Überlassen Sie die Mikrooptimierungen dem Compiler.
quelle
Ich liebe dieses Beispiel, weil es eine wichtige Lektion über Low-Level-Code zeigt. Ja, Sie können Assemblys schreiben, die so schnell sind wie Ihr C-Code. Dies ist tautologisch wahr, aber nicht unbedingt Mittel nichts. Klar jemand kann, sonst wäre der Assembler nicht die entsprechenden Optimierungen kennen.
Ebenso gilt das gleiche Prinzip, wenn Sie die Hierarchie der Sprachabstraktion aufsteigen. Ja, Sie können einen Parser in C schreiben, der so schnell ist wie ein schnelles und schmutziges Perl-Skript, und das tun viele Leute. Das heißt aber nicht, dass Ihr Code schnell ist, weil Sie C verwendet haben. In vielen Fällen führen die übergeordneten Sprachen Optimierungen durch, die Sie möglicherweise noch nie in Betracht gezogen haben.
quelle
In vielen Fällen kann die optimale Ausführung einer Aufgabe vom Kontext abhängen, in dem die Aufgabe ausgeführt wird. Wenn eine Routine in Assemblersprache geschrieben ist, ist es im Allgemeinen nicht möglich, die Reihenfolge der Anweisungen je nach Kontext zu variieren. Betrachten Sie als einfaches Beispiel die folgende einfache Methode:
Ein Compiler für 32-Bit-ARM-Code würde ihn angesichts der obigen Ausführungen wahrscheinlich wie folgt rendern:
oder vielleicht
Das könnte im handmontierten Code leicht optimiert werden, wie entweder:
oder
Beide von Hand zusammengestellten Ansätze würden 12 Bytes Code-Speicherplatz anstelle von 16 erfordern; Letzteres würde ein "Laden" durch ein "Hinzufügen" ersetzen, das auf einem ARM7-TDMI zwei Zyklen schneller ausführen würde. Wenn der Code in einem Kontext ausgeführt werden würde, in dem r0 nicht bekannt / egal ist, wären die Assembler-Versionen daher etwas besser als die kompilierte Version. Angenommen, der Compiler wusste, dass ein Register [z. B. r5] einen Wert enthalten würde, der innerhalb von 2047 Bytes der gewünschten Adresse 0x40001204 [z. B. 0x40001000] liegt, und wusste weiter, dass ein anderes Register [z. B. r7] ausgeführt wird um einen Wert zu halten, dessen niedrige Bits 0xFF waren. In diesem Fall könnte ein Compiler die C-Version des Codes optimieren, um einfach:
Viel kürzer und schneller als selbst der handoptimierte Assembler-Code. Angenommen, set_port_high ist im Kontext aufgetreten:
Überhaupt nicht unplausibel beim Codieren für ein eingebettetes System. Wenn
set_port_high
es in Assembly-Code geschrieben ist, müsste der Compiler r0 (das den Rückgabewert von enthältfunction1
) an eine andere Stelle verschieben, bevor er den Assembly-Code aufruft, und diesen Wert anschließend wieder auf r0 verschieben (dafunction2
der erste Parameter in r0 erwartet wird). Der "optimierte" Assembler-Code würde also fünf Anweisungen benötigen. Selbst wenn der Compiler keine Register kennen würde, die die Adresse oder den zu speichernden Wert enthalten, würde seine Version mit vier Befehlen (die er anpassen könnte, um alle verfügbaren Register zu verwenden - nicht unbedingt r0 und r1) die "optimierte" Assembly schlagen -sprachige Version. Wenn der Compiler die erforderlichen Adressen und Daten in r5 und r7 hätte, wie zuvor beschrieben,function1
würde er diese Register nicht ändern und könnte sie somit ersetzenset_port_high
mitstrb
Anweisung -vier Anweisungen kleiner und schneller als der "handoptimierte" Assembler-Code.Beachten Sie, dass handoptimierter Assembler-Code einen Compiler häufig übertreffen kann, wenn der Programmierer den genauen Programmablauf kennt, Compiler jedoch in Fällen glänzen, in denen ein Teil des Codes geschrieben wurde, bevor sein Kontext bekannt ist, oder wenn ein Teil des Quellcodes vorhanden ist aus mehreren Kontexten aufgerufen [if
set_port_high
der Compiler an fünfzig verschiedenen Stellen im Code verwendet wird, kann er unabhängig für jeden von ihnen entscheiden, wie er am besten erweitert werden soll].Im Allgemeinen würde ich vorschlagen, dass die Assemblersprache in den Fällen, in denen jeder Code aus einer sehr begrenzten Anzahl von Kontexten heraus aufgerufen werden kann, die größten Leistungsverbesserungen erzielt und die Leistung an Orten beeinträchtigt, an denen ein Teil von Code vorhanden ist Code kann aus vielen verschiedenen Kontexten betrachtet werden. Interessanterweise (und bequemerweise) sind die Fälle, in denen die Montage für die Leistung am vorteilhaftesten ist, häufig diejenigen, in denen der Code am einfachsten und am einfachsten zu lesen ist. Die Stellen, an denen Assembler-Code zu einem Durcheinander wird, sind häufig diejenigen, an denen das Schreiben in Assembler den geringsten Leistungsvorteil bietet.
[Kleiner Hinweis: Es gibt einige Stellen, an denen Assembler-Code verwendet werden kann, um ein hyperoptimiertes, klebriges Durcheinander zu erzielen. Zum Beispiel musste ein Code, den ich für den ARM gemacht habe, ein Wort aus dem RAM abrufen und eine von ungefähr zwölf Routinen basierend auf den oberen sechs Bits des Werts ausführen (viele Werte sind derselben Routine zugeordnet). Ich glaube, ich habe diesen Code so optimiert:
Das Register r8 enthielt immer die Adresse der Hauptversandtabelle (innerhalb der Schleife, in der der Code 98% seiner Zeit verbringt, wurde er nie für einen anderen Zweck verwendet); Alle 64 Einträge beziehen sich auf Adressen in den 256 Bytes davor. Da die primäre Schleife in den meisten Fällen ein hartes Ausführungszeitlimit von etwa 60 Zyklen hatte, war das Abrufen und Versenden von neun Zyklen sehr hilfreich, um dieses Ziel zu erreichen. Die Verwendung einer Tabelle mit 256 32-Bit-Adressen wäre einen Zyklus schneller gewesen, hätte jedoch 1 KB sehr wertvollen Arbeitsspeicher verschlungen [Flash hätte mehr als einen Wartezustand hinzugefügt]. Die Verwendung von 64 32-Bit-Adressen hätte das Hinzufügen eines Befehls zum Maskieren einiger Bits aus dem abgerufenen Wort erforderlich gemacht und immer noch 192 Bytes mehr verschlungen als die Tabelle, die ich tatsächlich verwendet habe. Die Verwendung der Tabelle der 8-Bit-Offsets ergab einen sehr kompakten und schnellen Code. aber nicht etwas, von dem ich erwarten würde, dass ein Compiler es jemals erfinden würde; Ich würde auch nicht erwarten, dass ein Compiler ein Register "Vollzeit" für das Halten der Tabellenadresse reserviert.
Der obige Code wurde als eigenständiges System ausgeführt. Es konnte regelmäßig C-Code aufrufen, jedoch nur zu bestimmten Zeiten, zu denen die Hardware, mit der es kommunizierte, alle 16 ms für zwei Intervalle von ungefähr einer Millisekunde sicher in den Ruhezustand versetzt werden konnte.
quelle
In letzter Zeit haben alle Geschwindigkeitsoptimierungen, die ich vorgenommen habe, gehirngeschädigten langsamen Code durch nur vernünftigen Code ersetzt. Da die Geschwindigkeit jedoch sehr wichtig war und ich mich ernsthaft bemühte, etwas schnell zu machen, war das Ergebnis immer ein iterativer Prozess, bei dem jede Iteration mehr Einblick in das Problem gab und Wege fand, das Problem mit weniger Vorgängen zu lösen. Die endgültige Geschwindigkeit hing immer davon ab, wie viel Einblick ich in das Problem bekam. Wenn ich zu irgendeinem Zeitpunkt Assembler-Code oder C-Code verwendet hätte, der überoptimiert war, hätte der Prozess der Suche nach einer besseren Lösung gelitten und das Endergebnis wäre langsamer.
quelle
Wenn ich in ASM codiere, reorganisiere ich die Anweisungen manuell, damit die CPU mehr davon parallel ausführen kann, wenn dies logisch möglich ist. Ich verwende kaum RAM, wenn ich in ASM codiere, zum Beispiel: In ASM können mehr als 20000 Codezeilen vorhanden sein, und ich habe Push / Pop noch nie verwendet.
Sie könnten möglicherweise in die Mitte des Opcodes springen, um den Code und das Verhalten selbst zu ändern, ohne die mögliche Strafe eines sich selbst ändernden Codes. Der Zugriff auf Register erfordert 1 Tick (manchmal 0,25 Ticks) der CPU. Der Zugriff auf den RAM kann Hunderte dauern.
Bei meinem letzten ASM-Abenteuer habe ich den RAM nie zum Speichern einer Variablen verwendet (für Tausende von ASM-Zeilen). ASM könnte möglicherweise unvorstellbar schneller als C ++ sein. Aber es hängt von vielen variablen Faktoren ab, wie zum Beispiel:
Ich lerne jetzt C # und C ++, weil mir klar wurde, dass Produktivität wichtig ist !! In der Freizeit können Sie versuchen, die schnellsten vorstellbaren Programme nur mit reinem ASM zu erstellen. Aber um etwas zu produzieren, verwenden Sie eine Hochsprache.
Zum Beispiel verwendete das letzte Programm, das ich codierte, JS und GLSL, und ich bemerkte nie ein Leistungsproblem, selbst wenn ich über JS sprach, das langsam ist. Dies liegt daran, dass das bloße Konzept der Programmierung der GPU für 3D die Geschwindigkeit der Sprache, die die Befehle an die GPU sendet, nahezu irrelevant macht.
Die Geschwindigkeit des Monteurs allein auf dem blanken Metall ist unwiderlegbar. Könnte es in C ++ noch langsamer sein? - Dies kann daran liegen, dass Sie Assembler-Code mit einem Compiler schreiben, der zunächst keinen Assembler verwendet.
Mein persönlicher Rat ist, niemals Assembler-Code zu schreiben, wenn Sie dies vermeiden können, obwohl ich Assembler liebe.
quelle
Alle Antworten hier scheinen einen Aspekt auszuschließen: Manchmal schreiben wir keinen Code, um ein bestimmtes Ziel zu erreichen, sondern nur zum Spaß . Es mag nicht wirtschaftlich sein, die Zeit dafür zu investieren, aber es gibt wohl keine größere Befriedigung, als das schnellste vom Compiler optimierte Code-Snippet in der Geschwindigkeit mit einer manuell gerollten asm-Alternative zu schlagen.
quelle
Ein C ++ - Compiler würde nach der Optimierung auf Organisationsebene Code erzeugen, der die integrierten Funktionen der Ziel-CPU verwendet. HLL wird Assembler aus mehreren Gründen niemals überholen oder übertreffen. 1.) HLL wird kompiliert und mit Accessor-Code, Grenzprüfung und möglicherweise eingebauter Speicherbereinigung (früher Adressierung des Bereichs im OOP-Manierismus) ausgegeben, wobei alle Zyklen (Flips und Flops) erforderlich sind. HLL leistet heutzutage hervorragende Arbeit (einschließlich neuerer C ++ - und anderer wie GO), aber wenn sie den Assembler (nämlich Ihren Code) übertreffen, müssen Sie die CPU-Dokumentation konsultieren. Vergleiche mit schlampigem Code sind mit Sicherheit nicht schlüssig und kompiliert langsam wie Assembler HLL abstrahiert die Details und entfernt sie nicht. Andernfalls wird Ihre App nicht ausgeführt, wenn sie vom Host-Betriebssystem überhaupt erkannt wird.
Die meisten Assembler-Codes (hauptsächlich Objekte) werden als "kopflos" ausgegeben, um sie in andere ausführbare Formate aufzunehmen, wobei weitaus weniger Verarbeitung erforderlich ist. Daher ist sie viel schneller, aber weitaus unsicherer. Wenn eine ausführbare Datei vom Assembler (NAsm, YAsm usw.) ausgegeben wird, wird sie immer noch schneller ausgeführt, bis sie in ihrer Funktionalität vollständig mit dem HLL-Code übereinstimmt. Die Ergebnisse können dann genau abgewogen werden.
Das Aufrufen eines Assembler-basierten Codeobjekts aus HLL in einem beliebigen Format erhöht den Verarbeitungsaufwand zusätzlich zu Speicherplatzaufrufen, bei denen global zugewiesener Speicher für variable / konstante Datentypen verwendet wird (dies gilt sowohl für LLL als auch für HLL). Denken Sie daran, dass die endgültige Ausgabe die CPU letztendlich als API und Abi in Bezug auf die Hardware (Opcode) verwendet und sowohl Assembler als auch "HLL-Compiler" im Wesentlichen / grundlegend identisch sind, wobei die einzig wahre Ausnahme die Lesbarkeit (grammatikalisch) ist.
Die Hello World-Konsolenanwendung in Assembler mit FAsm ist 1,5 KB groß (und dies ist in Windows unter FreeBSD und Linux sogar noch kleiner) und übertrifft alles, was GCC an seinem besten Tag herausbringen kann. Gründe sind implizites Auffüllen mit Nops, Zugriffsüberprüfung und Grenzüberprüfung, um nur einige zu nennen. Das eigentliche Ziel sind saubere HLL-Bibliotheken und ein optimierbarer Compiler, der auf eine "Hardcore" -Methode abzielt, und die meisten tun dies heutzutage (endlich). GCC ist nicht besser als YAsm - es sind die Codierungspraktiken und das Verständnis des betreffenden Entwicklers, und die "Optimierung" erfolgt nach unerfahrener Erkundung und Zwischenschulung und Erfahrung.
Compiler müssen für die Ausgabe im selben Opcode wie ein Assembler verknüpft und zusammengesetzt werden, da diese Codes nur von einer CPU ausgenommen werden (CISC oder RISC [auch PIC]). YAsm optimierte und bereinigte viel auf frühem NAsm, was letztendlich die gesamte Ausgabe dieses Assemblers beschleunigte, aber selbst dann produziert YAsm wie NAsm im Auftrag des Entwicklers ausführbare Dateien mit externen Abhängigkeiten, die auf Betriebssystembibliotheken abzielen, sodass die Laufleistung variieren kann. Zum Schluss befindet sich C ++ an einem Punkt, der für mehr als 80 Prozent unglaublich und weitaus sicherer als Assembler ist, insbesondere im kommerziellen Bereich ...
quelle
ld
macht jedoch keinen Unterschied, es sei denn, Sie versuchen, die Dateigröße (nicht nur die Größe der Datei) wirklich zu optimieren Textsegment). Siehe ein Wirbelwind-Tutorial zum Erstellen von wirklich teensy ELF Executables für Linux .std::vector
kompilieren im Debug-Modus. C ++ - Arrays sind nicht so. Compiler können Inhalte zur Kompilierungszeit überprüfen. Wenn Sie jedoch keine zusätzlichen Härtungsoptionen aktivieren, erfolgt keine Laufzeitprüfung. Siehe zum Beispiel eine Funktion, die die ersten 1024 Elemente einesint array[]
Args inkrementiert . Die asm-Ausgabe hat keine Laufzeitprüfungen: godbolt.org/g/w1HF5t . Alles was es bekommt ist ein Zeiger inrdi
, keine Größeninformationen. Es ist Sache des Programmierers, undefiniertes Verhalten zu vermeiden, indem er es niemals mit einem Array aufruft, das kleiner als 1024 ist.new
, manuell löschen mitdelete
, keine Überprüfung der Grenzen). Sie können C ++ verwenden, um beschissenen, aufgeblähten asm / Maschinencode zu erzeugen (wie die meisten Softwareprogramme), aber das ist der Fehler des Programmierers, nicht der von C ++. Sie können sogaralloca
Stapelspeicher als Array zuweisen.g++ -O3
Code zur Überprüfung von Grenzen für ein einfaches Array oder für das, was Sie sonst noch tun. C ++ macht es viel einfacher, aufgeblähte Binärdateien zu generieren (und tatsächlich müssen Sie vorsichtig sein , wenn Sie keine Leistung anstreben), aber es ist nicht buchstäblich unvermeidlich. Wenn Sie verstehen, wie C ++ zu asm kompiliert wird, können Sie Code erhalten, der nur etwas schlechter ist, als Sie von Hand schreiben könnten, aber mit Inlining und konstanter Weitergabe über einen größeren Maßstab, als Sie von Hand verwalten könnten.Die Montage könnte schneller sein, wenn Ihr Compiler viel generiert OO- Supportcode generiert.
Bearbeiten:
An Downvoter: Das OP schrieb: "Soll ich ... mich auf C ++ konzentrieren und die Assemblersprache vergessen?" und ich stehe zu meiner Antwort. Sie müssen immer den Code im Auge behalten, den OO generiert, insbesondere wenn Sie Methoden verwenden. Wenn Sie die Assemblersprache nicht vergessen, überprüfen Sie regelmäßig die Assemblierung, die Ihr OO-Code generiert. Ich glaube, dies ist ein Muss für das Schreiben leistungsfähiger Software.
Tatsächlich bezieht sich dies auf den gesamten kompilierbaren Code, nicht nur auf OO.
quelle