1. 기준: for 루프
가장 전통적인 방법은 for 루프를 사용한 순차 처리입니다:
from time import perf_counter
class Timer:
def __init__(self, print_tmpl='{} takes {:.1f} seconds'):
self.print_tmpl = print_tmpl
def __enter__(self):
self.start = perf_counter()
return self
def __exit__(self, *args):
self.end = perf_counter()
print(self.print_tmpl.format(self.print_tmpl.split()[0], self.end - self.start))
# 예시 데이터 처리 함수
def process_data(line):
# 실제 데이터 처리 로직
return line.strip()
# 데이터 준비
sample_lines = [f"line_{i}" for i in range(10000)]
with Timer(print_tmpl='for loop takes {:.1f} seconds'):
results = []
for item in sample_lines:
processed = process_data(item)
results.append(processed)
del results
출력 결과:
for loop takes 9.4 seconds
2. map 함수
Python의 map 함수는 for 루프를 간단히 대체할 수 있지만, 본질적으로는 순차 처리입니다:
with Timer(print_tmpl='map takes {:.1f} seconds'):
results = map(process_data, sample_lines)
results = list(results)
del results
출력 결과:
map takes 9.2 seconds
3. multiprocessing
multiprocessing은 Python의 다중 프로세스 패키지로, Python 프로그램 내에서 다중 프로세스를 생성하여 작업을 실행하고 병렬 계산을 수행할 수 있습니다.
multiprocessing에는 다양한 복잡한 사용법이 있지만, 이 글에서는 가장 간단하고 편리한 방법을 소개합니다.
(1) multiprocessing.Pool 프로세스에 작용하며, 프로세스 수를 지정할 수 있으며 기본값은 CPU 수입니다.
from multiprocessing import Pool
with Timer(print_tmpl='Pool() takes {:.1f} seconds'):
with Pool() as p:
# with Pool(4) as p: # 4개 프로세스 지정
results = p.map(process_data, sample_lines)
del results
출력 결과:
Pool() takes 2.5 seconds
Pool(4) takes 2.9 seconds
Pool(8) takes 1.8 seconds
Pool(16) takes 1.4 seconds
Pool(32) takes 1.6 seconds
(2) multiprocessing.dummy.Pool 스레드에 작용하며, 사용법은 위와 동일하지만 이 작업에서는 더 느립니다:
from multiprocessing.dummy import Pool as ThreadPool
with Timer(print_tmpl='ThreadPool() takes {:.1f} seconds'):
with ThreadPool() as p:
# with ThreadPool(4) as p: # 4개 스레드 지정
results = p.map(process_data, sample_lines)
del results
출력 결과:
ThreadPool() takes 37.4 seconds
ThreadPool(4) takes 29.4 seconds
ThreadPool(8) takes 33.3 seconds
ThreadPool(16) takes 34.4 seconds
4. p_tqdm
p_tqdm은 pathos.multiprocessing과 tqdm을 감싼 라이브러리로, 병렬 처리에 진행 상황 표시줄을 쉽게 추가할 수 있습니다. 주요 방법은 다음과 같습니다:
병렬 map: p_map - 순서 있는 병렬 map p_umap - 순서 없는 병렬 map 순차 map: t_map - 순서 있는 순차 map 사용법은 map과 동일하며, 여기서는 한 가지를 예시로 보여줍니다:
from p_tqdm import p_map, p_umap, t_map
with Timer(print_tmpl='p_map takes {:.1f} seconds'):
results = p_map(process_data, sample_lines)
del results
출력 결과:
100%|██████████████████████████████████████| 88880/88880 [05:28<00:00, 270.19it/s]
p_map takes 329.7 seconds
100%|██████████████████████████████████████| 88880/88880 [05:33<00:00, 266.75it/s]
p_umap takes 334.4 seconds
100%|█████████████████████████████████████| 88880/88880 [00:09<00:00, 9530.67it/s]
t_map takes 9.3 seconds
결론:
여러 순차 실행 방법은 시간이 거의 비슷합니다: 기준 for 루프, map, t_map;
프로세스 기반 병렬화 Pool과 스레드 기반 병렬화 ThreadPool 중 어느 것이 더 빠른지는 구체적인 상황에 따라 실험 분석해야 합니다. 이 글에서는 데이터가 SSD에 저장되어 있어 데이터 읽기가 병목이 아니고, 주요 소요 시간이 CPU 처리에 있기 때문에 프로세스 기반이 더 빠르고, 반대로 스레드 기반은 매우 느립니다. 반대로 I/O 집약적인 작업에서는 데이터 읽기가 병목이므로 ThreadPool이 더 빠를 수 있습니다;
프로세스 수나 스레드 수를 너무 크게 또는 너무 작게 설정하면 실행 시간이 길어질 수 있으므로, 자신의 작업과 머신 상황에 따라 적절한 수를 실험적으로 설정해야 합니다;
p_tqdm 라이브러리의 p_map, p_umap은 병렬 처리에 진행 상황 표시줄을 제공하지만 실행 시간이 너무 길어져 권장하지 않습니다. 진행 상황을 표시하고 싶다면 t_map을 순차 처리하거나 tqdm 라이브러리를 직접 사용하는 것이 더 빠릅니다;