Rumah > pembangunan bahagian belakang > Tutorial Python > Meningkatkan kecekapan ingatan dalam penterjemah yang berfungsi

Meningkatkan kecekapan ingatan dalam penterjemah yang berfungsi

Susan Sarandon
Lepaskan: 2024-12-26 13:30:10
asal
364 orang telah melayarinya

Improving memory efficiency in a working interpreter

Hayat hidup ialah ciri yang menarik bagi Rust dan pengalaman manusia. Ini adalah blog teknikal, jadi mari kita fokus pada yang pertama. Saya diakui adalah pengguna yang lambat untuk memanfaatkan jangka hayat untuk meminjam data dengan selamat dalam Rust. Dalam pelaksanaan treewalk Memphis, penterjemah Python saya yang ditulis dalam Rust, saya hampir tidak memanfaatkan jangka hayat (dengan mengklon tanpa henti) dan saya berulang kali mengelak pemeriksa pinjaman (dengan menggunakan kebolehubah dalaman, juga tanpa henti) apabila boleh.

Rakyat Rustacean saya, saya di sini hari ini untuk memberitahu anda bahawa ini berakhir sekarang. Baca bibir saya…tiada jalan pintas lagi.

Okey okey, biar betul. Apa itu jalan pintas berbanding cara yang betul adalah soal keutamaan dan perspektif. Kita semua telah melakukan kesilapan, dan saya di sini untuk bertanggungjawab ke atas diri saya.

Saya mula menulis jurubahasa enam minggu selepas saya mula-mula memasang rustc kerana saya tidak mempunyai kesejukan. Dengan sikap yang meleret-leret dan postur itu, mari kita mulakan kuliah hari ini tentang cara kita boleh menggunakan jangka hayat sebagai talian hayat kita untuk menambah baik pangkalan kod jurubahasa saya yang kembung.

Mengenal pasti dan mengelakkan data klon

Satu Hayat karat ialah mekanisme yang menyediakan jaminan masa kompilasi bahawa sebarang rujukan tidak melebihi umur objek yang dirujuknya. Ia membolehkan kita mengelakkan masalah "penunjuk berjuntai" daripada C dan C .

Ini mengandaikan anda memanfaatkannya sama sekali! Pengklonan ialah penyelesaian yang mudah apabila anda ingin mengelakkan kerumitan yang berkaitan dengan pengurusan jangka hayat, walaupun kelemahannya ialah peningkatan penggunaan memori dan sedikit kelewatan yang berkaitan dengan setiap kali data disalin.

Menggunakan seumur hidup juga memaksa anda untuk berfikir lebih idiomatik tentang pemilik dan meminjam dalam Rust, yang saya ingin lakukan.

Saya memilih calon pertama saya sebagai token daripada fail input Python. Pelaksanaan asal saya, yang sangat bergantung pada panduan ChatGPT semasa saya duduk di Amtrak, menggunakan aliran ini:

  1. kami menghantar teks Python kami kepada Builder
  2. Pembina mencipta Lexer, yang menandakan aliran input
  3. Pembina kemudian mencipta Parser, yang mengklonkan aliran token untuk menyimpan salinannya sendiri
  4. Pembina digunakan untuk mencipta Jurubahasa, yang berulang kali meminta Parser untuk pernyataan yang dihuraikan seterusnya dan menilainya sehingga kami sampai ke penghujung aliran token

Aspek yang mudah untuk mengklon aliran token ialah Lexer bebas untuk digugurkan selepas langkah 3. Dengan mengemas kini seni bina saya supaya Lexer memiliki token dan Parser hanya meminjamnya, Lexer kini perlu kekal hidup lebih lama lagi. Jangka hayat karat akan menjamin ini untuk kami: selagi Parser wujud memegang rujukan kepada token yang dipinjam, pengkompil akan menjamin bahawa Lexer yang memiliki token tersebut masih wujud, memastikan rujukan yang sah.

Seperti semua kod selalu, ini akhirnya menjadi perubahan yang lebih besar daripada yang saya jangkakan. Mari lihat sebabnya!

Penghurai baharu

Sebelum mengemas kini Parser untuk meminjam token daripada Lexer, ia kelihatan seperti ini. Dua bidang yang diminati untuk perbincangan hari ini ialah token dan current_token. Kami tidak tahu berapa besar Vec adalah, tetapi ia jelas milik kami (iaitu kami tidak meminjamnya).

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,
        }
    }
}
Salin selepas log masuk
Salin selepas log masuk

Selepas meminjam token daripada Lexer, ia kelihatan agak serupa, tetapi kini kita melihat SEUMUR HIDUP! Dengan menyambungkan token kepada 'a seumur hidup, pengkompil Rust tidak akan membenarkan pemilik token (iaitu Lexer kami) dan token itu sendiri digugurkan semasa Parser kami masih merujuknya. Ini rasa selamat dan mewah!

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,
        }
    }
}
Salin selepas log masuk
Salin selepas log masuk

Satu lagi perbezaan kecil yang anda mungkin perasan ialah baris ini:

static EOF: Token = Token::Eof;
Salin selepas log masuk

Ini adalah pengoptimuman kecil yang saya mula pertimbangkan sebaik sahaja Penghurai saya bergerak ke arah "cekap ingatan". Daripada membuat instantiat Token baharu::Eof setiap kali Parser perlu menyemak sama ada ia berada di penghujung strim teks, model baharu membenarkan saya membuat instantiat hanya satu token dan rujukan &EOF berulang kali.

Sekali lagi, ini adalah pengoptimuman kecil, tetapi ia merujuk kepada pemikiran yang lebih besar bagi setiap data yang wujud hanya sekali dalam ingatan dan setiap pengguna hanya merujuknya apabila diperlukan, yang mana Rust menggalakkan anda untuk melakukannya dan memegang tangan anda dengan kemas caranya.

