Heim Backend-Entwicklung Python-Tutorial Verbesserung der Gedächtniseffizienz in einem funktionierenden Dolmetscher

Verbesserung der Gedächtniseffizienz in einem funktionierenden Dolmetscher

Dec 26, 2024 pm 01:30 PM

Improving memory efficiency in a working interpreter

Lebenszeiten sind ein faszinierendes Merkmal von Rust und der menschlichen Erfahrung. Da dies ein technischer Blog ist, konzentrieren wir uns auf ersteres. Zugegebenermaßen war ich ein langsamer Anwender, wenn es darum ging, Lebenszeiten zu nutzen, um Daten in Rust sicher auszuleihen. In der Treewalk-Implementierung von Memphis, meinem in Rust geschriebenen Python-Interpreter, nutze ich die Lebensdauern kaum aus (durch ununterbrochenes Klonen) und entgehe dem Borrow-Checker immer wieder (indem ich innere Veränderlichkeit verwende, ebenfalls ununterbrochen), wann immer es möglich ist.

Meine Rustaceaner, ich bin heute hier, um Ihnen zu sagen, dass dies jetzt endet. Lies von meinen Lippen … keine Abkürzungen mehr.

Okay, okay, seien wir ehrlich. Was eine Abkürzung ist und was der richtige Weg ist, ist eine Frage der Prioritäten und der Perspektive. Wir haben alle Fehler gemacht und ich bin hier, um die Verantwortung für meine zu übernehmen.

Ich habe sechs Wochen nach der ersten Installation von rustc angefangen, einen Dolmetscher zu schreiben, weil ich keine Gänsehaut habe. Nachdem wir dieses Reden und Gehabe hinter uns haben, beginnen wir mit der heutigen Vorlesung darüber, wie wir Lebenszeiten als Lebensader nutzen können, um meine aufgeblähte Interpreter-Codebasis zu verbessern.

Identifizieren und Vermeiden geklonter Daten

Eine Rust-Lebensdauer ist ein Mechanismus, der zur Kompilierungszeit garantiert, dass Referenzen die Objekte, auf die sie verweisen, nicht überleben. Sie ermöglichen es uns, das Problem des „baumelnden Zeigers“ von C und C zu vermeiden.

Dies setzt voraus, dass Sie sie überhaupt nutzen! Das Klonen ist eine praktische Problemumgehung, wenn Sie die Komplexität vermeiden möchten, die mit der Verwaltung von Lebensdauern verbunden ist. Der Nachteil ist jedoch eine erhöhte Speichernutzung und eine leichte Verzögerung bei jedem Kopieren von Daten.

Die Verwendung von Lebenszeiten zwingt Sie auch dazu, idiomatischer über Eigentümer und Kreditaufnahme in Rust nachzudenken, was ich unbedingt tun wollte.

Meinen ersten Kandidaten habe ich als Token aus einer Python-Eingabedatei ausgewählt. Meine ursprüngliche Implementierung, die sich während meiner Zeit bei Amtrak stark auf die ChatGPT-Anleitung stützte, verwendete diesen Ablauf:

  1. Wir übergeben unseren Python-Text an einen Builder
  2. Der Builder erstellt einen Lexer, der den Eingabestream tokenisiert
  3. Der Builder erstellt dann einen Parser, der den Token-Stream klont, um eine eigene Kopie zu enthalten
  4. Der Builder wird verwendet, um einen Interpreter zu erstellen, der den Parser wiederholt nach seiner nächsten geparsten Anweisung fragt und diese auswertet, bis wir das Ende des Token-Streams erreichen

Der praktische Aspekt des Klonens des Token-Streams besteht darin, dass der Lexer nach Schritt 3 gelöscht werden konnte. Durch die Aktualisierung meiner Architektur, sodass der Lexer die Token besitzt und der Parser sie nur ausleiht, müsste der Lexer nun bleiben viel länger am Leben. Rust-Lebenszeiten würden dies für uns garantieren: Solange der Parser existierte, der eine Referenz auf ein geliehenes Token hielt, würde der Compiler garantieren, dass der Lexer, dem diese Token gehören, noch existierte, was eine gültige Referenz gewährleistete.

