Was ist der Unterschied zwischen LuaJIT und Standard Lua?

API7.ai

September 23, 2022

OpenResty (NGINX + Lua)

Lassen Sie uns über LuaJIT lernen, einen weiteren Eckpfeiler von OpenResty, und ich werde den zentralen Teil des heutigen Beitrags einigen wesentlichen und weniger bekannten Aspekten von Lua und LuaJIT widmen.

Sie können die Grundlagen von Lua über Suchmaschinen oder Lua-Bücher lernen, und ich empfehle das Buch Programming in Lua vom Lua-Autor.

Natürlich ist die Schwelle, korrekten LuaJIT-Code in OpenResty zu schreiben, nicht hoch. Dennoch ist es nicht einfach, effizienten LuaJIT-Code zu schreiben, und ich werde die Schlüsselelemente hier später im Abschnitt zur OpenResty-Leistungsoptimierung ausführlich behandeln.

Lassen Sie uns zunächst betrachten, wo LuaJIT in die Gesamtarchitektur von OpenResty passt.

OpenResty-Architektur

Wie bereits erwähnt, werden OpenResty-Worker-Prozesse durch Forking des Master-Prozesses erstellt. Die LuaJIT-Virtual-Machine im Master-Prozess wird ebenfalls geforkt. Alle Worker-Prozesse innerhalb desselben Worker teilen sich diese LuaJIT-Virtual-Machine, und die Ausführung von Lua-Code erfolgt in dieser Virtual-Machine.

Dies sind die Grundlagen, wie OpenResty funktioniert, die wir in späteren Artikeln ausführlicher besprechen werden. Heute beginnen wir damit, die Beziehung zwischen Lua und LuaJIT zu klären.

Beziehung zwischen Standard Lua und LuaJIT

Lassen Sie uns zunächst die wesentlichen Dinge klären.

Standard Lua und LuaJIT sind zwei verschiedene Dinge. LuaJIT ist nur mit der Lua 5.1-Syntax kompatibel.

Die neueste Version von Standard Lua ist jetzt 5.4.4, und die neueste Version von LuaJIT ist 2.1.0-beta3. In älteren Versionen von OpenResty vor einigen Jahren konnte man beim Kompilieren entweder die Standard-Lua-VM oder die LuaJIT-VM als Ausführungsumgebung wählen, aber jetzt wurde die Unterstützung für Standard Lua entfernt, und nur LuaJIT wird unterstützt.

Die LuaJIT-Syntax ist mit Lua 5.1 kompatibel, mit optionaler Unterstützung für Lua 5.2 und 5.3. Daher sollten wir zunächst die Lua 5.1-Syntax lernen und darauf aufbauend die LuaJIT-Funktionen lernen. Im vorherigen Artikel habe ich Sie in die Grundlagen der Lua-Syntax eingeführt. Heute werde ich nur einige einzigartige Merkmale von Lua erwähnen.

Es ist erwähnenswert, dass OpenResty nicht direkt die offizielle LuaJIT-Version 2.1.0-beta3 verwendet, sondern sie mit seinem Fork erweitert: openresty-luajit2.

Diese einzigartigen APIs wurden während der tatsächlichen Entwicklung von OpenResty aus Leistungsgründen hinzugefügt. Daher bezieht sich das LuaJIT, das wir später erwähnen, auf den von OpenResty selbst gepflegten LuaJIT-Zweig.

Warum LuaJIT?

Nach all dem Gerede über die Beziehung zwischen LuaJIT und Lua fragen Sie sich vielleicht, warum man nicht direkt Lua verwendet, sondern LuaJIT. Tatsächlich ist der Hauptgrund der Leistungsvorteil von LuaJIT.

Lua-Code wird nicht direkt interpretiert, sondern vom Lua-Compiler in Byte Code kompiliert und dann von der Lua-Virtual-Machine ausgeführt.

Die LuaJIT-Laufzeitumgebung verfügt neben einer Assembler-Implementierung des Lua-Interpreters über einen JIT-Compiler, der direkt Maschinencode erzeugen kann. Zunächst startet LuaJIT wie Standard Lua, wobei Lua-Code in Bytecode kompiliert wird, der vom LuaJIT-Interpreter interpretiert und ausgeführt wird.

Der Unterschied besteht darin, dass der LuaJIT-Interpreter während der Ausführung des Bytecodes einige Laufzeitstatistiken aufzeichnet, wie z. B. die tatsächliche Anzahl der Ausführungen jedes Lua-Funktionsaufrufs und die tatsächliche Anzahl der Ausführungen jeder Lua-Schleife. Wenn diese Zählungen einen zufälligen Schwellenwert überschreiten, wird der entsprechende Lua-Funktionseintrag oder die Lua-Schleife als heiß genug betrachtet, um den JIT-Compiler zu starten.

