CPython의 피-홀 최적화기는 무조건 점프(JUMP_ABSOLUTE, JUMP_FORWARD, RETURN_VALUE, RAISE_VARARGS) 후 다른 블록에서의 진입점이 없는 도달할 수 없는 블록을 찾아 바이트코드를 스캔합니다. 식별되면, 이 죽은 명령어들을 제거하여 캐시 압력을 줄이고 명령 밀도를 높입니다.
Python의 예외 처리 테이블, 루프 구조 및 조건부 점프는 목표 위치를 코드 객체의 co_code 시퀀스에 대한 절대 바이트 오프셋으로 저장하므로, 최적화기는 생존하는 각 명령어 앞에서 삭제된 바이트 수를 추적하는 재배치 맵을 구축해야 합니다. 그런 다음 모든 점프 명령어와 예외 처리기 범위를 통해 반복하면서, 대상 위치에서 누적 삭제 수를 빼서 목표 오프셋을 조정합니다. 이를 통해 SETUP_FINALLY 블록, FOR_ITER 루프 및 사용자 정의 점프가 이전 바이트코드가 압축된 후에도 올바른 opcode에 도달하도록 보장합니다.
데이터 파이프라인 팀은 ETL 유틸리티의 시작 스크립트에 if DEBUG: 플래그로 보호된 방대한 디버그 로깅 블록이 포함되어 있다는 것을 발견했습니다. 여기서 **DEBUG**는 **False**로 설정된 모듈 수준의 상수였습니다. 조건이 정적으로 거짓임에도 불구하고, 컴파일된 바이트코드에는 여전히 로깅 논리가 포함되어 있어 .pyc 파일 크기를 40% 증가시키고 프로덕션 서버에서 명령어 캐시 지역성을 약간 저하시켰습니다.
그들은 세 가지 명확한 접근 방식을 평가했습니다.
첫 번째, 그들은 C 전처리기 또는 Jinja2 템플릿을 사용하여 배포 전에 디버그 코드를 제거하는 것을 고려했습니다. 이 접근 방식은 프로덕션에서 디버그 바이트코드가 0인 것을 보장했지만, 복잡한 빌드 단계 의존성을 도입하고 개발과 프로덕션 코드베이스 간 섬세한 차이를 초래하여 소스 코드가 실행 중인 바이트코드와 더 이상 일치하지 않는 경우의 프로덕션 문제 디버깅을 복잡하게 만들 위험이 있었습니다.
두 번째, 그들은 모든 디버그 블록을 서브모듈의 별도 함수로 리팩토링하는 것을 평가하여 호출되지 않은 함수가 로드되지 않기를 희망했습니다. 하지만 Python의 import 시스템은 전체 모듈을 한 번에 컴파일하며, 호출되지 않은 함수는 모듈의 딕셔너리에 코드 객체로 남아 있습니다. 피-홀 최적화기는 절차 간의 죽은 코드 제거를 수행하지 않기 때문에 바이트코드 크기는 변경되지 않았습니다.
세 번째, 그들은 CPython의 컴파일 파이프라인을 조사하여 피-홀 최적화기가 if False: 구조 뒤의 코드를 자동으로 제거한다는 것을 발견했습니다. 컴파일러는 블록 주위에 무조건 점프를 생성하고 피-홀 패스는 도달할 수 없는 꼬리를 삭제합니다. dis 모듈로 RETURN_VALUE 또는 JUMP_FORWARD 뒤에 죽은 코드가 없음을 확인하여 최적화가 활성화되었음을 확인했습니다. 그들은 이 내장 메커니즘에 의존하기로 결정하여 **DEBUG**가 런타임에서 계산된 변수가 아닌 리터럴 **False**가 되도록 하여 추가 도구 없이 컴파일된 바이트코드 크기를 35% 줄였습니다.
왜 피-홀 최적화기는 이전 점프 목표가 계산된 점프 명령어에 의해 주소 지정될 때 도달할 수 없는 코드를 제거하지 않습니까?
계산된 점프는 스택의 값에 따라 런타임에서 도착지를 결정하며, 예를 들어 MATCH 명령문이나 동적 디스패치 패턴에서와 같습니다. 최적화기는 어떤 오프셋이 목표가 될 수 있는지 정적으로 알 수 없기 때문에, 모든 명령어가 진입점이 될 수 있다고 보수적으로 가정해야 합니다. 따라서, 최적화기는 무조건 점프 및 제어 흐름 그래프의 정적 분석을 통해 증명 가능한 도달할 수 없는 코드만 삭제하며, 정의되지 않은 동작을 방지하기 위해 동적 디스패치의 목표가 될 수 있는 블록은 보존합니다.
최적화기는 NOP 명령어를 사용하는 점프 자리 표시자를 삭제할 때 예외 처리기 테이블(co_exceptiontable)을 어떻게 처리합니까?
컴파일러가 아직 알려지지 않은 위치로 점프를 생성할 때, 보통 NOP (no-operation) 명령어를 자리 표시자나 패딩으로 방출한 다음 나중에 점프 목표를 수정합니다. 피-홀 최적화 중, 이러한 NOP는 공간을 절약하기 위해 제거됩니다. 최적화기는 원본 오프셋과 최종 오프셋 간의 양방향 매핑을 유지합니다. 예외 테이블을 처리할 때—try/except 블록의 start, end 및 handler 오프셋을 저장하는—삭제된 바이트의 누적 델타를 각 항목에 적용합니다. **NOP**가 예외 범위에 포함되면, 그 제거는 end 오프셋을 왼쪽으로 이동시켜 보호된 바이트코드 범위가 정확하게 유지되고 예외가 올바른 경계에서 잡히도록 합니다.
피-홀 최적화기가 C 컴파일러에서 보는 것처럼 독립적인 명령어의 순서를 변경하여 파이프라인 효율성을 개선하지 못하게 하는 것은 무엇입니까?
Python의 바이트코드는 추적 생성에 사용되는 평가 스택 의미 및 행 번호 테이블과 밀접하게 연결되어 있습니다. 명령어의 순서를 변경하는 것은 예를 들어 **LOAD_CONST**를 **LOAD_NAME**보다 앞서 이동하는 것과 같은 경우에는 예외가 발생할 때 스택의 상태를 변경할 수 있으며, 이는 추적 내의 보고된 행 번호를 변경하거나 인터프리터 루프에 필요한 스택 깊이 불변을 위반할 수 있습니다. 또한 Python은 프레임 객체 및 f_lasti(명령 포인터)의 반추적을 허용하기 때문에, 임의의 순서 변경은 결정론적 오프셋-소스 매핑에 의존하는 디버거 및 프로파일러를 방해할 수 있습니다. 따라서 최적화기는 도달할 수 없는 코드를 삭제하고 점프를 리디렉션하는 것 외에는 실행 가능한 명령어의 상대적 순서를 변경하지 않도록 제한됩니다.