파이썬 프로그램을 빠르게 실행

참고: 이 메시지는 martinheinz.dev
파이톤을 싫어하는 사람들은 항상 그것을 사용하고 싶지 않은 이유 중 하나가 속도가 너무 느린 것이라고 말한다.응, 구체적인 프로그램. 어떤 프로그래밍 언어를 사용하든지, 빠르든지, 느리든지, 어느 정도는 그 프로그램을 작성하는 개발자, 그리고 최적화되고 빠른 프로그램을 작성하는 기능과 능력에 달려 있다.
그래서 일부 사람들이 틀렸다는 것을 증명하고 파이톤 프로그램의 성능을 어떻게 향상시키고 진정으로 빠르게 하는지 봅시다!

시간 및 분석
우리가 어떤 것을 최적화하기 전에 코드의 어떤 부분이 실제적으로 프로그램 전체의 속도를 떨어뜨릴지 찾아내야 한다.때때로 프로그램의 병목이 뚜렷할 수 있지만, 프로그램이 어디에 있는지 모르면 다음과 같은 옵션을 찾을 수 있습니다.
주의: 이것은 내가 프레젠테이션에 사용할 프로그램입니다. e 에서 X 까지의 멱 (Python 문서에서 추출) 을 계산합니다.
# slow_program.py
from decimal import *

def exp(x):
    getcontext().prec += 2
    i, lasts, s, fact, num = 0, 0, 1, 1, 1
    while s != lasts:
        lasts = s
        i += 1
        fact *= i
        num *= x
        s += num / fact
    getcontext().prec -= 2
    return +s

exp(Decimal(150))
exp(Decimal(400))
exp(Decimal(3000))

가장 게으른 분석
우선 가장 간단하면서도 게으른 해결 방안인 Unixtime 명령:
~ $ time python3.8 slow_program.py

real    0m11,058s
user    0m11,050s
sys     0m0,008s
만약 당신이 전체 프로젝트를 위해 시간을 재려고 한다면, 이것은 효과가 있을 수 있지만, 이것은 통상적으로 부족하다.

가장 상세한 분석
다른 한쪽은cProfile입니다. 너무 많은 정보를 줄 것입니다.
~ $ python3.8 -m cProfile -s time slow_program.py
         1297 function calls (1272 primitive calls) in 11.081 seconds

   Ordered by: internal time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        3   11.079    3.693   11.079    3.693 slow_program.py:4(exp)
        1    0.000    0.000    0.002    0.002 {built-in method _imp.create_dynamic}
      4/1    0.000    0.000   11.081   11.081 {built-in method builtins.exec}
        6    0.000    0.000    0.000    0.000 {built-in method __new__ of type object at 0x9d12c0}
        6    0.000    0.000    0.000    0.000 abc.py:132(__new__)
       23    0.000    0.000    0.000    0.000 _weakrefset.py:36(__init__)
      245    0.000    0.000    0.000    0.000 {built-in method builtins.getattr}
        2    0.000    0.000    0.000    0.000 {built-in method marshal.loads}
       10    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap_external>:1233(find_spec)
      8/4    0.000    0.000    0.000    0.000 abc.py:196(__subclasscheck__)
       15    0.000    0.000    0.000    0.000 {built-in method posix.stat}
        6    0.000    0.000    0.000    0.000 {built-in method builtins.__build_class__}
        1    0.000    0.000    0.000    0.000 __init__.py:357(namedtuple)
       48    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap_external>:57(_path_join)
       48    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap_external>:59(<listcomp>)
        1    0.000    0.000   11.081   11.081 slow_program.py:1(<module>)
