Handbuch zur dynamischen Ablaufverfolgung in Solaris

Tail-Call-Optimierung

Wenn eine Funktion mit dem Aufruf einer anderen Funktion endet, kann der Compiler eine Tail-Call-Optimierung durchführen, die darin besteht, dass die aufgerufene Funktion den Stack-Frame des Aufrufers wieder verwendet. Dieses Verfahren findet in der SPARC-Architektur sehr häufige Anwendung, wo der Compiler das Registerfenster des Aufrufers in der aufgerufenen Funktion wieder verwendet, um den Druck auf die Registerfenster zu minimieren.

Diese Optimierung bewirkt, dass der return-Prüfpunkt der aufrufenden Funktion vor dem entry-Prüfpunkt der aufgerufenen Funktion ausgelöst wird. Diese Reihenfolge kann durchaus Verwirrung schaffen. Wenn Sie beispielsweise alle aus einer bestimmten Funktion aufgerufenen Funktionen und alle von ihr aufgerufenen Funktionen aufzeichnen möchten, könnten Sie das folgende Skript verwenden:

fbt::foo:entry
{
	self->traceme = 1;
}

fbt:::entry
/self->traceme/
{
	printf("called %s", probefunc);
}

fbt::foo:return
/self->traceme/
{
	self->traceme = 0;
}

Wenn jedoch foo() mit einem optimierten Endaufruf (Tail-Call) endet, wird die aus der Endposition rekursiv aufgerufene Funktion einschließlich aller Funktionen, die sie aufruft, nicht erfasst. Der Kernel kann nicht nach Bedarf dynamisch deoptimiert werden, und DTrace soll hier keine falschen Tatsachen über die Codestruktur vortäuschen. Deshalb sollte Ihnen bewusst sein, wann die Tail-Call-Optimierung möglicherweise angewendet wird.

Bei Quellcode in der Art des folgenden Beispiels ist die Verwendung der Tail-Call-Optimierung wahrscheinlich:

	return (bar());

Auch in Quellcode wie diesem:

	(void) bar();
	return;

Umgekehrt kann der Aufruf von bar in Funktionsquellcode, der wie das folgende Beispiel endet, nicht() optimiert werden, da der Aufruf von bar() kein Endaufruf ist:

	bar();
	return (rval);

Um festzustellen, ob ein Aufruf einer Tail-Call-Optimierung unterzogen wurde, gehen Sie wie folgt vor:

Aufgrund der Befehlssatzarchitektur ist die Tail-Call-Optimierung auf SPARC-Systemen wesentlich verbreiteter als auf x86-Systemen. In diesem Beispiel wird mdb eingesetzt, um die Tail-Call-Optimierung in der Kernelfunktion dup() zu entdecken:


# dtrace -q -n fbt::dup:return'{printf("%s+0x%x", probefunc, arg0);}'

Führen Sie, während dieser Befehl läuft, ein Programm aus, das ein dup(2) durchführt, zum Beispiel einen bash-Prozess. Der obige Befehl sollte eine Ausgabe wie in folgendem Beispiel liefern:


dup+0x10
^C

Untersuchen Sie die Funktion nun mit mdb:


# echo "dup::dis" | mdb -k
dup:                            sra       %o0, 0, %o0
dup+4:                          mov       %o7, %g1
dup+8:                          clr       %o2
dup+0xc:                        clr       %o1
dup+0x10:                       call      -0x1278       <fcntl>
dup+0x14:                       mov       %g1, %o7

Die Ausgabe zeigt, dass dup+0x10 ein Aufruf der Funktion fcntl() und keine ret-Anweisung ist. Der Aufruf von fcntl() ist also ein Beispiel für eine Tail-Call-Optimierung.