Python 3.8에 추가된 per opcode cache 정보

8426 단어 Python-devPython
Python 3.8에서 LOAD 가져오기1GLOBAL 명령에 사용되는 per-opcode cache에 대해 살펴보겠습니다.

LOAD_GLOBAL 명령


Python의 전역 변수 로드가 로컬 변수 로드보다 느립니다.따라서 글로벌 변수에 여러 번 액세스할 때 로컬 변수에 저장하는 기술이 있습니다.
$ python3 -m timeit -s '
> def foo():
>     for _ in range(1000):
>         sum
> ' -- 'foo()'
10000 loops, best of 5: 29.9 usec per loop

$ python3 -m timeit -s '
def foo():
    _sum = sum
    for i in range(1000):
        _sum
' -- 'foo()'
20000 loops, best of 5: 16.7 usec per loop
이 예에서 속도는 2배도 안 되지만 순환하는 비용이 포함되어 있기 때문에 명령을 불러오는 단일체의 속도는 3배 정도 된다.
로컬 변수는 함수를 컴파일할 때 정수로 인덱스되고, 이 인덱스로 로컬 변수의 그룹에 접근하기 때문에 속도가 매우 빠르다.
>>> def foo():
...     s = 1
...     s
...
>>> import dis
>>> dis.dis(foo)
  2           0 LOAD_CONST               1 (1)
              2 STORE_FAST               0 (s)

  3           4 LOAD_FAST                0 (s)   # 0 番目のローカル変数をロードする命令
              6 POP_TOP
              8 LOAD_CONST               0 (None)
             10 RETURN_VALUE