Bercakap tentang pengoptimuman, saya sepatutnya menanda aras penggunaan memori sebelum dan selepas. Memandangkan saya tidak melakukannya, saya tidak mempunyai apa-apa lagi untuk diperkatakan mengenai perkara itu.

Seperti yang saya nyatakan sebelum ini, mengikat seumur hidup Lexer dan Parser saya bersama-sama memberi impak yang besar pada corak Builder saya. Mari lihat rupanya!

Pembina baharu: MemphisContext

Dalam aliran yang saya terangkan di atas, ingat bagaimana saya menyebut bahawa Lexer boleh digugurkan sebaik sahaja Parser mencipta salinan tokennya sendiri? Ini secara tidak sengaja telah mempengaruhi reka bentuk Builder saya, yang bertujuan untuk menjadi komponen yang menyokong mendalangi interaksi Lexer, Parser dan Interpreter, sama ada anda bermula dengan strim teks Python atau laluan ke fail Python.

Seperti yang anda lihat di bawah, terdapat beberapa aspek lain yang tidak sesuai untuk reka bentuk ini:

  1. perlu memanggil kaedah downcast berbahaya untuk mendapatkan Jurubahasa.
  2. kenapa saya fikir tidak mengapa untuk memulangkan Parser kepada setiap ujian unit hanya untuk meneruskannya terus ke interpreter.run(&mut parser)?!
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()))
            );
        }
    }
}
Salin selepas log masuk

Di bawah ialah antara muka MemphisContext baharu. Mekanisme ini menguruskan seumur hidup Lexer secara dalaman (untuk memastikan rujukan kami kekal cukup lama untuk memastikan Parser kami gembira!) dan hanya mendedahkan perkara yang diperlukan untuk menjalankan ujian ini.

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,
        }
    }
}
Salin selepas log masuk
Salin selepas log masuk

context.run_and_return_interpreter() masih agak kikuk dan bercakap kepada masalah reka bentuk lain yang mungkin saya atasi: apabila anda menjalankan penterjemah, adakah anda mahu memulangkan hanya nilai pulangan akhir atau sesuatu yang membolehkan anda mengakses nilai sewenang-wenangnya daripada jadual simbol? Kaedah ini memilih pendekatan yang terakhir. Saya sebenarnya berpendapat ada kes untuk melakukan kedua-duanya, dan akan terus mengubahsuai API saya untuk membenarkan perkara ini semasa kita pergi.

Secara kebetulan, perubahan ini meningkatkan keupayaan saya untuk menilai sekeping kod Python yang sewenang-wenangnya. Jika anda ingat dari saga WebAssembly saya, saya terpaksa bergantung pada semak silang TreewalkAdapter saya untuk melakukannya pada masa itu. Kini, antara muka Wasm kami jauh lebih bersih.

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,
        }
    }
}
Salin selepas log masuk
Salin selepas log masuk

Konteks antara muka.evaluate_oneshot() mengembalikan hasil ungkapan dan bukannya jadual simbol penuh. Saya tertanya-tanya sama ada terdapat cara yang lebih baik untuk memastikan mana-mana kaedah "oneshot" hanya boleh beroperasi pada konteks sekali, memastikan tiada pengguna menggunakannya dalam konteks stateful. Saya akan terus merenungnya!

Adakah ini berbaloi?

Memphis ialah latihan pembelajaran yang pertama sekali, jadi ini sangat berbaloi!

Selain berkongsi token antara Lexer dan Parser, saya mencipta antara muka untuk menilai kod Python dengan boilerplate yang kurang ketara. Walaupun perkongsian data memperkenalkan kerumitan tambahan, perubahan ini membawa faedah yang jelas: penggunaan memori yang dikurangkan, jaminan keselamatan yang dipertingkatkan melalui pengurusan seumur hidup yang lebih ketat dan API diperkemas yang lebih mudah untuk diselenggara dan dilanjutkan.

Saya memilih untuk mempercayai ini adalah pendekatan yang betul, kebanyakannya untuk mengekalkan harga diri saya. Akhirnya, saya berhasrat untuk menulis kod yang menggambarkan dengan jelas prinsip perisian dan kejuruteraan komputer. Kami kini boleh membuka sumber Memphis, menunjuk kepada pemilik tunggal token dan tidur dengan nyenyak pada waktu malam!

Langgan & Simpan [pada apa-apa]

Jika anda ingin mendapatkan lebih banyak siaran seperti ini terus ke peti masuk anda, anda boleh melanggan di sini!

Di tempat lain

Selain membimbing jurutera perisian, saya juga menulis tentang pengalaman saya mengemudi bekerja sendiri dan autisme yang didiagnosis lewat. Kurang kod dan bilangan jenaka yang sama.

  • Kopi Kesan Tasik, Bab 1 - Dari titik awal org

Atas ialah kandungan terperinci Meningkatkan kecekapan ingatan dalam penterjemah yang berfungsi. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

sumber:dev.to
Kenyataan Laman Web ini
Kandungan artikel ini disumbangkan secara sukarela oleh netizen, dan hak cipta adalah milik pengarang asal. Laman web ini tidak memikul tanggungjawab undang-undang yang sepadan. Jika anda menemui sebarang kandungan yang disyaki plagiarisme atau pelanggaran, sila hubungi admin@php.cn
Artikel terbaru oleh pengarang
Tutorial Popular
Lagi>
Muat turun terkini
Lagi>
kesan web
Kod sumber laman web
Bahan laman web
Templat hujung hadapan