Wie bei jedem Code war dies letztendlich eine größere Änderung, als ich erwartet hatte. Mal sehen warum!

Der neue Parser

Bevor der Parser aktualisiert wurde, um die Token vom Lexer auszuleihen, sah es so aus. Die beiden Interessengebiete für die heutige Diskussion sind Token und current_token. Wir haben keine Ahnung, wie groß der Vec ist, aber es gehört eindeutig uns (d. h. wir leihen es uns nicht aus).

pub struct Parser {
    state: Container<State>,
    tokens: Vec<Token>,
    current_token: Token,
    position: usize,
    line_number: usize,
    delimiter_depth: usize,
}

impl Parser {
    pub fn new(tokens: Vec<Token>, state: Container<State>) -> Self {
        let current_token = tokens.first().cloned().unwrap_or(Token::Eof);
        Parser {
            state,
            tokens,
            current_token,
            position: 0,
            line_number: 1,
            delimiter_depth: 0,
        }
    }
}
Nach dem Login kopieren
Nach dem Login kopieren

Nachdem wir uns die Token vom Lexer geliehen haben, sieht es ziemlich ähnlich aus, aber jetzt sehen wir ein LEBENSLANGES! Durch die Verknüpfung von Token mit der Lebensdauer 'a verhindert der Rust-Compiler, dass der Eigentümer der Token (unser Lexer) und die Token selbst gelöscht werden, während unser Parser noch auf sie verweist. Das fühlt sich sicher und schick an!

static EOF: Token = Token::Eof;

/// A recursive-descent parser which attempts to encode the full Python grammar.
pub struct Parser<'a> {
    state: Container<State>,
    tokens: &'a [Token],
    current_token: &'a Token,
    position: usize,
    line_number: usize,
    delimiter_depth: usize,
}

impl<'a> Parser<'a> {
    pub fn new(tokens: &'a [Token], state: Container<State>) -> Self {
        let current_token = tokens.first().unwrap_or(&EOF);
        Parser {
            state,
            tokens,
            current_token,
            position: 0,
            line_number: 1,
            delimiter_depth: 0,
        }
    }
}
Nach dem Login kopieren
Nach dem Login kopieren

Ein weiterer kleiner Unterschied, der Ihnen vielleicht auffällt, ist diese Zeile:

static EOF: Token = Token::Eof;
Nach dem Login kopieren

Dies ist eine kleine Optimierung, über die ich nachzudenken begann, als sich mein Parser in Richtung „speichereffizient“ bewegte. Anstatt jedes Mal ein neues Token::Eof zu instanziieren, wenn der Parser prüfen muss, ob es sich am Ende des Textstroms befindet, konnte ich mit dem neuen Modell nur ein einzelnes Token instanziieren und wiederholt auf &EOF verweisen.

Auch hier handelt es sich um eine kleine Optimierung, aber sie spiegelt die übergeordnete Denkweise wider, dass jedes Datenelement nur einmal im Speicher vorhanden ist und jeder Verbraucher bei Bedarf nur darauf verweist, wozu Rust Sie sowohl ermutigt als auch freundlich mitnimmt der Weg.

Apropos Optimierung: Ich hätte die Speichernutzung vorher und nachher wirklich vergleichen sollen. Da ich dies nicht getan habe, kann ich dazu nichts mehr sagen.

Wie ich bereits erwähnt habe, hat die Verknüpfung der Lebensdauer meines Lexers und Parsers einen großen Einfluss auf mein Builder-Muster. Mal sehen, wie das aussieht!

Der neue Builder: MemphisContext

Erinnern Sie sich daran, wie ich in dem oben beschriebenen Ablauf erwähnt habe, dass der Lexer gelöscht werden könnte, sobald der Parser seine eigene Kopie der Token erstellt hat? Dies hatte unbeabsichtigt Einfluss auf das Design meines Builders, der die Komponente sein sollte, die die Orchestrierung von Lexer-, Parser- und Interpreter-Interaktionen unterstützt, unabhängig davon, ob Sie mit einem Python-Textstream oder einem Pfad zu einer Python-Datei beginnen.