Der JIT-Compiler versucht, den entsprechenden Lua-Code-Pfad zu kompilieren, beginnend am Eintrittspunkt der heißen Funktion oder der Position der heißen Schleife. Der Kompilierungsprozess wandelt den LuaJIT-Bytecode in eine von LuaJIT definierte IR (Intermediate Representation) um und erzeugt dann Maschinencode für die Zielarchitektur.

Daher besteht die sogenannte LuaJIT-Leistungsoptimierung im Wesentlichen darin, so viel Lua-Code wie möglich für die Maschinencodegenerierung durch den JIT-Compiler verfügbar zu machen, anstatt in den Interpretationsmodus des Lua-Interpreters zurückzufallen. Sobald Sie dies verstanden haben, können Sie die Natur der OpenResty-Leistungsoptimierung verstehen, die Sie später lernen werden.

Besondere Merkmale von Lua

Wie im vorherigen Artikel beschrieben, ist die Lua-Sprache relativ einfach. Für Ingenieure mit einem Hintergrund in anderen Entwicklungssprachen ist es leicht, die Logik des Codes zu erkennen, sobald Sie einige einzigartige Aspekte von Lua bemerken. Als nächstes betrachten wir einige der ungewöhnlicheren Aspekte der Lua-Sprache.

1. Index beginnt bei 1

Lua ist die einzige Programmiersprache, die ich kenne, die mit einem Index von 1 beginnt. Dies ist zwar für Nicht-Programmierer besser verständlich, führt jedoch leicht zu Programmierfehlern. Hier ist ein Beispiel.

$ resty -e 't={100}; ngx.say(t[0])'

Sie könnten erwarten, dass das Programm 100 ausgibt oder einen Fehler meldet, dass der Index 0 nicht existiert. Aber überraschenderweise wird nichts ausgegeben, und es werden keine Fehler gemeldet. Fügen wir also den type-Befehl hinzu und sehen, was die Ausgabe ist.

$ resty -e 't={100};ngx.say(type(t[0]))'
nil

Es stellt sich heraus, dass es sich um den nil-Wert handelt. Tatsächlich ist die Bestimmung und Handhabung von nil-Werten in OpenResty ebenfalls ein verwirrender Punkt, daher werden wir später mehr darüber sprechen, wenn wir über OpenResty sprechen.

2. Verwenden von .. zum Verketten von Zeichenketten

Im Gegensatz zu den meisten Sprachen, die + verwenden, verwendet Lua zwei Punkte, um Zeichenketten zu verketten.

$ resty -e "ngx.say('hello' .. ', world')"
hello, world

In der tatsächlichen Projektentwicklung verwenden wir in der Regel mehrere Entwicklungssprachen, und das ungewöhnliche Design von Lua wird Entwickler immer wieder zum Nachdenken bringen, wenn die Zeichenkettenverknüpfung ein wenig verwirrend ist.

3. Die Tabelle ist die einzige Datenstruktur

Im Gegensatz zu Python, einer Sprache mit vielen eingebauten Datenstrukturen, hat Lua nur eine Datenstruktur, die Tabelle, die Arrays und Hash-Tabellen umfassen kann.

local color = {first = "red", "blue", third = "green", "yellow"}
print(color["first"])                 --> Ausgabe: red
print(color[1])                         --> Ausgabe: blue
print(color["third"])                --> Ausgabe: green
print(color[2])                         --> Ausgabe: yellow
print(color[3])                         --> Ausgabe: nil

Wenn Sie keinen Wert explizit als Schlüssel-Wert-Paar zuweisen, verwendet die Tabelle standardmäßig eine Zahl als Index, beginnend mit 1. Daher ist color[1] blue.

Außerdem ist es schwierig, die korrekte Länge in der Tabelle zu erhalten. Schauen wir uns diese Beispiele an.

local t1 = { 1, 2, 3 }
print("Test1 " .. table.getn(t1))

local t2 = { 1, a = 2, 3 }
print("Test2 " .. table.getn(t2))

local t3 = { 1, nil }
print("Test3 " .. table.getn(t3))

local t4 = { 1, nil, 2 }
print("Test4 " .. table.getn(t4))

Ergebnis:

Test1 3
Test2 2
Test3 1
Test4

Wie Sie sehen können, gibt außer dem ersten Testfall, der eine Länge von 3 zurückgibt, keiner der nachfolgenden Tests das erwartete Ergebnis zurück. Tatsächlich ist es wichtig zu beachten, dass in Lua nur dann die korrekte Länge der Tabelle zurückgegeben wird, wenn die Tabelle eine Sequenz ist.

