정말 멋진 것들을 여러분과 공유하고 싶었습니다. 저는 Python 바이트코드에 대해 배우고 중첩에 대한 지원을 추가한 방법을 포함합니다. 기능이 있는데 인쇄소 직원이 500단어 미만으로 유지해야 한다고 하더군요.
연휴 주간입니다라고 그는 어깨를 으쓱했습니다. 내가 무엇을 하기를 바라나요?
코드조각 제외, 흥정했어요
괜찮습니다, 양보했습니다.
애초에 왜 바이트코드를 사용하는지 아시나요?
저는 인쇄기만 운영하고 있지만 당신을 믿습니다.
그렇습니다. 시작해 보겠습니다.
Rust로 작성된 Python 인터프리터인 Memphis에는 두 개의 실행 엔진이 있습니다. 둘 다 모든 코드를 실행할 수는 없지만 둘 다 일부 코드를 실행할 수는 있습니다.
내 treewalk 인터프리터는 자신이 무엇을 하고 있는지 모른다면 만들 수 있는 것입니다. ?♂️ 입력 Python 코드를 토큰화하고 추상 구문 트리(AST)를 생성한 다음 트리를 살펴보고 각 노드를 평가합니다. 표현식은 값을 반환하고 명령문은 Python 범위 지정 규칙을 준수하는 일련의 범위로 구현되는 기호 테이블을 수정합니다. 쉬운 뉴모닉 LEGB(로컬, 엔클로징, 글로벌, 내장)를 기억하세요.
내 바이트코드 VM은 자신이 무엇을 하고 있는지 모르지만 자신이 했던 것처럼 행동하고 싶을 때 구축할 것입니다. 또한 ?♂️. 이 엔진의 경우 토큰과 AST는 동일하게 작동하지만 걷기보다는 질주를 시작합니다. AST를 이하 바이트코드로 알려진 중간 표현(IR)으로 컴파일합니다. 그런 다음 개념적으로 CPU처럼 작동하여 바이트코드 명령을 순서대로 실행하지만 완전히 소프트웨어로 구현되는 스택 기반 가상 머신(VM)을 만듭니다.
(두 가지 접근 방식을 모두 혼란 없이 완벽하게 안내하려면 Crafting Interpreters가 훌륭합니다.)
애초에 우리는 왜 이런 일을 하는 걸까요? 이식성과 성능이라는 두 가지 P를 기억하세요. 2000년대 초반에는 Java 바이트코드의 이식성에 대해 아무도 입을 다물지 않았던 것을 기억하십니까? JVM만 있으면 모든 시스템에서 컴파일된 Java 프로그램을 실행할 수 있습니다! Python은 기술적인 이유와 마케팅적인 이유로 이 접근 방식을 선택하지 않았지만 이론적으로는 동일한 원칙이 적용됩니다. (실제로는 컴파일 단계가 달라서 이 웜캔을 열어본게 후회스럽습니다.)
그래도 성능이 가장 중요합니다. 프로그램 수명 동안 AST를 여러 번 탐색하는 것보다 컴파일된 IR이 더 효율적인 표현입니다. AST를 반복적으로 탐색하는 오버헤드를 피함으로써 성능이 향상되었으며, 플랫 구조로 인해 런타임 시 분기 예측 및 캐시 지역성이 향상되는 경우가 많습니다.
(컴퓨터 아키텍처에 대한 배경 지식이 없다면 캐싱에 대해 생각하지 않는다고 비난하지는 않습니다. 저는 해당 업계에서 경력을 시작했으며 캐싱을 피하는 방법에 대해 생각하는 것보다 훨씬 적게 생각합니다. 동일한 코드 줄을 두 번 작성합니다. 그러니 성능 부분에서는 저를 믿으세요. 그게 제 리더십 스타일입니다: 맹목적인 신뢰.)
안녕 친구, 500 단어입니다. 프레임을 로드하고 찢어버려야 합니다.
벌써?! 코드 조각을 제외하셨나요?
코드 조각은 없습니다.
알았어 알았어. 500개만 더요. 약속합니다.
약 1년 전에 바이트코드 VM 구현을 표로 작성하기 전에는 꽤 많은 시간이 걸렸습니다. Python 함수와 클래스를 정의하고 해당 함수를 호출하고 해당 클래스를 인스턴스화할 수 있었습니다. 나는 몇 가지 테스트를 통해 이 동작을 단속했습니다. 하지만 내 구현이 지저분하고 더 재미있는 것을 추가하기 전에 기본 사항을 다시 살펴봐야 한다는 것을 알고 있었습니다. 이제 크리스마스 주간인데 재미있는 내용을 추가하고 싶습니다.
TODO를 주시하면서 함수를 호출하려면 이 스니펫을 고려하세요.
fn compile_function_call( &mut self, name: &str, args: &ParsedArguments) ) -> Result<Bytecode, CompileError> { let mut opcodes = vec![]; // We push the args onto the stack in reverse call order so that we will pop // them off in call order. for arg in args.args.iter().rev() { opcodes.extend(self.compile_expr(arg)?); } let (_, index) = self.get_local_index(name); // TODO how does this know if it is a global or local index? this may not be the right // approach for calling a function opcodes.push(Opcode::Call(index)); Ok(opcodes) }
고려가 끝났나요? 함수 인수를 스택에 로드하고 "함수를 호출"합니다. 바이트코드에서는 모든 이름이 인덱스로 변환되지만(VM 런타임 동안 인덱스 액세스가 더 빠르기 때문에) 여기서는 로컬 인덱스를 다루고 있는지, 글로벌 인덱스를 다루고 있는지 알 수 있는 방법이 없습니다.
이제 개선된 버전을 살펴보세요.
fn compile_function_call( &mut self, name: &str, args: &ParsedArguments) ) -> Result<Bytecode, CompileError> { let mut opcodes = vec![self.compile_load(name)]; // We push the args onto the stack in reverse call order so that we will pop // them off in call order. for arg in args.args.iter().rev() { opcodes.extend(self.compile_expr(arg)?); } let argc = opcodes.len() - 1; opcodes.push(Opcode::Call(argc)); Ok(opcodes) }
해당 코드를 고려해 주셔서 감사합니다.
이제 중첩된 함수 호출을 지원합니다! 무엇이 바뀌었나요?
compile_load가 무엇을 하는지 살펴보겠습니다.
fn compile_load(&mut self, name: &str) -> Opcode { match self.ensure_context() { Context::Global => Opcode::LoadGlobal(self.get_or_set_nonlocal_index(name)), Context::Local => { // Check locals first if let Some(index) = self.get_local_index(name) { return Opcode::LoadFast(index); } // If not found locally, fall back to globals Opcode::LoadGlobal(self.get_or_set_nonlocal_index(name)) } } }
여기에는 몇 가지 핵심 원칙이 적용됩니다.
오늘 마지막으로 알려드릴 내용은 이러한 변수 이름이 어떻게 매핑되는지 살펴보는 것입니다. 아래 코드 조각에서 로컬 인덱스는 code.varnames에 있고 로컬이 아닌 인덱스는 code.names에 있음을 알 수 있습니다. 둘 다 변수 및 이름 매핑을 포함하여 Python 바이트코드 블록에 대한 메타데이터가 포함된 CodeObject에 있습니다.
fn compile_function_call( &mut self, name: &str, args: &ParsedArguments) ) -> Result<Bytecode, CompileError> { let mut opcodes = vec![]; // We push the args onto the stack in reverse call order so that we will pop // them off in call order. for arg in args.args.iter().rev() { opcodes.extend(self.compile_expr(arg)?); } let (_, index) = self.get_local_index(name); // TODO how does this know if it is a global or local index? this may not be the right // approach for calling a function opcodes.push(Opcode::Call(index)); Ok(opcodes) }
varname과 이름의 차이 때문에 몇 주 동안 괴로웠지만(CPython에서는 이를 co_varname과 co_name이라고 부릅니다) 실제로는 매우 간단합니다. varnames는 특정 범위의 모든 지역 변수에 대한 변수 이름을 보유하고 이름은 모든 비지역 변수에 대해 동일하게 수행됩니다.
이를 제대로 추적하면 다른 모든 것은 잘 작동합니다. 런타임 시 VM은 LOAD_GLOBAL 또는 LOAD_FAST를 확인하고 각각 전역 네임스페이스 사전 또는 로컬 스택을 찾는 방법을 알고 있습니다.
친구! 구텐베르그 씨가 전화를 해서 더 이상 인쇄기를 보류할 수 없다고 말했습니다.
알겠습니다! 괜찮은! 알겠습니다! 배송해드려요. ?
쉿! 인쇄기사님은 제가 결론을 쓰고 있는 줄 모르셔서 짧게 말씀드리겠습니다.
변수 범위 지정과 함수 호출을 탄탄하게 활용하면서 점차 스택 추적 및 비동기 지원과 같은 기능에 관심을 기울이고 있습니다. 바이트코드에 대한 이 다이빙이 즐거웠거나 자신만의 인터프리터 구축에 대해 질문이 있는 경우, 귀하의 의견을 듣고 싶습니다. 댓글을 남겨주세요!
구독 및 할인 [아무것도 없음]
이런 게시물을 받은편지함으로 직접 받아보려면 여기에서 구독하세요!
저와 함께 일하세요
저는 때때로 어리석은 지원 환경에서 기술적인 문제를 해결하고 경력 성장을 이룰 수 있도록 소프트웨어 엔지니어를 멘토링합니다. 관심이 있으시면 여기에서 세션을 예약하실 수 있습니다.
다른 곳
멘토링 외에도 자영업과 자폐증을 늦게 진단받은 경험에 대해서도 글을 씁니다. 코드는 적고 농담 수는 동일합니다.
위 내용은 Python 바이트코드에 중첩 함수에 대한 지원을 추가하는 방법의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!