Der Nachteil des JIT-Compilers: Warum NYI vermeiden?

API7.ai

September 30, 2022

OpenResty (NGINX + Lua)

Im vorherigen Artikel haben wir uns mit FFI in LuaJIT beschäftigt. Wenn Ihr Projekt nur die von OpenResty bereitgestellte API verwendet und Sie keine C-Funktionen aufrufen müssen, dann ist FFI für Sie nicht so wichtig. Sie müssen lediglich sicherstellen, dass lua-resty-core aktiviert ist.

Aber NYI in LuaJIT, worüber wir heute sprechen werden, ist ein entscheidendes Problem, dem jeder Ingenieur, der OpenResty verwendet, nicht entkommen kann und das die Leistung erheblich beeinflusst.

Sie können mit OpenResty schnell logisch korrekten Code schreiben, aber ohne ein Verständnis von NYI können Sie keinen effizienten Code schreiben und die Leistungsfähigkeit von OpenResty nicht nutzen. Der Leistungsunterschied zwischen den beiden ist mindestens eine Größenordnung.

Was ist NYI?

Lassen Sie uns zunächst einen Punkt wiederholen, den wir bereits erwähnt haben.

Die Laufzeitumgebung von LuaJIT besteht neben einer Assembler-Implementierung des Lua-Interpreters aus einem JIT-Compiler, der direkt Maschinencode erzeugen kann.

Die Implementierung des JIT-Compilers in LuaJIT ist noch nicht vollständig. Er kann einige Funktionen nicht kompilieren, weil sie schwer zu implementieren sind und weil die Autoren von LuaJIT derzeit im Ruhestand sind. Dazu gehören die gängigen Funktionen pairs(), unpack(), das Lua-C-Modul basierend auf der Lua-CFunction-Implementierung und so weiter. Dies ermöglicht es dem JIT-Compiler, in den Interpreter-Modus zurückzufallen, wenn er auf eine Operation stößt, die er auf dem aktuellen Codepfad nicht unterstützt.

Die offizielle Website von LuaJIT hat eine vollständige Liste dieser NYIs, und ich empfehle Ihnen, sie durchzugehen. Das Ziel des Artikels ist nicht, dass Sie sich diese Liste merken, sondern dass Sie sich beim Schreiben von Codes bewusst daran erinnern.

Unten habe ich einige Funktionen aus der NYI-Liste für die String-Bibliothek herausgenommen.

String-Bibliothek

Der Kompilierungsstatus von string.byte ist ja, was bedeutet, dass es mit JIT optimiert werden kann, und Sie können es in Ihrem Code ohne Bedenken verwenden.

Der Kompilierungsstatus von string.char ist 2.1, was bedeutet, dass es seit LuaJIT 2.1 unterstützt wird. Wie wir wissen, basiert LuaJIT in OpenResty auf LuaJIT 2.1, daher können Sie es sicher verwenden.

Der Kompilierungsstatus von string.dump ist never, d.h., es wird nicht mit JIT optimiert und fällt in den Interpreter-Modus zurück. Bis jetzt gibt es keine Pläne, dies in Zukunft zu unterstützen.

string.find hat einen Kompilierungsstatus von 2.1 partial, was bedeutet, dass es seit LuaJIT 2.1 teilweise unterstützt wird, und die Anmerkung danach besagt, dass es nur die Suche nach festen Zeichenketten unterstützt, nicht die Mustererkennung. Für die Suche nach festen Zeichenketten kann string.find also mit JIT optimiert werden.

Natürlich sollten wir die Verwendung von NYI vermeiden, damit mehr unseres Codes JIT-kompiliert werden kann und die Leistung gewährleistet ist. In einer realen Umgebung müssen wir jedoch manchmal unweigerlich einige NYI-Funktionen verwenden. Was sollten wir also tun?

Alternativen zu NYI

Machen Sie sich keine Sorgen. Die meisten NYI-Funktionen können wir respektvoll hinter uns lassen und ihre Funktionalität auf andere Weise implementieren. Als Nächstes habe ich einige typische NYIs ausgewählt, um sie zu erklären und Sie durch die verschiedenen Arten von NYI-Alternativen zu führen. Auf diese Weise können Sie auch andere NYIs kennenlernen.

string.gsub()

Schauen wir uns zunächst die Funktion string.gsub() an, die eine eingebaute Lua-Funktion zur String-Manipulation ist und globale String-Ersetzungen durchführt, wie im folgenden Beispiel.

$ resty -e 'local new = string.gsub("banana", "a", "A"); print(new)'
bAnAnA

Diese Funktion ist eine NYI-Funktion und kann nicht von JIT kompiliert werden.

Wir könnten versuchen, eine Ersatzfunktion in der API von OpenResty zu finden, aber für die meisten Menschen ist es nicht praktisch, sich alle APIs und ihre Verwendung zu merken. Deshalb öffne ich in meiner Entwicklungsarbeit immer die GitHub-Dokumentationsseite für das lua-nginx-module.

