2012-10-07 5 views
24

Frage:Kosten Push vs. mov (Stack vs. Naharbeitsspeicher) und der Aufwand für Funktionsaufrufe

Ist Zugriff auf den Stapel der gleichen Geschwindigkeit wie Speicher zugreifen?

Zum Beispiel könnte ich wählen, etwas Arbeit innerhalb des Stapels zu tun, oder ich könnte Arbeit direkt mit einem markierten Speicherort im Speicher tun.

Also, speziell: ist push ax die gleiche Geschwindigkeit wie mov [bx], ax? Ebenso ist pop ax die gleiche Geschwindigkeit wie mov ax, [bx]? (Nehmen bx einen Ort in near Speicher hält.)

Motivation für Frage:

Es ist in C gemeinsamen triviale Funktionen abzuschrecken, die Parameter nehmen.

Ich dachte immer, das ist, weil nicht nur die Parameter auf den Stapel geschoben werden müssen und dann vom Stapel verschwinden, sobald die Funktion zurückgibt, sondern auch weil der Funktionsaufruf den Kontext der CPU bewahren muss, was mehr Stapel bedeutet Verwendung.

Aber vorausgesetzt, man weiß die Antwort auf die Überschrift Frage, sollte es möglich sein, den Overhead zu quantifizieren, den die Funktion verwendet, um sich selbst (push/pop/preserve Kontext, etc.) in Bezug auf eine äquivalente Anzahl von direkten Speicherzugriffe. Daher die Schlagzeile.


( bearbeiten: Klarstellung: near oben verwendet wird, wie in der segmented memory model von 16-Bit-x86-Architektur zu far gegenüber.)

+5

Wow. Ich bin ein Forscher. Ich habe gerade eine gute, nicht-n00b Frage zu StackOverflow gefunden. Erlebe meine Erkundung mit Champagner und einer Aufwertung! –

+1

Ich dachte immer Push/Pop-Aufruf der Dekrement/Inkrement-Operationen auf ESP als Overhead im Vergleich zu mov .... aber ich denke, es sollte viel mehr zu sein. – loxxy

Antwort

17

Heute Ihre C-Compiler Sie austricksen können. Es kann einfache Inline-Funktionen enthalten, und wenn dies der Fall ist, gibt es keinen Funktionsaufruf oder -rückgabe und möglicherweise werden keine zusätzlichen Stapelmanipulationen im Zusammenhang mit der Übergabe und dem Zugriff auf formale Funktionsparameter (oder eine äquivalente Operation, wenn die Funktion inline ist) ausgeführt verfügbare Register sind erschöpft), wenn alles in Registern ausgeführt werden kann oder, noch besser, wenn das Ergebnis ein konstanter Wert ist und der Compiler das sehen und ausnutzen kann.

Funktionsaufrufe können auf modernen CPUs relativ billig (aber nicht unbedingt kostenlos) sein, wenn sie wiederholt werden und wenn es einen separaten Befehlscache und verschiedene Vorhersagemechanismen gibt, die bei der effizienten Codeausführung helfen.

Ansonsten würde ich erwarten, dass die Performance-Implikationen der Wahl "local var vs global var" von den Speichernutzungsmustern abhängen. Wenn in der CPU ein Speichercache vorhanden ist, befindet sich der Stapel wahrscheinlich in diesem Cache, es sei denn, Sie ordnen ihm große Arrays oder Strukturen zu, heben ihn auf oder haben tiefe Funktionsaufrufe oder tiefe Rekursionen, was Cache-Misses verursacht. Wenn auf die globale Variable von Interesse oft zugegriffen wird oder wenn oft auf ihre Nachbarn zugegriffen wird, würde ich erwarten, dass diese Variable auch die meiste Zeit im Cache ist. Auch wenn Sie auf große Speicherbereiche zugreifen, die nicht in den Cache passen, haben Sie Cache-Fehlschläge und möglicherweise reduzierte Leistung (möglicherweise, weil es möglicherweise eine bessere, Cache-freundliche Art gibt, um das zu tun, was Sie tun) möchte tun).

Wenn die Hardware ziemlich dumm ist (keine oder kleine Caches, keine Vorhersage, keine Befehlsneuordnung, keine spekulative Ausführung, nichts), wollen Sie den Speicherdruck und die Anzahl der Funktionsaufrufe deutlich reduzieren, weil jeder zählt .

Noch ein weiterer Faktor ist Befehlslänge und Decodierung. Anweisungen zum Zugreifen auf einen Ort auf dem Stapel (relativ zu dem Stapelzeiger) können kürzer sein als Befehle zum Zugreifen auf einen beliebigen Speicherort an einer gegebenen Adresse. Kürzere Anweisungen können schneller dekodiert und ausgeführt werden.

Ich würde sagen, dass es für alle Fälle keine definitive Antwort ist, weil die Leistung abhängt:

  • Hardware
  • Compiler
  • Ihr Programm und seine Speichermuster Zugriff
+0

Danke Alexey - guter Punkt über lokale var (stack, correct?) Vs. globale var (Speicher, richtig?) - hatte nicht so gedacht. –

+0

Re: Beliebiger Speicherort - deshalb beschränke ich die Betrachtung auf "nahe" Speicher. Macht das einen Unterschied? –

+0

Re: Ihr Punkt über unterschiedliche Befehlslänge & Dekodierzeit - meinst du einen Unterschied zwischen zum Beispiel 'mov [bx], ax' vs.' mov [loc], ax', unter der Annahme 'loc equ 0xfffd' (oder einige nahe Offset)? (Danke, wie immer, für Ihre wirklich tollen Antworten !!) –

11

Für den Uhr-Zyklus-neugierig ...

Für diejenigen, die bestimmte Taktzyklen sehen möchten, sind instruction/latency tables für eine Vielzahl von modernen x86 und x86-64 CPUs verfügbar here (Danke an hirschhornsalz für das Aufzeigen dieser).

Sie dann erhält, auf einem Pentium 4-Chip:

  • push ax und mov [bx], ax (rot eingerahmt) sind nahezu identisch in ihrer Effizienz mit identischen Latenzen und Durchsatz.
  • pop ax und mov ax, [bx] (blau eingerahmt) sind ähnlich effizient, mit identischen Durch trotz mov ax, [bx] zweimal die Latenz der pop ax
mit

Pentium 4 Instruction Timing Table

Was die Nachfolge-Frage in den Kommentaren (3 Kommentar):

  • indirekt (dh mov [bx], ax) als die direkte Adressierung nicht wesentlich verschieden ist Adressierung (dh mov [loc], ax), wobei loc eine Variable ist, die einen unmittelbaren Wert, z. loc equ 0xfffd.

Fazit: des Kombinieren mit Alexey's thorough answer, und es gibt einen ziemlich soliden Fall für die Effizienz des Stapel mit und läßt die Compiler entscheiden, wann eine Funktion inlined werden soll.

(Randbemerkung: In der Tat, auch so weit zurück wie die 8086 aus dem Jahr 1978, war den Stack noch nicht weniger effizient als mov die in dem Speicher entspricht, wie aus these old 8086 instruction timing tables zu sehen.)


Understanding Latency & Durchsatz

Ein bisschen mehr benötigt werden, um Timing-Tabellen für moderne CPUs zu verstehen.Diese sollten helfen: