ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • CPython Internals
    Software Development/Python 2024. 6. 1. 00:00

    해당 글은 CPython Internals 책을 읽으면서 중요하게 봤었던 내용의 일부를 요약한 것입니다. 자세한 내용은 아라의 책에 있습니다.
    https://product.kyobobook.co.kr/detail/S000061776273

     

    CPython 파헤치기 | 앤서니 쇼 - 교보문고

    CPython 파헤치기 | 인터프리터 레벨에서 살펴보는 파이선 내부 동작 원리파이썬이 인터프리터 레벨에서 작동하는 방식을 이해하면 파이썬의 기능을 최대한 활용할 수 있고 애플리케이션을 최적

    product.kyobobook.co.kr

    "아무리 성공적인 혁신이라도 한때는 아이디어였다. 헌신, 학습, 협력이 성공과 성장을 위한 기반이다. 지금 하는 게 아예 안 하는 것보다 낫다."

    파이썬 언어와 문법

    컴파일러: 한 언어를 다른 언어로 변환해줌

    파이썬 코드는 기계어 대신 바이트코드(.pyc 파일에 저장되고 실행을 위해 캐싱된다)라는 저수준 중간 언어로 컴파일된다.

    cpython repository의 해당 디렉토리에 파이썬 언어의 기능을 설명하는 rst파일이 있음

    with A() as a, B() as b:
        SUITE
    # 동일
    with A() as a:
        with B() as b:
            SUITE
    # 동일
    with (
        A() as a,
        B() as b,
    ):
        SUITE

    cpython/Grammar /python.gram에 기계가 읽을 수 있는 사양이 있음

    # python.gram 파일을 수정하고
    make regen-pegen
    make
    # 하면 새로운 문법 추가할 수 있음
    
    # 토큰 목록 출력(Grammar 디렉토리 안에 있음)
    ./python.exe -m tokenize -e test_token.py
    
    # 디버그 빌드를 -d 플래그로 실행하면 C 파서가 실행되는 과정을 자세히 볼 수 있음.
    # 이떄 ./configure --with-pydebug make로 빌드해야 함.
    
    ./python.exe -d test_token.py

    렉싱과 파싱

    cpython은 CST(concrete syntax tree), AST(abstract syntax tree) 두가지 구조를 사용.

    렉서가 CST 생성. 파서가 AST 생성.

    파서는 일력 스트림으로 들어오는 토큰들이 문법적으로 허용된느 토큰과 상태인지 확인하며 CST를 생성.

    '파서-토크나이저'는 커서가 텍스트 입력의 끝에 도달하거나 문법 오류가 발견될 때 까지 파서와 토크나이저 실행.

     

    추상 구문 트리

    CST로 기본적인 문법 구조는 알 수 있지만 함수, 스코프, 루프 같은 파이썬 언어 사양에 대한 의미를 결정할 수는 없다.

    코드를 컴파일하기 전에 CST를 실제 파이썬 언어 구조와 의미 요소를 표현하는 고수준 구조인 AST로 변환해야 한다.

    컴파일러

    컴파일러는 AST를 CPU 명령으로 바꾼다.

    1. 컴파일러: AST를 순회하며 논리적 실행 순서를 나타내는 제어 흐름 그래프를 생성

    2. 어셈블러: CFG의 노드들을 실행 가능한 명령을 순차적으로 나열한 바이트코드 형태로 변환
    컴파일러 관련 소스 코드
    https://github.com/python/cpython/blob/main/Python/compile.c

    https://github.com/python/cpython/blob/main/Include/compile.h

    컴파일러 state: 심벌 테이블을 담는 컨테이너 타입

    심벌 테이블: 변수 이름을 포함하고 추가로 하위 심벌 테이블을 포함할 수 있음. 컴파일러는 심벌 테이블에서 얻은 namespace에 스코프를 결정하고 참조를 실행

    PyAST_ComepileObject()에 컴파일러 상태와 symtable, AST로 파싱된 모듈이 준비되면 컴파일이 시작된다.

     

    코어 컴파일러가 하는 일

    1. 컴파일러 상태와 심벌 테이블, AST를 CFG로 변환

    2. 논리 오류나 코드 오류를 탐지해 실행 단계를 런타일 예외로부터 보호한다.

     

    내장 함수인 complie()로 컴파일러를 직접 호출할 수 있다.

    co = compile("a+1", "test_compile.py", mode="eval")

    단순한 표현식의 경우에는 "eval", 모듈이나 함수, 클래스일 경우에는 "exec"모드를 사용하자.

    co.co_code 컴파일된 코드는 코드 객체의 co_code 프로퍼티에 담긴다.

     

    바이트코드 역어셈블러 모듈 dis로 화면에 바이트코드를 출력하거나 Instruction 인스턴스 리스트를 얻을 수 있다.

    import dis

    dis.dis(co.co_code)

     

    모든 구문과 표현식 타입에는 해당 타입에 대한 compiler_*() 함수가 존재한다.

     

    어셈블리

    컴파일 단계가 끝나면 프레임 블록 리스트가 완성된다. 각 프레임 블록은 일련의 명령 리스트와 다음 블록을 가리키는 포인터르 ㄹ가진다.

    어셈블러는 기본 프레임 블록들에 대해 깊이 우선 탐색을 실행하고 명령들을 단일한 바이트코드 시퀀스로 병합한다.

     

    평가 루프

    컴파일도니 코드 객체는 바이트코드로 표현된 이산 연산 리스트를 포함한다.

    CPython에서 코드는 평가 루프라는 중심 루프에서 실행된다. CPython 인터프리터는 마셜링된 .pyc 파일이나 컴파일러가 전달한 코드 객체를 평가하고 실행한다.

    평가 루프는 스택 프레임 기반 시스템으로 바이트코드를 실행한다.

    https://github.com/python/cpython/blob/main/Python/ceval.c: 평가 루프 구현 핵심
    https://github.com/python/cpython/blob/main/Python/ceval_gil.c: GIL 제어 알고리즘

    1. 평가 루프는 코드 객체를 입력받아 프레임 객체를 변환

    2. 인터프리터는 최소 1개의 쓰레드를 가진다.

    3. 각 쓰레드는 쓰레드 상태를 가진다.

    4. 프레임 객체는 프레임 스택에서 실행된다.

    5. 값 스택에서 변수를 참조할 수 있다.

     

    CPython은 한 인터프리터 내에서 동시에 여러 스레드를 실행할 수 있다. 인터프리터 상태는 인터프리터 내의 스레드들을 연결 리스트로 관리한다.

     

    스레드 상태

    • 고유 식별자
    • 다른 스레드 상태와 연결된 연결 리스트
    • 스레드를 스폰한 인터프리터의 상태
    • 현재 실행 중인 프레임
    • 현재 재귀 깊이
    • 선택적 추적 함수들
    • 현재 처리 중인 예외
    • 현재 처리 중인 비동기 예외
    • 여러 예외가 발생할 때의 예외 스택
    • GIL 카운터
    • 비동기 제너레이터 카운터

    https://github.com/python/cpython/blob/main/Python/thread.c: 스레드 API 구현
    https://github.com/python/cpython/blob/main/Include/pythread.h: 스레딩 API

     

    sys.setrace()는 인자로 전달받은 함수를 현재 스레드 상태의 기본 추적 함수로 설정한다.

    메모리 관리

    1. 참조 카운팅

    2. 가비지 컬렉션

     

    C에서 변수를 사용하기 위해서는 운영 체제로부터 메모리를 항당받아야 한다.

    1. 정적 메모리 할당: 필요한 메모리는 컴파일 시간에 계산되고 실행 파일이 실행될 때 할당된다.

    static int nuber = 0;

    2. 자동 메모리 할당: 스코프에 필요한 메모리는 프레임에 진입할 때 콜스택내에 할당되고 프레임이 끝나면 해제된다.

    3. 동적 메모리 할당: 메모리 할당 API를 호출해 런타일에 메모리를 동적으로 요청하고 할당

    운영 체제는 프로세스가 메모리를 동적으로 할당할 수 있도록 시스템 메모리의 일부를 예약해 두는데 이 공간을 heap이라고 한다.

     

    CPython 동적 메모리 할당에 크게 의존하면서 가비지 컬렉션과 참조 카운팅 알고리즘을 사용해 메모리를 자동으로 해제하는 안전장치를 추가했다.

     

    파이썬 객체 메모리를 개발자가 직접 할당하는 대신 하나의 통합 API를 통해 자동으로 할당된다. 이러한 메모리 관리 설계로 인해 CPython 표준 파이브러리와 C와 작성된 코어 모듈 전체는 이 통합 API를 사용해야 한다.

    메모리 할당자

    CPython 3가지 메모리 할당자 제공

    1. 저수준 도메인: 시스템 힙이나 대용량 메모리 또는 비객체 메모리를 할당

    2. 객체 도메인: 파이썬 객체와 관련된 메모리를 할당하는 데 사용

    3. PyMem 도메인: PYMEM_DOMAIN_OBJ와 동일하며 레거시 API 용도로 제공

     

    _Alloc: size 바이트만큼 메모리 할당 후 포인터 반환

    _Calloc: nelem개의 elsize 크기 요소들을 할당하고 포인터를 반환

    _Realloc: new_size 크기로 메모리를 재할당

    _Free: ptr 위치의 메모리를 해제하고 힙으로 반환

     

    malloc: 저수준 메모리 도메인을 위한 운영 체제 할당자

    pymalloc: PyMem 도메인과 객체 메모리 도메인을 위한 CPython 할당자

     

    블록, 풀, 아레나

    아레나: 가장 큰 단위의 메모리 그룹 256KB 단위로 할당: 단현화된 메모리보다는 연속적인 메모리를 불러오는 게 더 빠르다.

    시스템 힙에 할당되며 mmap(메모리 매핑) 메모리 매핑은 아레나 힙 단편화를 줄이는데 도움된다.

    풀: 아레나에서 풀에 담을 수 있는 블록의 최대 크기는 512바이트다.

    블록: 풀의 메모리는 블록 단위로 할당된다.

     

    파이썬 디버그 API _debugmallocstats() 이 함수로 메모리 사용량을 확인할 수 있다.

    tracemalloc 모듈로 객체 할당자의 메모리 할당 동작을 디버깅할 수 있다.

     

    python -X tracemalloc=2 test.py

     

    CPython 참조 카운터의 장점

    단순함과 속도 효율성을 제공.

     

    CPython 가비지 컬렉션

    백그라운드에서 실행

    자주 실행되면 CPU 자원을 많이 소모하게된다.

    정해진 양만큼의 연산이 일어났을 때 주기적으로 실행
    https://github.com/python/cpython/blob/main/Modules/gcmodule.c: 모듈과 알고리즘 구현

    참조 카운터의 카운터가 0에 도달하면 객체의 수명이 끝나고 메모리에서 해제된다.

    참조 카운터는 순환 참조를 일으키는 객체들을 완벽하게 처리하지 못한다.

    가비지 컬렉터는 도달할 수 있는 객체들을 찾는 대신 참조 카운터와 특수한 가비지 컬렉션 알고리즘을 사용하여 도달할 수 없는 객체들을 찾는다.

    컬렉션 알고리즘

    초기화

    1. GCState를 인터프리터로부터 얻는다.

    2. 가비지 콜렉터 활성화 여부를 확인

    3. GC가 이미 실행중인지 확인

    4. 수거 함수 collect()를 수거 상태 콜백과 함께 실행

    5. GC 완료 표시

     

    수거 단계

    객체 해제하기

    도달 불가능하다고 결정된 객체들은 다음 단계들을 따라 해제된다.

    세대별 가비지 컬렉션

    세대별 가비지 컬렉션은 대부분의 객체가 생성된 직후 파괴된다는 관측에 근거한 기법이다.

    import gc

    gc.set_debug(gc.DEBUG_STAST)

     

    병렬성과 동시성

    운영체제는 프로세스를 제어하기 위해 새 프로세스를 시작하는 API를 제공한다. PID 부여받고 실행 

    POSIX 프로세스는 운영 체제에 등록된 최소한의 프로퍼티를 가진다. 

    POSIX 시스템은 fork API 제공. 어떤 프로세스든 이 API를 이용해 자식 프로세스를 포크할 수 있다.

    포크하면 현재 실행 중인 프로세스의 모든 어트리뷰트를 복제해 새 프로세스를 생성한다.

    이때 부모의 힙, 레지스터, 카운터 위치도 새 프로세스로 복제된다.

     

    규와 파이프를 사용해 데이터 교환하기

    세마포어

    한 프로세스가 파일이나 네트워크 소켓에 쓰고 있을 때 다른 프로세스가 같은 파일에 쓰기 시작하면 데이터는 바로 손상된다.

    운영체제는 세마포어를 사용해 이를 막는다.

    디버그

    Profile: 파이썬으로 작성된 프로파일러

    cProfile: C로 작성된 빠른 프로파일러

    pythoon -m cProfile test.py

    pythoon -m cProfile -o out.pstat test.py

     

    스네이크비즈를 통한 시각화

    python -m pip install snakeviz

    CPython 기여

    https://github.com/python/cpython/issues

    1. 이슈 번호 확인

    2. 포크에 브랜치를 생성

    3. 테스트 모듈로 이슈를 재현할 수 있는 테스트 추가

    4. 변경사항이 PEP 7, PEP 8 지키는지 확인

    5. 회귀테스트 실행 후 모든 테스트 통과 후 make patchcheck 실행.

    6. 커밋 후 푸쉬

     

    댓글

Designed by Tistory.