진정한 병렬성을 살리려면 concurrent.futures를 사용하라

매일 공부(ML)·2022년 8월 17일
0

이어드림

목록 보기
121/146

진정한 병렬성을 살리려면 concurrent.futures를 사용하라

파이썬 프로그램을 작성하다보면 성능의 벽에 부딪히게 되는데 코드 최적화를 수행해도 필요 수준보다 느릴 수 있다.

그렇다고, C언어로 작성하기에 코드 복잡도가 올라가고 한 부분만 C로 작성하는 것도 쉬운 것은 아니다.

그래서, CPython or SWIG or CLIF와 같은 도구를 활용하면 좋지만, 비용 문제에서 벗어날 수 없다

해결책은 concurrent.futures 내장 모듈을 사용하여 multiprocessing 내장 모듈이 정확히 맞아 떨어지게 한다.


#mymodule.py

def gcd(pair):
    a,b = pair
    low = min(a,b)
    for i in range(low, 0,-1):
        if a % i == 0 and b % i == 0:
            return i
    assert False. '도달할 수 없음'
#run_serial.py
import my_module
import time

NUMBERS = [
    (1963309, 2265973) , (2030677.3814172)
]

def main():
    start = time.time()
    results = list(map(my_module.gcd, NUMBERS))
    end = time.time()
    delta = end - start
    print(f' 총 {delta:.3f} 초 걸림')

if __name__ == '__main__':
    main()

총 0.911초 걸림


#run_threads.py
import my_module
from concurrent.futures import ThreadPoolExecutor
import time

NUMBERS = [
    ...
]

def main():
    start = time.time()
    pool = ThreadPoolExecutor(max_workers=2)
    results = list(pool.map(my_module.gcd, NUMBERS))
    end = time.time()
    delta = end - start
    print(f' 총 {delta:.3f} 초 걸림')

if __name__ == '__main__':
    main()

총 1.436초 걸림


#이제 코드 한 줄만 바꾸면 속도가 빨라진ㄷ
#concurrent.futures 모듈에 있는 ThreadPoolExecutor를 같은 모듈의 ProcessPoolExecutor로 바꾸면
#프로그램 속도가 빨라진다.

##run_parallel.py

import my_module
from concurrent.futures import ProcessPoolExecutor
import time

NUMBERS = [
    ...
]

def main():
    start = time.time()
    pool = ProcessPoolExecutor(max_workers=2) #이 부분만 바꿈
    results = list(pool.map(my_module.gcd, NUMBERS))
    end = time.time()
    delta = end - start
    print(f'총 {delta:.3f} 초 걸림')

if __name__ == '__main__':
	main()

총 0.683 초 걸림


마법같은 일이 벌어진 이유

  • ProcessPoolExecutor 클래스가 multiprocessing 모듈이 제공하는 저수준 요소 활용

  • (부모) 이 객체(ProcessPoolExecutor 인스턴스)는 입력 데이터로 들어온 map 메서드에 전달된 NUMBERS의 각 원소를 취한다.

  • (부모) 이 객체는 1번에서 얻은 원소를 pickle모듈을 사용하여 이진 데이터로 직렬화한다.

  • (부모,자식)이 객체는 로컬 소켓을 통해 주 인터프리터 프로세스부터 자식 인터프리터 프로세스에게 2번에서 직렬화한 데이터를 복사한다.

  • (자식) 이 객체는 pickle를 사용해서 데이터를 파이썬 객체로 역직렬화한다.

  • (자식) 이 객체는 gcd함수가 들어있는 모듈을 임포트한다.

  • (자식) 이 객체는 입력 데이터에 대해 gcd함수를 실행한다. 이때 다른 자식 인터프리터 프로세스와 병렬로 실행한다.

  • (자식) 이 객체는 gcd함수의 결과를 이진 데이터로 직렬화한다.

  • (부모, 자식) 이 객체는 로컬 소켓을 통해 자식 인터프리터 프로세스부터 부모 인터프리터 프로세스에게 7번 직렬화한 결과 데이터를 돌려준다.

  • (부모) 이 객체는 데이터를 파이썬 객체로 역직렬화한다.

  • (부모) 여러 자식 프로세스가 돌려준 결ㄹ과를 병합해서 한 list로 만든다


Summary

  • CPU 병목 지점을 C 확장 모듈로 옮기면 파이썬에 투자한 비용을 최대한 유지하면서 프로그램 성능을 개선하는데 효과적일 수 있다.

  • 그러나, C 확장 모듈로 옮기려면 많은 비용이 들고 포팅하는 과정에서 버그가 생길 수 있다.

  • multiprocessing 모듈을 사용하면 특정 유형의 파이썬 계산을 최소의 노력으로 병렬화할 수 있다.

  • concurrent.futures 내장 모듈이 제공하는 간단한 ProcessPoolExecutor클래스를 활용하면 multiprocessing의 능력을 최대한 활용

  • 사용할 수 있는 모든 방법을 다 써보기 전에 multiprocessing이 제공하는 고급 기능을 시도하지 말라.

profile
성장을 도울 아카이빙 블로그

0개의 댓글