Wie Sie unten sehen können, gibt es bei diesem Design noch ein paar andere nicht ideale Aspekte:

  1. Sie müssen eine gefährliche Downcast-Methode aufrufen, um den Interpreter zu erhalten.
  2. Warum dachte ich, dass es in Ordnung sei, einen Parser zu jedem Unit-Test zurückzugeben, um ihn dann direkt wieder an interpreter.run(&mut parser) weiterzuleiten?!
fn downcast<T: InterpreterEntrypoint + 'static>(input: T) -> Interpreter {
    let any_ref: &dyn Any = &input as &dyn Any;
    any_ref.downcast_ref::<Interpreter>().unwrap().clone()
}

fn init(text: &str) -> (Parser, Interpreter) {
    let (parser, interpreter) = Builder::new().text(text).build();

    (parser, downcast(interpreter))
}


#[test]
fn function_definition() {
     let input = r#"
def add(x, y):
    return x + y

a = add(2, 3)
"#;
    let (mut parser, mut interpreter) = init(input);

    match interpreter.run(&mut parser) {
        Err(e) => panic!("Interpreter error: {:?}", e),
        Ok(_) => {
            assert_eq!(
                interpreter.state.read("a"),
                Some(ExprResult::Integer(5.store()))
            );
        }
    }
}
Nach dem Login kopieren

Unten finden Sie die neue MemphisContext-Schnittstelle. Dieser Mechanismus verwaltet die Lexer-Lebensdauer intern (um unsere Referenzen lange genug am Leben zu halten, damit unser Parser zufrieden ist!) und stellt nur das bereit, was zum Ausführen dieses Tests erforderlich ist.

pub struct Parser {
    state: Container<State>,
    tokens: Vec<Token>,
    current_token: Token,
    position: usize,
    line_number: usize,
    delimiter_depth: usize,
}

impl Parser {
    pub fn new(tokens: Vec<Token>, state: Container<State>) -> Self {
        let current_token = tokens.first().cloned().unwrap_or(Token::Eof);
        Parser {
            state,
            tokens,
            current_token,
            position: 0,
            line_number: 1,
            delimiter_depth: 0,
        }
    }
}
Nach dem Login kopieren
Nach dem Login kopieren

context.run_and_return_interpreter() ist immer noch etwas umständlich und weist auf ein weiteres Designproblem hin, das ich vielleicht später angehen werde: Wenn Sie den Interpreter ausführen, möchten Sie nur den endgültigen Rückgabewert zurückgeben oder etwas, das Ihnen den Zugriff auf beliebige Werte ermöglicht? aus der Symboltabelle? Diese Methode wählt den letztgenannten Ansatz. Ich denke tatsächlich, dass beides sinnvoll ist, und werde meine API weiterhin optimieren, um dies zu ermöglichen.

Übrigens hat diese Änderung meine Fähigkeit verbessert, einen beliebigen Teil des Python-Codes auszuwerten. Wenn Sie sich an meine WebAssembly-Saga erinnern, musste ich mich damals auf meinen Crosscheck-TreewalkAdapter verlassen, um das zu tun. Jetzt ist unsere Wasm-Schnittstelle viel sauberer.

static EOF: Token = Token::Eof;

/// A recursive-descent parser which attempts to encode the full Python grammar.
pub struct Parser<'a> {
    state: Container<State>,
    tokens: &'a [Token],
    current_token: &'a Token,
    position: usize,
    line_number: usize,
    delimiter_depth: usize,
}

impl<'a> Parser<'a> {
    pub fn new(tokens: &'a [Token], state: Container<State>) -> Self {
        let current_token = tokens.first().unwrap_or(&EOF);
        Parser {
            state,
            tokens,
            current_token,
            position: 0,
            line_number: 1,
            delimiter_depth: 0,
        }
    }
}
Nach dem Login kopieren
Nach dem Login kopieren

Die Schnittstelle context.evaluate_oneshot() gibt das Ausdrucksergebnis anstelle einer vollständigen Symboltabelle zurück. Ich frage mich, ob es eine bessere Möglichkeit gibt, sicherzustellen, dass eine der „Oneshot“-Methoden nur einmal auf einen Kontext angewendet werden kann, um sicherzustellen, dass kein Verbraucher sie in einem zustandsbehafteten Kontext verwendet. Daran werde ich weiter brodeln!

Hat sich das gelohnt?

