Vorteile und Nachteile von `string` in OpenResty
API7.ai
December 8, 2022
Im letzten Artikel haben wir uns mit den gängigen Blockierungsfunktionen in OpenResty vertraut gemacht, die oft von Anfängern falsch verwendet werden. Ab diesem Artikel werden wir uns mit dem Kern der Leistungsoptimierung befassen, was viele Optimierungstechniken beinhaltet, die uns helfen können, die Leistung von OpenResty-Code schnell zu verbessern. Daher sollten wir dies nicht auf die leichte Schulter nehmen.
In diesem Prozess müssen wir mehr Testcode schreiben, um zu erfahren, wie man diese Optimierungstechniken verwendet und ihre Wirksamkeit überprüft, damit wir sie gut nutzen können.
Hinter den Kulissen der Leistungsoptimierungstipps
Optimierungstechniken gehören alle zum "praktischen" Teil, daher wollen wir vorher über die "Theorie" der Optimierung sprechen.
Leistungsoptimierungsmethoden werden sich mit den Iterationen von LuaJIT und OpenResty ändern. Einige Methoden könnten direkt durch die zugrunde liegende Technologie optimiert werden und müssen nicht mehr beherrscht werden; gleichzeitig wird es einige neue Optimierungstechniken geben. Daher ist es am wichtigsten, das konstante Konzept hinter diesen Optimierungstechniken zu beherrschen.
Schauen wir uns einige der kritischen Ideen zur Leistung in der OpenResty-Programmierung an.
Theorie 1: Die Verarbeitung von Anfragen sollte kurz, einfach und schnell sein
OpenResty ist ein Webserver, daher verarbeitet er oft 1.000+, 10.000+ oder sogar 100.000+ Client-Anfragen gleichzeitig. Um die höchste Gesamtleistung zu erreichen, müssen wir sicherstellen, dass einzelne Anfragen schnell verarbeitet werden und verschiedene Ressourcen, wie z.B. Speicher, zurückgewonnen werden.
- Das hier erwähnte "kurz" bedeutet, dass der Lebenszyklus der Anfrage kurz sein sollte, um Ressourcen nicht lange zu blockieren, ohne sie freizugeben; selbst bei langen Verbindungen sollte ein Schwellenwert für die Zeit oder die Anzahl der Anfragen festgelegt werden, um Ressourcen regelmäßig freizugeben.
- Das zweite "einfach" bezieht sich darauf, nur eine Sache in einer API zu tun. Komplexe Geschäftslogik sollte in mehrere APIs aufgeteilt werden, und der Code sollte einfach gehalten werden.
- Schließlich bedeutet "schnell", dass der Hauptthread nicht blockiert werden sollte und nicht zu viele CPU-Operationen ausgeführt werden sollten. Selbst wenn dies notwendig ist, sollten andere Methoden, die wir im letzten Artikel vorgestellt haben, nicht vergessen werden.
Diese architektonische Überlegung ist nicht nur für OpenResty geeignet, sondern auch für weitere Entwicklungssprachen und Plattformen, daher hoffe ich, dass Sie dies sorgfältig verstehen und darüber nachdenken können.
Theorie 2: Vermeiden Sie die Erzeugung von Zwischendaten
Die Vermeidung von nutzlosen Daten im Zwischenprozess ist wohl die dominierende Optimierungstheorie in der OpenResty-Programmierung. Schauen wir uns ein kleines Beispiel an, um nutzlose Daten im Zwischenprozess zu erklären.
$ resty -e 'local s= "hello"
s = s .. " world"
s = s .. "!"
print(s)
'
In diesem Code-Snippet haben wir mehrere Verkettungsoperationen auf der Variablen s
durchgeführt, um das Ergebnis hello world!
zu erhalten. Aber nur der endgültige Zustand hello world!
von s
ist nützlich. Der Anfangswert von s
und die Zwischenzuweisungen sind alle Zwischendaten, die so wenig wie möglich erzeugt werden sollten.
Der Grund dafür ist, dass diese temporären Daten Initialisierungs- und GC-Leistungsverluste mit sich bringen. Unterschätzen Sie diese Verluste nicht; wenn dies in heißem Code wie Schleifen auftritt, wird die Leistung offensichtlich beeinträchtigt. Ich werde dies später auch mit einem String-Beispiel erklären.
string
s sind unveränderlich
Nun zurück zum Thema dieses Artikels, string
. Hier möchte ich betonen, dass string
s in Lua unveränderlich sind.
Das bedeutet natürlich nicht, dass string
s nicht verkettet, modifiziert usw. werden können, aber wenn wir einen string
ändern, ändern wir nicht den ursprünglichen string
, sondern erstellen ein neues string
-Objekt und ändern die Referenz auf den string
. Wenn der ursprüngliche string
keine anderen Referenzen hat, wird er natürlich von Lua's GC (Garbage Collection) zurückgewonnen.
Der offensichtliche Vorteil von unveränderlichen string
s ist, dass sie Speicher sparen. Auf diese Weise gibt es nur eine Kopie des gleichen string
s im Speicher, und verschiedene Variablen zeigen auf die gleiche Speicheradresse.
Der Nachteil dieses Designs ist, dass beim Hinzufügen und Zurückgewinnen von string
s jedes Mal, wenn Sie einen string
hinzufügen, LuaJIT lj_str_new
aufrufen muss, um zu prüfen, ob der string
bereits existiert; wenn nicht, muss ein neuer string
erstellt werden. Wenn Sie dies sehr oft tun, wird dies die Leistung erheblich beeinträchtigen.
Schauen wir uns ein konkretes Beispiel für eine string
-Verkettungsoperation wie in diesem Beispiel an, das in vielen OpenResty-Open-Source-Projekten zu finden ist.
$ resty -e 'local begin = ngx.now()
local s = ""
-- `for`-Schleife, die `..` verwendet, um String-Verkettungen durchzuführen
for i = 1, 100000 do
s = s .. "a"
end
ngx.update_time()
print(ngx.now() - begin)
'
Was dieser Beispielcode tut, ist 100.000 string
-Verkettungen auf der Variablen s
durchzuführen und die Laufzeit auszugeben. Obwohl das Beispiel etwas extrem ist, gibt es einen guten Eindruck von der Differenz vor und nach der Leistungsoptimierung. Ohne Optimierung läuft dieser Code auf meinem Laptop 0,4 Sekunden, was immer noch relativ langsam ist. Wie sollten wir ihn also optimieren?
In den vorherigen Artikeln wurde die Antwort gegeben, nämlich table
zu verwenden, um eine Ebene der Kapselung durchzuführen, alle temporären Zwischen-string
s zu entfernen und nur die ursprünglichen Daten und das Endergebnis zu behalten. Schauen wir uns die konkrete Code-Implementierung an.
$ resty -e 'local begin = ngx.now()
local t = {}
-- for-Schleife, die ein Array verwendet, um den String zu halten, und jedes Mal die Länge des Arrays zählt
for i = 1, 100000 do
t[#t + 1] = "a"
end
-- Verkettung der Strings mit der concat-Methode des Arrays
local s = table.concat(t, "")
ngx.update_time()
print(ngx.now() - begin)
'
Wir können sehen, dass dieser Code jeden String nacheinander mit table
speichert, und der Index wird durch #t + 1
bestimmt, also die aktuelle Länge von table
plus 1
. Schließlich wird die table.concat
-Funktion verwendet, um jedes Array-Element zu verketten. Dies überspringt natürlich alle temporären Strings und vermeidet 100.000 Mal lj_str_new
und GC.
Das war unsere Code-Analyse, aber wie funktioniert die Optimierung? Der optimierte Code dauert nur 0,007 Sekunden, was eine Leistungssteigerung von mehr als 50 Mal bedeutet. In einem tatsächlichen Projekt könnte die Leistungssteigerung noch deutlicher sein, da wir in diesem Beispiel nur ein Zeichen a
hinzugefügt haben.
Wie wäre die Leistungsdifferenz, wenn der neue string
die Länge von 10x a
hätte?
Sind die 0,007 Sekunden des Codes gut genug für unsere Optimierungsarbeit? Nein, es kann noch optimiert werden. Lassen Sie uns noch eine Codezeile ändern und das Ergebnis sehen.
$ resty -e 'local begin = ngx.now()
local t = {}
-- for-Schleife, die ein Array verwendet, um den String zu halten, und die Länge des Arrays selbst verwaltet
for i = 1, 100000 do
t[i] = "a"
end
local s = table.concat(t, "")
ngx.update_time()
print(ngx.now() - begin)
'
Dieses Mal haben wir t[#t + 1] = "a"
in t[i] = "a"
geändert, und mit nur einer Codezeile können wir 100.000 Funktionsaufrufe vermeiden, um die Länge des Arrays zu erhalten. Erinnern Sie sich an die Operation, um die Länge eines Arrays zu erhalten, die wir im table
-Abschnitt früher erwähnt haben? Sie hat eine Zeitkomplexität von O(n)
, eine relativ teure Operation. Hier umgehen wir einfach die Operation, die Länge des Arrays zu erhalten, indem wir unseren Array-Index selbst verwalten. Wie das Sprichwort sagt: Wenn Sie es sich nicht leisten können, es zu vermasseln, können Sie es vermeiden.
Natürlich ist dies eine einfachere Schreibweise. Der folgende Code zeigt deutlicher, wie wir den Index eines Arrays selbst verwalten können.
$ resty -e 'local begin = ngx.now()
local t = {}
local index = 1
for i = 1, 100000 do
t[index] = "a"
index = index + 1
end
local s = table.concat(t, "")
ngx.update_time()
print(ngx.now() - begin)
'
Reduzieren Sie andere temporäre string
s
Die Fehler, über die wir gerade gesprochen haben, temporäre string
s, die durch string
-Verkettung verursacht werden, sind offensichtlich. Mit ein paar Erinnerungen an den obigen Beispielcode glaube ich, dass wir keine ähnlichen Fehler mehr machen werden. Es gibt jedoch einige verstecktere temporäre string
s, die in OpenResty erzeugt werden und viel schwerer zu erkennen sind. Zum Beispiel die string
-Behandlungsfunktion, die wir unten diskutieren werden, wird oft verwendet. Können Sie sich vorstellen, dass sie auch temporäre string
s erzeugt?
Wie wir wissen, schneidet die string.sub
-Funktion einen bestimmten Teil eines string
s ab. Wie wir bereits erwähnt haben, sind string
s in Lua unveränderlich, daher beinhaltet das Abschneiden eines neuen Strings lj_str_new
und nachfolgende GC-Operationen.
resty -e 'print(string.sub("abcd", 1, 1))'
Die Funktion des obigen Codes besteht darin, das erste Zeichen des string
s zu extrahieren und auszugeben. Natürlich wird dies unweigerlich einen temporären string
erzeugen. Gibt es eine bessere Möglichkeit, den gleichen Effekt zu erzielen?
resty -e 'print(string.char(string.byte("abcd")))'
Natürlich. Schauen wir uns diesen Code an, wir verwenden zuerst string.byte
, um den numerischen Code des ersten Zeichens zu erhalten, und dann string.char
, um die Zahl in das entsprechende Zeichen umzuwandeln. Dieser Prozess erzeugt keine temporären string
s. Daher ist es am effizientesten, string.byte
zu verwenden, um string
-bezogene Scans und Analysen durchzuführen.
Nutzen Sie die SDK-Unterstützung für den table
-Typ
Nachdem Sie gelernt haben, wie man temporäre string
s reduziert, sind Sie begierig darauf, es auszuprobieren? Dann können wir das Ergebnis des obigen Beispielcodes nehmen und es als Inhalt des Antwortkörpers an den Client ausgeben. An dieser Stelle können Sie pausieren und versuchen, diesen Code selbst zu schreiben.
$ resty -e 'local begin = ngx.now()
local t = {}
local index = 1
for i = 1, 100000 do
t[index] = "a"
index = index + 1
end
local response = table.concat(t, "")
ngx.say(response)
'
Wenn Sie diesen Code schreiben können, sind Sie bereits den meisten OpenResty-Entwicklern voraus. OpenResty's Lua API berücksichtigt bereits die Verwendung von table
s für string
-Verkettungen, daher akzeptiert ngx.say
, ngx.print
, ngx.log
, cosocket:send
und andere APIs, die viele string
s verarbeiten können, nicht nur string
als Parameter, sondern auch table
als Parameter.
resty -e 'local begin = ngx.now()
local t = {}
local index = 1
for i = 1, 100000 do
t[index] = "a"
index = index + 1
end
ngx.say(t)
'
In diesem letzten Code-Snippet haben wir local response = table.concat(t, "")
, den string
-Verkettungsschritt, weggelassen und das table
direkt an ngx.say
übergeben. Dies verschiebt die string
-Verkettungsaufgabe von der Lua-Ebene auf die C-Ebene und vermeidet eine weitere string
-Suche, -Erzeugung und -GC. Für lange string
s ist dies ein weiterer erheblicher Leistungsgewinn.
Zusammenfassung
Nachdem wir diesen Artikel gelesen haben, können wir sehen, dass sich viel von OpenResty's Leistungsoptimierung mit verschiedenen Details befasst. Daher müssen wir LuaJIT und OpenResty's Lua API gut kennen, um optimale Leistung zu erzielen. Dies erinnert uns auch daran, dass wir, wenn wir den vorherigen Inhalt vergessen haben, ihn rechtzeitig überprüfen und festigen müssen.
Zum Schluss denken Sie über ein Problem nach: Schreiben Sie die Strings hello
, world
und !
in das Fehlerprotokoll. Können wir einen Beispielcode ohne string
-Verkettung schreiben?
Vergessen Sie auch nicht die andere Frage im Text. Wie wäre die Leistungsdifferenz im folgenden Code, wenn die neuen string
s die Länge von 10x a
hätten?
$ resty -e 'local begin = ngx.now()
local t = {}
for i = 1, 100000 do
t[#t + 1] = "a"
end
local s = table.concat(t, "")
ngx.update_time()
print(ngx.now() - begin)
'
Sie können diesen Artikel auch gerne mit Ihren Freunden teilen, um zu lernen und zu kommunizieren.