Zum Beispiel können wir gsub als Schlüsselwort verwenden, um die Dokumentationsseite zu durchsuchen, und ngx.re.gsub wird uns einfallen.

Wir können auch das zuvor empfohlene restydoc-Tool verwenden, um die OpenResty-API zu durchsuchen. Sie können versuchen, es zu verwenden, um nach gsub zu suchen.

$ restydoc -s gsub

Wie Sie sehen können, wird nicht das erwartete ngx.re.gsub zurückgegeben, sondern Lua-Funktionen werden angezeigt. Tatsächlich gibt restydoc in diesem Stadium eine exakte eindeutige Übereinstimmung zurück, daher ist es besser geeignet, wenn Sie den API-Namen explizit kennen. Für unscharfe Suchen müssen Sie dies manuell in der Dokumentation durchführen.

Zurück zu den Suchergebnissen sehen wir, dass die Funktionsdefinition von ngx.re.gsub wie folgt lautet:

newstr, n, err = ngx.re.gsub(subject, regex, replace, options?)

Hier sind die Funktionsparameter und Rückgabewerte mit spezifischen Bedeutungen benannt. Tatsächlich empfehle ich in OpenResty nicht, viele Kommentare zu schreiben. Meistens ist ein guter Name besser als mehrere Zeilen Kommentare.

Für Ingenieure, die mit dem OpenResty-Regularsystem nicht vertraut sind, könnten Sie verwirrt sein, wenn Sie die Variable options am Ende sehen. Die Erklärung der Variable befindet sich jedoch nicht in dieser Funktion, sondern in der Dokumentation der Funktion ngx.re.match.

Wenn Sie sich die Dokumentation für options ansehen, werden Sie feststellen, dass, wenn wir sie auf jo setzen, PCRE JIT aktiviert wird, sodass Code, der ngx.re.gsub verwendet, sowohl von LuaJIT als auch von PCRE JIT JIT-kompiliert werden kann.

Ich werde nicht auf die Details der Dokumentation eingehen. Die OpenResty-Dokumentation ist ausgezeichnet, also lesen Sie sie sorgfältig durch, und Sie können die meisten Ihrer Probleme lösen.

string.find()

Im Gegensatz zu string.gsub ist string.find im einfachen Modus (d.h. String-Suche) JIT-fähig, während string.find für String-Suchen mit Regelmäßigkeit nicht JIT-fähig ist, was mit der API ngx.re.find von OpenResty durchgeführt wird.

Wenn Sie also in OpenResty eine String-Suche durchführen, müssen Sie zunächst klar unterscheiden, ob Sie nach einer festen Zeichenkette oder einem regulären Ausdruck suchen. Wenn es sich um Ersteres handelt, verwenden Sie string.find und setzen Sie am Ende plain auf true.

string.find("foo bar", "foo", 1, true)

Im letzteren Fall sollten Sie die API von OpenResty verwenden und die JIT-Option für PCRE aktivieren.

ngx.re.find("foo bar", "^foo", "jo")

Es wäre angemessener, hier eine Ebene der Verpackung vorzunehmen und die Optimierungsoptionen standardmäßig zu aktivieren, ohne den Endbenutzer so viele Details wissen zu lassen. Auf diese Weise ist es nach außen hin eine einheitliche String-Suchfunktion. Wie Sie spüren können, sind manchmal zu viele Optionen und zu viel Flexibilität keine gute Sache.

unpack()

Die dritte Funktion, die wir uns ansehen werden, ist unpack(). unpack() ist ebenfalls eine Funktion, die vermieden werden sollte, insbesondere nicht im Schleifenkörper. Stattdessen können Sie über die Indexnummern eines Arrays darauf zugreifen, wie in diesem Beispiel aus dem folgenden Code.

$ resty -e '
 local a = {100, 200, 300, 400}
 for i = 1, 2 do
    print(unpack(a))
 end'

$ resty -e 'local a = {100, 200, 300, 400}
 for i = 1, 2 do
    print(a[1], a[2], a[3], a[4])
 end'

Lassen Sie uns etwas tiefer in unpack eintauchen, und diesmal können wir restydoc zur Suche verwenden.

$ restydoc -s unpack

Wie Sie der Dokumentation von unpack entnehmen können, ist unpack(list [, i [, j]]) äquivalent zu return list[i], list[i+1], list[j], und Sie können sich unpack als syntaktischen Zucker vorstellen. Auf diese Weise können Sie genau wie über einen Array-Index darauf zugreifen, ohne die JIT-Kompilierung von LuaJIT zu unterbrechen.

pairs()

Schließlich schauen wir uns die Funktion pairs() an, die die Hashtabelle durchläuft und ebenfalls nicht von JIT kompiliert werden kann.

Leider gibt es jedoch keine gleichwertige Alternative dazu. Sie können nur versuchen, sie zu vermeiden oder stattdessen Arrays zu verwenden, die über numerische Indizes zugreifen, und insbesondere die Hashtabelle nicht auf dem heißen Codepfad durchlaufen. Hier erkläre ich den Code-Hot-Pfad, was bedeutet, dass der Code viele Male zur Ausführung zurückkehrt, zum Beispiel innerhalb einer riesigen Schleife.