Memphis ist in erster Linie eine Lernübung, das hat sich also absolut gelohnt!

Zusätzlich zur gemeinsamen Nutzung der Token zwischen Lexer und Parser habe ich eine Schnittstelle erstellt, um Python-Code mit deutlich weniger Boilerplate auszuwerten. Während das Teilen von Daten zu zusätzlicher Komplexität führte, bringen diese Änderungen klare Vorteile mit sich: geringere Speichernutzung, verbesserte Sicherheitsgarantien durch strengeres Lifetime-Management und eine optimierte API, die einfacher zu warten und zu erweitern ist.

Ich glaube, dass dies der richtige Ansatz war, vor allem, um mein Selbstwertgefühl zu bewahren. Letztendlich möchte ich Code schreiben, der die Prinzipien der Software- und Computertechnik klar widerspiegelt. Wir können jetzt die Memphis-Quelle öffnen, auf den einzelnen Besitzer der Token verweisen und nachts tief und fest schlafen!

Abonnieren und sparen [bei nichts]

Wenn Sie weitere Beiträge wie diesen direkt in Ihrem Posteingang erhalten möchten, können Sie sich hier anmelden!

Anderswo

Neben der Betreuung von Softwareentwicklern schreibe ich auch über meine Erfahrungen beim Umgang mit der Selbstständigkeit und spät diagnostiziertem Autismus. Weniger Code und die gleiche Anzahl an Witzen.

  • Kaffee mit Seeeffekt, Kapitel 1 – Von Scratch dot org

Das obige ist der detaillierte Inhalt vonVerbesserung der Gedächtniseffizienz in einem funktionierenden Dolmetscher. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Erklärung dieser Website
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn

Heiße KI -Werkzeuge

Undresser.AI Undress

Undresser.AI Undress

KI-gestützte App zum Erstellen realistischer Aktfotos

AI Clothes Remover

AI Clothes Remover

Online-KI-Tool zum Entfernen von Kleidung aus Fotos.

Undress AI Tool

Undress AI Tool

Ausziehbilder kostenlos

Clothoff.io

Clothoff.io

KI-Kleiderentferner

Video Face Swap

Video Face Swap

Tauschen Sie Gesichter in jedem Video mühelos mit unserem völlig kostenlosen KI-Gesichtstausch-Tool aus!

Heiße Werkzeuge

Notepad++7.3.1

Notepad++7.3.1

Einfach zu bedienender und kostenloser Code-Editor

SublimeText3 chinesische Version

SublimeText3 chinesische Version

Chinesische Version, sehr einfach zu bedienen

Senden Sie Studio 13.0.1

Senden Sie Studio 13.0.1

Leistungsstarke integrierte PHP-Entwicklungsumgebung

Dreamweaver CS6

Dreamweaver CS6

Visuelle Webentwicklungstools

SublimeText3 Mac-Version

SublimeText3 Mac-Version

Codebearbeitungssoftware auf Gottesniveau (SublimeText3)

Heiße Themen

Java-Tutorial
1662
14
PHP-Tutorial
1262
29
C#-Tutorial
1235
24
Python vs. C: Anwendungen und Anwendungsfälle verglichen Python vs. C: Anwendungen und Anwendungsfälle verglichen Apr 12, 2025 am 12:01 AM

Python eignet sich für Datenwissenschafts-, Webentwicklungs- und Automatisierungsaufgaben, während C für Systemprogrammierung, Spieleentwicklung und eingebettete Systeme geeignet ist. Python ist bekannt für seine Einfachheit und sein starkes Ökosystem, während C für seine hohen Leistung und die zugrunde liegenden Kontrollfunktionen bekannt ist.

Der 2-stündige Python-Plan: ein realistischer Ansatz Der 2-stündige Python-Plan: ein realistischer Ansatz Apr 11, 2025 am 12:04 AM

Sie können grundlegende Programmierkonzepte und Fähigkeiten von Python innerhalb von 2 Stunden lernen. 1. Lernen Sie Variablen und Datentypen, 2. Master Control Flow (bedingte Anweisungen und Schleifen), 3.. Verstehen Sie die Definition und Verwendung von Funktionen, 4. Beginnen Sie schnell mit der Python -Programmierung durch einfache Beispiele und Code -Snippets.