다른 한편, 전역 변수를 불러오는 것은 다음과 같다.
  • 컴파일할 때 이름 일람의 배열 만들기
  • LOAD_이름의 인덱스가 GLOVAL 명령에 전달되는 매개변수
  • 인덱스로 접근해서 이름 일람열에서 이름 꺼내기
  • 모듈의 전역 변수 dict에서 이 이름 검색
  • (존재하지 않을 때) 내장 변수 dict에서 검색
  • >>> def foo():
    ...     sum
    ...
    >>> dis.dis(foo)
      2           0 LOAD_GLOBAL              0 (sum)  # 0番目の名前 (="sum") でグローバル変数をロードする
                  2 POP_TOP
                  4 LOAD_CONST               0 (None)
                  6 RETURN_VALUE
    >>> foo.__code__.co_names  # 名前一覧配列
    ('sum',)
    
    검색이 너무 많은 dict가 그룹 접근보다 느리면 두 번 실행하기 때문에 전역 변수 접근에 시간이 걸립니다.

    dict 버전


    Pythn 3.6에서 dict의 설치를 변경할 때 변수 ma_version 가 dict에 추가됩니다.
    dict를 변경할 때마다 전역 계수기를 증가시켜 마-version에 설치합니다.따라서 다른 dict나 같은 dict에 변경 사항이 있으면version이 다르다.전역 변수 dict와 내부 변수 dict의 버전을 기억하면 두 번째 이후에 이전 검색 결과를 다시 사용할 수 있습니다.

    per opcode cache


    LOAD_GLOBAL 명령을 고속화하려는 경우에만 LOAD또한 GLOBAL 명령 매개 변수에 액세스하는 이름으로 정렬된 인덱스를 키로 사용하여 캐시를 사용할 수 있습니다.하지만 앞으로 다른 명령도 캐시가 이뤄질 것을 고려해 peropcode cache를 실시했다.

    Python의 opcode는 명령마다 2바이트(word code라고도 함).함수가 1000번 실행될 때 캐시 영역을 확보하십시오.캐시는 비교적 큰 데이터 2 를 필요로 하기 때문에 명령마다 바이트의 색인표를 만듭니다.최종 명령 수 1+LOAD-GLOBAL 명령 수(캐시 엔트리 크기) 바이트 영역을 사용합니다.

    효과

    $ local/py38/bin/python3 -m timeit -s '
    > def foo():
    >     for _ in range(1000):
    >         sum
    > ' -- 'foo()'
    10000 loops, best of 5: 20.4 usec per loop
    
    $ local/py38/bin/python3 -m timeit -s '
    > def foo():
    >     _sum = sum
    >     for _ in range(1000):
    >         _sum
    > ' -- 'foo()'
    20000 loops, best of 5: 18.1 usec per loop
    
    서로 다른 조건하에서 컴파일된 이진법이기 때문에 첫 번째 파이톤 3.7의 결과와 직접 비교할 수 없지만 로컬 변수와의 접근 차이가 작아진 것을 알 수 있다.고속화를 위해 일시적으로 로컬 변수에 놓는 기교는 별로 추천되지 않겠죠.
    실제 라이브러리의 기준 세트 (pypperformance) 를 사용한 결과는 캐시를 가져오기 전후에 이렇습니다.
    $ ./cpython/python -m perf compare_to master.json opcache_load_global.json -G  --min-speed=2
    Slower (2):
    - pickle: 19.1 us +- 0.2 us -> 19.7 us +- 0.8 us: 1.03x slower (+3%)
    - unpickle_list: 8.66 us +- 0.04 us -> 8.85 us +- 0.06 us: 1.02x slower (+2%)
    
    Faster (23):
    - scimark_lu: 424 ms +- 22 ms -> 384 ms +- 4 ms: 1.10x faster (-9%)
    - regex_compile: 359 ms +- 4 ms -> 330 ms +- 1 ms: 1.09x faster (-8%)
    - django_template: 250 ms +- 3 ms -> 231 ms +- 2 ms: 1.08x faster (-8%)
    - unpickle_pure_python: 802 us +- 12 us -> 754 us +- 9 us: 1.06x faster (-6%)
    - pickle_pure_python: 1.04 ms +- 0.01 ms -> 991 us +- 15 us: 1.05x faster (-5%)
    - hexiom: 20.8 ms +- 0.2 ms -> 19.8 ms +- 0.1 ms: 1.05x faster (-5%)
    - logging_simple: 18.4 us +- 0.2 us -> 17.6 us +- 0.2 us: 1.05x faster (-4%)
    - sympy_expand: 774 ms +- 5 ms -> 741 ms +- 3 ms: 1.04x faster (-4%)
    - json_dumps: 28.1 ms +- 0.2 ms -> 27.0 ms +- 0.2 ms: 1.04x faster (-4%)
    - logging_format: 20.4 us +- 0.2 us -> 19.6 us +- 0.3 us: 1.04x faster (-4%)
    - richards: 147 ms +- 2 ms -> 141 ms +- 1 ms: 1.04x faster (-4%)
    - meteor_contest: 189 ms +- 1 ms -> 182 ms +- 1 ms: 1.04x faster (-4%)
    - xml_etree_iterparse: 226 ms +- 2 ms -> 217 ms +- 2 ms: 1.04x faster (-4%)
    - sympy_str: 358 ms +- 3 ms -> 345 ms +- 4 ms: 1.04x faster (-4%)
    - sqlalchemy_imperative: 44.0 ms +- 1.2 ms -> 42.4 ms +- 1.2 ms: 1.04x faster (-4%)
    - sympy_sum: 167 ms +- 1 ms -> 161 ms +- 1 ms: 1.04x faster (-4%)
    - nqueens: 217 ms +- 1 ms -> 211 ms +- 1 ms: 1.03x faster (-3%)
    - fannkuch: 1.09 sec +- 0.01 sec -> 1.07 sec +- 0.00 sec: 1.03x faster (-3%)
    - raytrace: 1.11 sec +- 0.02 sec -> 1.08 sec +- 0.01 sec: 1.03x faster (-3%)
    - dulwich_log: 122 ms +- 1 ms -> 119 ms +- 1 ms: 1.03x faster (-3%)
    - logging_silent: 419 ns +- 5 ns -> 410 ns +- 5 ns: 1.02x faster (-2%)
    - sympy_integrate: 33.5 ms +- 0.1 ms -> 32.8 ms +- 0.2 ms: 1.02x faster (-2%)
    - pathlib: 40.8 ms +- 0.4 ms -> 40.0 ms +- 0.5 ms: 1.02x faster (-2%)
    

    향후


    LOAD_GLOBAL 작업보다 복잡하고 어렵지만, 속성 액세스를 위한 LOADATTR, LOAD_나는 METHOD에 대한 캐시를 실현하고 싶다.
    또한 {"a": foo(), "b": bar()}처럼 구축 버튼은 문자열로만 구성된 dict display의 CONSTMAP_KEY라는 명령이 있었지만 이 명령을 사용할 때 매번 해시표를 구축하지 말고 구축된 해시표에서 dict를 빨리 만들어야 한다는 생각도 있었다.
    다른 한편, 1000번을 실행하면 현금 캐시를 구축하는 방법이 너무 간단하기 때문에 이 방면의 개량이 필요할 수 있다.
    베타 1 전에 꼬집어 넣었기 때문에 3.8 발매 전에 리버트에 걸렸을 수도 있어요. 
    LOAD_GLOBAL의 경우 32바이트이지만 앞으로 다른 명령의 캐시가 실행되면 더 커질 수 있다. 

    좋은 웹페이지 즐겨찾기