Golang Reflektion: Ist das langsam?
Takashi Yamamoto
Infrastructure Engineer · Leapcell

Warum wird Reflektion benötigt?
Zuerst müssen wir verstehen, welche Vorteile Reflektion bringen kann. Wenn sie keine Vorteile bringt, dann brauchen wir sie in der Tat nicht zu verwenden und uns keine Sorgen über die Auswirkungen auf die Leistung zu machen.
Das Implementierungsprinzip der Reflektion in der Go-Sprache
Die Go-Sprache hat wenige Syntaxelemente und ein einfaches Design, daher hat sie keine besonders starke Ausdruckskraft. Das reflect
-Paket in der Go-Sprache kann jedoch einige ihrer syntaktischen Nachteile ausgleichen. Reflektion kann sich wiederholende Programmierarbeit reduzieren, und Toolkits verwenden Reflektion, um verschiedene struct-Eingabeparameter zu verarbeiten.
Verwenden von Reflektion, um zu beurteilen, ob eine Struktur leer ist
Geschäftsszenario
Auf diese Weise können wir direkt zurückkehren, wenn die eingehende Struktur leer ist, ohne SQL zu verketten, wodurch vollständige Tabellenscans und langsames SQL vermieden werden.
Implementierung ohne Verwendung von Reflektion
Wenn wir keine Reflektion verwenden, müssen wir jedes Feld einzeln überprüfen, wenn wir feststellen müssen, ob eine Struktur leer ist. Die Implementierung ist wie folgt:
type aStruct struct { Name string Male string } func (s *aStruct) IsEmpty() bool { return s.Male == "" && s.Name == "" } type complexSt struct { A aStruct S []string IntValue int } func (c *complexSt) IsEmpty() bool { return c.A.IsEmpty() && len(c.S) == 0 && c.IntValue == 0 }
Wenn wir zu diesem Zeitpunkt eine neue Struktur hinzufügen müssen, um zu beurteilen, ob sie leer ist, müssen wir die entsprechende Methode implementieren, um jedes Feld zu überprüfen.
Implementierung mit Reflektion
Wenn wir es mit Reflektion implementieren, können wir uns auf: Golang Empty Struct Judgment
beziehen. Zu diesem Zeitpunkt müssen wir nur die entsprechende Struktur übergeben, um zu erfahren, ob die entsprechenden Daten leer sind, ohne dass eine wiederholte Implementierung erforderlich ist.
Leistungsvergleich
func BenchmarkReflectIsStructEmpty(b *testing.B) { s := complexSt{ A: aStruct{}, S: make([]string, 0), IntValue: 0, } for i := 0; i < b.N; i++ { IsStructEmpty(s) } } func BenchmarkNormalIsStructEmpty(b *testing.B) { s := complexSt{ A: aStruct{}, S: make([]string, 0), IntValue: 0, } for i := 0; i < b.N; i++ { s.IsEmpty() } }
Ausführen von Leistungstests
# -benchmem zur Anzeige der Anzahl der Speicherzuweisungen pro Operation # -benchtime=3s zur Angabe der Ausführungszeit als 3s. Im Allgemeinen sind die in 1s, 3s und 5s erhaltenen Ergebnisse ähnlich. Wenn die Leistung schlecht ist, gilt: Je länger die Ausführungszeit, desto genauer der durchschnittliche Leistungswert. # -count=3 zur Angabe der Anzahl der Ausführungen. Mehrere Ausführungen können die Genauigkeit gewährleisten. # -cpu n zur Angabe der Anzahl der CPU-Kerne. Im Allgemeinen verbessert die Erhöhung der Anzahl der CPU-Kerne die Leistung, aber es besteht keine positive Korrelation. Da Kontextwechsel Auswirkungen haben, wenn mehr Kerne vorhanden sind, hängt es davon ab, ob es sich um eine IO-intensive oder CPU-intensive Anwendung handelt. Ein Vergleich kann in Multi-Goroutine-Tests durchgeführt werden. go test -bench="." -benchmem -cpuprofile=cpu_profile.out -memprofile=mem_profile.out -benchtime=3s -count=3.
Ausführungsergebnisse
BenchmarkReflectIsStructEmpty-16 8127797 493 ns/op 112 B/op 7 allocs/op BenchmarkReflectIsStructEmpty-16 6139068 540 ns/op 112 B/op 7 allocs/op BenchmarkReflectIsStructEmpty-16 7282296 465 ns/op 112 B/op 7 allocs/op BenchmarkNormalIsStructEmpty-16 1000000000 0.272 ns/op 0 B/op 0 allocs/op BenchmarkNormalIsStructEmpty-16 1000000000 0.285 ns/op 0 B/op 0 allocs/op BenchmarkNormalIsStructEmpty-16 1000000000 0.260 ns/op 0 B/op 0 allocs/op
Ergebnisanalyse
Die Bedeutung der Ergebnisspalten:
Ergebnisspalte | Bedeutung |
---|---|
BenchmarkReflectIsStructEmpty - 16 | BenchmarkReflectIsStructEmpty ist der Name der Testfunktion, und - 16 gibt an, dass der Wert von GOMAXPROCS (Anzahl der Threads) 16 ist |
2899022 | Es wurden insgesamt 2899022 Ausführungen durchgeführt |
401 ns/op | Gibt an, dass durchschnittlich 401 Nanosekunden pro Operation aufgewendet wurden |
112 B/op | Gibt an, dass 112 Byte Speicher pro Operation zugewiesen wurden |
7 allocs/op | Gibt an, dass der Speicher sieben Mal zugewiesen wurde |
Der Zeitaufwand für jede durch Reflektion beurteilte Operation ist ungefähr 1000-mal so hoch wie bei direkter Beurteilung, und sie bringt auch sieben zusätzliche Speicherzuweisungen mit sich, die jedes Mal um 112 Byte erhöht werden. Insgesamt sinkt die Leistung im Vergleich zum direkten Betrieb immer noch erheblich.
Kopieren von Strukturfeldern mit demselben Namen mithilfe von Reflektion
Implementierung ohne Verwendung von Reflektion
In tatsächlichen Geschäftsoberflächen müssen wir häufig Daten zwischen DTO
und VO
konvertieren, und meistens handelt es sich um das Kopieren von Feldern mit demselben Namen. Wenn wir zu diesem Zeitpunkt keine Reflektion verwenden, müssen wir jedes Feld kopieren, und wenn eine neue Struktur kopiert werden muss, müssen wir das Schreiben der new
-Methode wie folgt wiederholen, was viel wiederholende Arbeit mit sich bringt:
type aStruct struct { Name string Male string } type aStructCopy struct { Name string Male string } func newAStructCopyFromAStruct(a *aStruct) *aStructCopy { return &aStructCopy{ Name: a.Name, Male: a.Male, } }
Implementierung mit Reflektion
Wenn wir Reflektion zum Kopieren von Strukturen verwenden und es eine neue Struktur gibt, die kopiert werden muss, müssen wir nur den Strukturzeiger übergeben, um Felder mit demselben Namen zu kopieren. Die Implementierung ist wie folgt:
func CopyIntersectionStruct(src, dst interface{}) { sElement := reflect.ValueOf(src).Elem() dElement := reflect.ValueOf(dst).Elem() for i := 0; i < dElement.NumField(); i++ { dField := dElement.Type().Field(i) sValue := sElement.FieldByName(dField.Name) if!sValue.IsValid() { continue } value := dElement.Field(i) value.Set(sValue) } }
Leistungsvergleich
func BenchmarkCopyIntersectionStruct(b *testing.B) { a := &aStruct{ Name: "test", Male: "test", } for i := 0; i < b.N; i++ { var ac aStructCopy CopyIntersectionStruct(a, &ac) } } func BenchmarkNormalCopyIntersectionStruct(b *testing.B) { a := &aStruct{ Name: "test", Male: "test", } for i := 0; i < b.N; i++ { newAStructCopyFromAStruct(a) } }
Ausführen von Leistungstests
go test -bench="." -benchmem -cpuprofile=cpu_profile.out -memprofile=mem_profile.out -benchtime=3s -count=3.
Ausführungsergebnisse
BenchmarkCopyIntersectionStruct-16 10789202 352 ns/op 64 B/op 5 allocs/op BenchmarkCopyIntersectionStruct-16 10877558 304 ns/op 64 B/op 5 allocs/op BenchmarkCopyIntersectionStruct-16 10167404 322 ns/op 64 B/op 5 allocs/op BenchmarkNormalCopyIntersectionStruct-16 1000000000 0.277 ns/op 0 B/op 0 allocs/op BenchmarkNormalCopyIntersectionStruct-16 1000000000 0.270 ns/op 0 B/op 0 allocs/op BenchmarkNormalCopyIntersectionStruct-16 1000000000 0.259 ns/op 0 B/op 0 allocs/op
Ähnlich wie beim ersten obigen Ausführungsergebnis ist der Zeitaufwand für Reflektion immer noch 1000-mal so hoch wie für die Nichtverwendung von Reflektion, und die Speicherzuweisung erhöht sich ebenfalls jedes Mal um 64 Byte. In tatsächlichen Geschäftsszenarien können mehrere Reflektionen kombiniert werden. Wenn Sie die tatsächliche Leistung testen müssen, können Sie Ihren eigenen BenchmarkTest schreiben. Vergleichende Flammenplots können den Anteil der Laufzeit deutlicher zeigen.
Schlussfolgerung
In Geschäftsoberflächen gehen wir davon aus, dass die Oberflächenantwort 10 ms beträgt und der durchschnittliche Betrieb einer Reflektionsmethode 400 Nanosekunden beträgt, was zu einer zusätzlichen Speicherzuweisung von ungefähr 64 - 112 Byte führt.
1ms [Millisekunde] = 1000μs [Mikrosekunde]=1000 * 1000ns [Nanosekunde] 1MB = 1024KB = 1024 * 1024 B
Wenn eine Oberflaeche 1000 Reflektionsvorgänge im Link ausführt, erhöht ein einzelner Vorgang die Oberflächenlatenz um ungefähr 0,4 ms. Im Allgemeinen erreicht die Anzahl der Middleware- und Geschäftsoperationen in einer einzelnen Anfrage selten diese Anzahl, sodass die Auswirkungen auf die Antwortzeit im Wesentlichen ignoriert werden können. Im tatsächlichen Geschäft sind mehr Verluste beim Kopieren von Speicher und beim Netzwerk-IO zu verzeichnen.
Reflektion hat jedoch auch echte Probleme bei der Codierung. Sie ist schwieriger zu warten und zu verstehen als gewöhnlicher Geschäftscode. Daher müssen wir bei der Verwendung sorgfältig vorgehen, um eine übermäßige Verwendung zu vermeiden, die die Komplexität des Codes kontinuierlich erhöht.
Leapcell: Die beste serverlose Plattform für Golang-App-Hosting
Schließlich möchte ich die beste Plattform für die Bereitstellung von Golang-Diensten empfehlen: Leapcell
1. Multi - Language Support
- Entwickeln Sie mit JavaScript, Python, Go oder Rust.
2. Stellen Sie unbegrenzt Projekte kostenlos bereit
- Zahlen Sie nur für die Nutzung - keine Anforderungen, keine Gebühren.
3. Unschlagbare Kosteneffizienz
- Bezahlen Sie nach Nutzungsdauer ohne Leerlaufgebühren.
- Beispiel: 25 US-Dollar unterstützen 6,94 Millionen Anfragen bei einer durchschnittlichen Antwortzeit von 60 ms.
4. Optimierte Entwicklererfahrung
- Intuitive Benutzeroberfläche für müheloses Setup.
- Vollautomatische CI / CD-Pipelines und GitOps-Integration.
- Echtzeitmetriken und -protokollierung für verwertbare Erkenntnisse.
5. Mühelose Skalierbarkeit und hohe Leistung
- Automatische Skalierung zur einfachen Bewältigung hoher Parallelität.
- Kein operativer Aufwand - konzentrieren Sie sich einfach auf das Bauen.
Weitere Informationen finden Sie in der Dokumentation!
Leapcell Twitter: https://x.com/LeapcellHQ