Python: Spiele, GUIs und mehr Python: Spiele, GUIs und mehr Apr 13, 2025 am 12:14 AM

Python zeichnet sich in Gaming und GUI -Entwicklung aus. 1) Spielentwicklung verwendet Pygame, die Zeichnungen, Audio- und andere Funktionen bereitstellt, die für die Erstellung von 2D -Spielen geeignet sind. 2) Die GUI -Entwicklung kann Tkinter oder Pyqt auswählen. Tkinter ist einfach und einfach zu bedienen. PYQT hat reichhaltige Funktionen und ist für die berufliche Entwicklung geeignet.

Wie viel Python können Sie in 2 Stunden lernen? Wie viel Python können Sie in 2 Stunden lernen? Apr 09, 2025 pm 04:33 PM

Sie können die Grundlagen von Python innerhalb von zwei Stunden lernen. 1. Lernen Sie Variablen und Datentypen, 2. Master -Steuerungsstrukturen wie wenn Aussagen und Schleifen, 3. Verstehen Sie die Definition und Verwendung von Funktionen. Diese werden Ihnen helfen, einfache Python -Programme zu schreiben.

Python vs. C: Lernkurven und Benutzerfreundlichkeit Python vs. C: Lernkurven und Benutzerfreundlichkeit Apr 19, 2025 am 12:20 AM

Python ist leichter zu lernen und zu verwenden, während C leistungsfähiger, aber komplexer ist. 1. Python -Syntax ist prägnant und für Anfänger geeignet. Durch die dynamische Tippen und die automatische Speicherverwaltung können Sie die Verwendung einfach zu verwenden, kann jedoch zur Laufzeitfehler führen. 2.C bietet Steuerung und erweiterte Funktionen auf niedrigem Niveau, geeignet für Hochleistungsanwendungen, hat jedoch einen hohen Lernschwellenwert und erfordert manuellem Speicher und Typensicherheitsmanagement.

Python und Zeit: Machen Sie das Beste aus Ihrer Studienzeit Python und Zeit: Machen Sie das Beste aus Ihrer Studienzeit Apr 14, 2025 am 12:02 AM

Um die Effizienz des Lernens von Python in einer begrenzten Zeit zu maximieren, können Sie Pythons DateTime-, Zeit- und Zeitplanmodule verwenden. 1. Das DateTime -Modul wird verwendet, um die Lernzeit aufzuzeichnen und zu planen. 2. Das Zeitmodul hilft, die Studie zu setzen und Zeit zu ruhen. 3. Das Zeitplanmodul arrangiert automatisch wöchentliche Lernaufgaben.

Python: Erforschen der primären Anwendungen Python: Erforschen der primären Anwendungen Apr 10, 2025 am 09:41 AM

Python wird in den Bereichen Webentwicklung, Datenwissenschaft, maschinelles Lernen, Automatisierung und Skripten häufig verwendet. 1) In der Webentwicklung vereinfachen Django und Flask Frameworks den Entwicklungsprozess. 2) In den Bereichen Datenwissenschaft und maschinelles Lernen bieten Numpy-, Pandas-, Scikit-Learn- und TensorFlow-Bibliotheken eine starke Unterstützung. 3) In Bezug auf Automatisierung und Skript ist Python für Aufgaben wie automatisiertes Test und Systemmanagement geeignet.

Python: Automatisierung, Skript- und Aufgabenverwaltung Python: Automatisierung, Skript- und Aufgabenverwaltung Apr 16, 2025 am 12:14 AM

Python zeichnet sich in Automatisierung, Skript und Aufgabenverwaltung aus. 1) Automatisierung: Die Sicherungssicherung wird durch Standardbibliotheken wie OS und Shutil realisiert. 2) Skriptschreiben: Verwenden Sie die PSUTIL -Bibliothek, um die Systemressourcen zu überwachen. 3) Aufgabenverwaltung: Verwenden Sie die Zeitplanbibliothek, um Aufgaben zu planen. Die Benutzerfreundlichkeit von Python und die Unterstützung der reichhaltigen Bibliothek machen es zum bevorzugten Werkzeug in diesen Bereichen.

See all articles