...
내부 시간(cProfile에 따라 줄을 정렬하기 위해 time 모듈과 cumtime 파라미터를 사용하여 테스트 스크립트를 실행합니다.이것은 우리에게 많은 정보를 주었다. 너는 위의 선이 대략 실제 출력의 10퍼센트 정도라는 것을 볼 수 있다.이 점에서 우리는 exp 함수가 원흉(서프라이즈, 서프라이즈)이라는 것을 알 수 있다. 현재 우리는 더욱 구체적인 시간과 분석을 얻을 수 있다...

특정 기능 타이밍
주의력을 어디로 끌어들일지 알았으니 코드의 나머지 부분을 측정하지 않은 상태에서 느린 함수에 대해 시간을 재기를 원할 수도 있다.이를 위해 우리는 간단한 장식기를 사용할 수 있다.
def timeit_wrapper(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()  # Alternatively, you can use time.process_time()
        func_return_val = func(*args, **kwargs)
        end = time.perf_counter()
        print('{0:<10}.{1:<8} : {2:<8}'.format(func.__module__, func.__name__, end - start))
        return func_return_val
    return wrapper
그런 다음 이 장식기를 다음과 같이 테스트의 함수에 적용할 수 있습니다.
@timeit_wrapper
def exp(x):
    ...

print('{0:<10} {1:<8} {2:^8}'.format('module', 'function', 'time'))
exp(Decimal(150))
exp(Decimal(400))
exp(Decimal(3000))
이것은 우리에게 이런 출력을 주었다.
~ $ python3.8 slow_program.py
module     function   time  
__main__  .exp      : 0.003267502994276583
__main__  .exp      : 0.038535295985639095
__main__  .exp      : 11.728486061969306
고려해야 할 것은 우리가 실제로 측정하고 싶은 시간이다.시간제 제공time.perf_countertime.process_time.Python 프로그램 프로세스가 실행되지 않은 시간을 포함하여 perf_counter 절대값을 되돌려줍니다. 따라서 기계 부하의 영향을 받을 수 있습니다.다른 한편 process_time 은 사용자 시간 (시스템 시간 포함) 만 되돌려줍니다. 이것은 프로세스의 시간일 뿐입니다.

속력을 내다
이제 재미있는 부분을 보도록 하겠습니다.파이썬 프로그램을 더 빨리 실행합시다.나는 당신에게 신기하게 성능 문제를 해결할 수 있는 해커, 기교, 코드 세션을 보여 주지 않을 것입니다.이것은 일반적인 생각과 전략에 대한 것이다. 그것을 사용할 때 성능에 큰 영향을 미치고, 어떤 경우에는 속도가 30% 높아진다.

내장 데이터 유형 사용
이 점은 매우 뚜렷하다.내장된 데이터 형식은 매우 빠르며, 특히 나무나 체인 테이블 등 사용자 정의 형식에 비해 빠르다.이것은 내장 프로그램이 C 언어로 이루어졌기 때문에 파이톤으로 코드를 작성할 때 우리는 속도적으로 C 언어와 일치할 수 없다.

사용lru_cache 캐시/로깅
나는 이전의 블로그 글here에서 이미 이 점을 보여 주었지만, 나는 간단한 예로 다시 한 번 반복할 가치가 있다고 생각한다.
import functools
import time

# caching up to 12 different results
@functools.lru_cache(maxsize=12)
def slow_func(x):
    time.sleep(2)  # Simulate long computation
    return x

slow_func(1)  # ... waiting for 2 sec before getting result
slow_func(1)  # already cached - result returned instantaneously!

slow_func(3)  # ... waiting for 2 sec before getting result
상기 함수는 time.sleep를 사용하여 복잡한 계산을 시뮬레이션한다.처음 파라미터 1 를 호출할 때, 2초를 기다린 다음 결과를 되돌려줍니다.다시 호출할 때 결과는 캐시되어 있기 때문에 함수체를 건너뛰고 결과를 즉시 되돌려줍니다.더 많은 실제 생활에 관한 예는 앞의 박문here을 참조하십시오.

로컬 변수 사용
이것은 모든 범위 내에서 변수를 찾는 속도와 관계가 있다.나는 모든 범위를 쓴다. 왜냐하면 그것은 단순히 국부 변수와 전역 변수를 사용하는 것에 관한 것이 아니기 때문이다.실제로 함수의 국부 변수(가장 빠른), 클래스 속성(예를 들어self.name-비교적 느린)과 전역(예를 들어 가져온 함수, 예를 들어time.time(가장 느린) 사이에도 검색 속도가 다르다.
불필요해 보이는 임무를 사용하여 실적을 높일 수 있다. 예를 들어 다음과 같다.
#  Example #1
class FastClass:

    def do_stuff(self):
        temp = self.value  # this speeds up lookup in loop
        for i in range(10000):
            ...  # Do something with `temp` here

#  Example #2
import random

def fast_function():
    r = random.random
    for i in range(10000):
        print(r())  # calling `r()` here, is faster than global random.random()

함수 사용
이것은 직감과 상반되는 것 같다. 함수를 호출하면 더 많은 내용을 창고에 넣고, 함수로부터 되돌아오는 데 비용이 발생하기 때문이다. 그러나 이것은 앞의 부분과 관련이 있다.만약 전체 코드를 하나의 파일에 넣기만 하고 함수에 넣지 않는다면, 전역 변수가 존재하기 때문에 속도가 훨씬 느릴 것이다.따라서 전체 코드를 main 함수에 포장하고 한 번만 호출하면 다음과 같이 코드의 속도를 높일 수 있다.

def main():
    ...  # All your previously global code

main()

속성에 액세스하지 않음
프로그램 속도를 늦출 수 있는 또 다른 방법은 점 연산자.로 대상 속성에 접근할 때 사용된다.이 연산자는 __getattribute__ 사전 검색을 터치합니다. 코드에 추가 비용이 발생합니다.그렇다면 우리는 어떻게 해야만 진정으로 그것을 사용하는 것을 피할 수 있습니까?
#  Slow:
import re

def slow_func():
    for i in range(10000):
        re.findall(regex, line)  # Slow!

#  Fast:
from re import findall

def fast_func():
    for i in range(10000):
        findall(regex, line)  # Faster!

밧줄 조심
순환에서 모드 ((%s 를 사용하거나 .format() 를 실행할 때 문자열의 동작이 매우 느릴 수 있습니다.우리는 또 어떤 더 좋은 선택이 있습니까?최근의 연구를 바탕으로 우리가 유일하게 사용해야 할 것은 f-string이다. 이것은 가장 읽을 수 있고 간결하며 가장 빠른 방법이다.이 트윗에 따르면 가장 빠르고 느린 방법 목록은 다음과 같습니다.
f'{s} {t}'  # Fast!
s + '  ' + t 
' '.join((s, t))
'%s %s' % (s, t) 
'{} {}'.format(s, t)
Template('$s $t').substitute(s=s, t=t)  # Slow!

발전기는 매우 빠를 수 있다
생성기의 속도는 천성적으로 빠른 것이 아니다. 왜냐하면 계산이 지연되고 시간이 아닌 메모리를 절약하기 위해서이다.그러나 절약된 메모리는 프로그램이 실제적으로 더 빨리 실행될 수 있다.어떻게 써요?알겠습니다. 만약 큰 데이터 집합이 있고, 생성기 (교체기) 를 사용하지 않는다면, 데이터는 CPU 1급 캐시를 넘을 수 있습니다. 이것은 메모리 중치의 검색 속도를 크게 낮출 수 있습니다.
CPU는 성능 면에서 캐시에 있는 모든 데이터를 최대한 가까이서 저장하는 것이 중요합니다.너는 그가 어디에서 이 문제를 언급했는지 볼 수 있다.

결론
최적화의 첫 번째 규칙은 이렇게 하지 않는 것이다.하지만 만약 당신이 정말 이렇게 해야 한다면, 나는 이 몇 가지 건의가 당신이 이 점을 해낼 수 있도록 도와줄 수 있기를 바랍니다.파이썬 글을 더 읽고 싶다면, 테스트와 일반적인 기교에 관한 블로그 글을 보고 싶을 수도 있다.

좋은 웹페이지 즐겨찾기