Was ist also eine Sequenz? Zunächst ist eine Sequenz eine Teilmenge eines Arrays. Das heißt, die Elemente einer Tabelle sind mit einem positiven ganzzahligen Index zugänglich, und es gibt keine Schlüssel-Wert-Paare. Im obigen Code sind alle Tabellen außer t2 Arrays.

Zweitens enthält die Sequenz keine Lücken, d. h. nil. Kombiniert man diese beiden Punkte, ist die obige Tabelle t1 eine Sequenz, während t3 und t4 Arrays, aber keine Sequenzen sind.

Bis zu diesem Punkt haben Sie vielleicht immer noch eine Frage, warum die Länge von t4 1 ist? Dies liegt daran, dass bei nil die Logik zur Ermittlung der Länge nicht weiterläuft, sondern direkt zurückgegeben wird.

Ich weiß nicht, ob Sie es vollständig verstanden haben. Dieser Teil ist wirklich ziemlich kompliziert. Gibt es also eine Möglichkeit, die gewünschte Tabellenlänge zu erhalten? Natürlich gibt es das. OpenResty erweitert dies, und ich werde später im Kapitel über die Tabelle darauf eingehen, lassen wir also die Spannung hier.

4. Alle Variablen sind standardmäßig global

Ich möchte betonen, dass Sie, es sei denn, Sie sind sich ziemlich sicher, neue Variablen immer als local-Variablen deklarieren sollten.

local s = 'hello'

Dies liegt daran, dass in Lua Variablen standardmäßig global sind und in einer Tabelle namens _G platziert werden. Variablen, die nicht lokal sind, werden in der globalen Tabelle gesucht, was ein teurer Vorgang ist. Falsch geschriebene Variablennamen können zu Fehlern führen, die schwer zu identifizieren und zu beheben sind.

Daher empfehle ich in OpenResty dringend, dass Sie Variablen immer mit local deklarieren, auch wenn Sie ein Modul benötigen.

-- Empfohlen
local xxx = require('xxx')

-- Vermeiden
require('xxx')

LuaJIT

Mit diesen vier besonderen Merkmalen von Lua im Hinterkopf, kommen wir nun zu LuaJIT.

LuaJIT ist neben der Lua 5.1-Kompatibilität und der JIT-Unterstützung eng mit der FFI (Foreign Function Interface) integriert, was es Ihnen ermöglicht, externe C-Funktionen aufzurufen und C-Datenstrukturen direkt in Ihrem Lua-Code zu verwenden. Hier ist das einfachste Beispiel.

local ffi = require("ffi")
ffi.cdef[[
int printf(const char *fmt, ...);
]]
ffi.C.printf("Hello %s!", "world")

In nur wenigen Codezeilen können Sie die C-Funktion printf direkt aus Lua aufrufen und Hello world! ausgeben. Sie können den resty-Befehl verwenden, um es auszuführen und zu sehen, ob es funktioniert.

Ebenso können wir FFI verwenden, um die C-Funktionen von NGINX und OpenSSL aufzurufen, um viel mehr zu tun. Der FFI-Ansatz bietet eine bessere Leistung als der traditionelle Lua/C-API-Ansatz, weshalb das lua-resty-core-Projekt existiert. Im nächsten Abschnitt werden wir über FFI und lua-resty-core sprechen.

Darüber hinaus erweitert LuaJIT aus Leistungsgründen die Tabellenfunktionen: table.new und table.clear, zwei wesentliche Leistungsoptimierungsfunktionen, die häufig in der lua-resty-Bibliothek von OpenResty verwendet werden. Allerdings sind nur wenige Entwickler mit ihnen vertraut, da die Dokumentation intensiv ist und es keinen Beispielcode gibt. Wir werden sie für den Abschnitt zur Leistungsoptimierung aufheben.

Zusammenfassung

Lassen Sie uns den heutigen Inhalt überprüfen.

OpenResty wählt LuaJIT anstelle von Standard Lua aus Leistungsgründen und pflegt seinen eigenen LuaJIT-Zweig. LuaJIT basiert auf der Lua 5.1-Syntax und ist selektiv mit einigen Lua 5.2- und Lua 5.3-Syntaxen kompatibel, um sein System zu bilden. Was die Lua-Syntax betrifft, die Sie beherrschen müssen, hat sie ihre besonderen Merkmale in Bezug auf Index, Zeichenkettenverknüpfung, Datenstrukturen und Variablen, auf die Sie beim Schreiben von Code besonders achten sollten.

Sind Ihnen beim Lernen von Lua und LuaJIT Fallstricke begegnet? Teilen Sie uns Ihre Meinungen mit, und ich habe einen Beitrag geschrieben, um die Fallstricke zu teilen, auf die ich gestoßen bin. Sie sind auch eingeladen, diesen Beitrag mit Ihren Kollegen und Freunden zu teilen, um gemeinsam zu lernen und Fortschritte zu machen.