Nach diesen vier Beispielen fassen wir zusammen, dass Sie, um die Verwendung von NYI-Funktionen zu umgehen, auf diese beiden Punkte achten müssen.

  • Verwenden Sie bevorzugt die von OpenResty bereitgestellte API anstelle der Standardbibliotheksfunktionen von Lua. Denken Sie daran, dass Lua eine eingebettete Sprache ist und wir in OpenResty programmieren, nicht in Lua.
  • Wenn Sie als letzten Ausweg die NYI-Sprache verwenden müssen, stellen Sie sicher, dass sie sich nicht auf dem Code-Hot-Pfad befindet.

Wie erkennt man NYI?

All dieses Gerede über NYI-Umgehung soll Ihnen beibringen, was zu tun ist. Es wäre jedoch inkonsistent mit einer der Philosophien, die OpenResty vertritt, wenn es hier abrupt enden würde.

Was automatisch von der Maschine erledigt werden kann, betrifft keine Menschen.

Menschen sind keine Maschinen, und es wird immer Übersehen geben. Die automatische Erkennung von NYI, die im Code verwendet wird, ist eine wesentliche Reflexion des Wertes eines Ingenieurs.

Hier empfehle ich die mit LuaJIT gelieferten Module jit.dump und jit.v. Beide geben den Prozess aus, wie der JIT-Compiler arbeitet. Ersteres gibt detaillierte Informationen aus, die zum Debuggen von LuaJIT selbst verwendet werden können. Sie können sich auf seinen Quellcode beziehen, um ein tieferes Verständnis zu erlangen; Letzteres gibt einfachere Ausgaben aus, wobei jede Zeile einem Trace entspricht, und wird normalerweise verwendet, um zu überprüfen, ob es JIT-fähig ist.

Wie sollten wir das tun? Wir können damit beginnen, die folgenden beiden Codezeilen zu init_by_lua hinzuzufügen.

local v = require "jit.v"
v.on("/tmp/jit.log")

Dann führen Sie Ihr Belastungstest-Tool oder einige hundert Unit-Test-Sets aus, um LuaJIT heiß genug zu machen, um die JIT-Kompilierung auszulösen. Sobald das erledigt ist, überprüfen Sie die Ergebnisse von /tmp/jit.log.

Natürlich ist dieser Ansatz relativ mühsam, daher reicht resty aus, wenn Sie es einfach halten wollen, und die OpenResty-CLI bietet die folgenden Optionen.

$resty -j v -e 'for i=1, 1000 do
      local newstr, n, err = ngx.re.gsub("hello, world", "([a-z])[a-z]+", "[$0,$1]", "i")
 end'
 [TRACE   1 (command line -e):1 stitch C:107bc91fd]
 [TRACE   2 (1/stitch) (command line -e):2 -> 1]

Dabei ist -j in resty die LuaJIT-bezogene Option, die Werte dump und v folgen, entsprechend dem Aktivieren des jit.dump- und jit.v-Modus.

In der Ausgabe des jit.v-Moduls ist jede Zeile ein erfolgreich kompilierter Trace-Objekt. Gerade eben war ein Beispiel für einen JIT-fähigen Trace, und wenn NYI-Funktionen gefunden werden, wird in der Ausgabe angegeben, dass es sich um NYIs handelt, wie im Beispiel des folgenden pairs.

$resty -j v -e 'local t = {}
 for i=1,100 do
     t[i] = i
 end

 for i=1, 1000 do
     for j=1,1000 do
         for k,v in pairs(t) do
             --
         end
     end
 end'

Es kann nicht JIT-kompiliert werden, daher zeigt das Ergebnis eine NYI-Funktion in Zeile 8 an.

 [TRACE   1 (command line -e):2 loop]
 [TRACE --- (command line -e):7 -- NYI: bytecode 72 at (command line -e):8]

Schlusswort

Dies ist das erste Mal, dass wir ausführlicher über Leistungsprobleme in OpenResty gesprochen haben. Nachdem Sie diese Optimierungen über NYI gelesen haben, was denken Sie? Sie können Ihre Meinung in einem Kommentar hinterlassen.

Abschließend hinterlasse ich Ihnen eine nachdenkliche Frage, als wir über Alternativen zur Funktion string.find() diskutierten; ich erwähnte, dass es besser wäre, eine Ebene der Verpackung vorzunehmen und die Optimierungsoptionen standardmäßig zu aktivieren. Also überlasse ich Ihnen diese Aufgabe für eine kleine Probefahrt.

Schreiben Sie Ihre Antworten gerne in den Kommentarbereich, und Sie sind herzlich eingeladen, diesen Artikel mit Ihren Kollegen und Freunden zu teilen, um zu kommunizieren und gemeinsam Fortschritte zu machen.