[CS] 벡터화와 병렬 처리로 데이터 처리 가속화

Hyunjun Kim·2025년 7월 19일
0

Computer_Science

목록 보기
15/19

학습목표

  • 벡터화와 병렬 처리의 개념을 이해하고, 데이터 처리 성능을 가속화하는 방법을 학습한다.
  • 파이썬을 중심으로 벡터화와 병렬 처리를 구현하며, 다른 언어에서의 활용 사례를 간단히 살펴본다.
  • NumPy, Pandas, Joblib을 활용한 실습을 통해 성능 차이를 체감한다.
  • 대규모 데이터 처리 시 Apache Spark와의 비교를 통해 벡터화와 병렬 처리의 한계를 이해한다.

벡터화란?

벡터화(Vectorization)는 반복문(예: for 루프)을 사용하지 않고, 배열 단위로 연산을 수행하여 데이터를 효율적으로 처리하는 기법이다. 이는 대량의 데이터를 한 번에 처리하여 연산 속도를 크게 향상시킨다.

핵심 원리

  • SIMD(Single Instruction, Multiple Data): CPU가 단일 명령으로 여러 데이터를 동시에 처리하는 기술. 예를 들어, 100만 개 숫자를 2배로 만드는 연산을 단일 명령으로 수행하여 속도를 높인다.
  • 메모리 접근 최적화: 데이터가 메모리에 연속적으로 저장되어 CPU 캐시를 효율적으로 사용한다. 이는 메모리 대역폭을 최적화하고, 데이터 읽기/쓰기 지연을 줄여 연산 속도를 가속화한다.

파이썬에서의 구현

  • NumPyPandas는 C로 작성된 백엔드를 통해 SIMD를 활용하며, 파이썬의 기본 반복문 대비 수십~수백 배 빠른 성능을 제공한다.
  • 장점:
    • 코드가 간결하여 작성과 유지보수가 용이하다.
    • 메모리 사용이 효율적이며, 불필요한 데이터 복사를 최소화한다.
    • Garbage Collection(GC)이 자주 발생하지 않아, GC로 인한 STD(Stop-The-World) 시간이 줄어들어 응답 지연과 처리량 감소가 최소화된다. STD란 GC가 메모리를 정리하는 동안 프로그램 실행이 잠시 멈추는 시간을 의미하며, 이는 응답 시간 증가와 처리량 저하를 유발한다.

다른 언어에서의 벡터화

  • C++: Eigen, Armadillo 라이브러리를 통해 고성능 벡터화 연산을 구현. 예: 행렬 곱셈에서 SIMD를 활용해 최적화.
  • Julia: 기본적으로 벡터화 연산이 최적화되어 있으며, MATLAB과 유사한 문법을 제공한다.
  • R: vector() 또는 apply() 계열 함수를 통해 유사한 벡터화 처리 가능.

벡터화의 예 (파이썬)

  • 비효율적인 반복문: 각 요소를 개별적으로 처리하므로 CPU 캐시 활용이 비효율적이고, 실행 시간이 길어진다.
    data = [i * 2 for i in range(1000000)]  # 느림
  • 효율적인 벡터화: NumPy를 사용해 배열 전체를 한 번에 처리, SIMD와 캐시 최적화를 활용.
    import numpy as np
    data = np.arange(1000000) * 2  # 빠름

병렬 처리란?

병렬 처리(Parallel Processing)는 작업을 여러 프로세서(CPU 코어, GPU 등)에 분할하여 동시에 처리하는 기법이다. 이를 통해 전체 처리 시간을 단축할 수 있다.

핵심 원리

  • 작업 분할: 큰 작업을 독립적인 태스크로 나누어 각 프로세서가 동시에 처리. 예: 100만 개 데이터 처리를 4코어 CPU로 나누면 각 코어가 25만 개를 처리.
  • 다중 코어 활용: 현대 CPU는 여러 코어를 가지며, 병렬 처리는 각 코어에 작업을 할당하여 CPU의 전체 처리 능력을 활용한다. 예를 들어, 8코어 CPU는 최대 8개의 태스크를 동시에 실행 가능.
  • 멀티스레딩 vs 멀티프로세싱:
    • 멀티스레딩: 동일한 메모리 공간을 공유하며 경량 작업을 처리. 하지만 파이썬의 GIL로 인해 CPU 바운드 작업에는 비효율적.
    • 멀티프로세싱: 각 프로세스가 독립된 메모리 공간에서 실행되며, CPU 바운드 작업에 적합.

파이썬에서의 구현

  • 파이썬의 GIL(Global Interpreter Lock)은 한 번에 하나의 스레드만 파이썬 바이트코드를 실행하도록 제한한다. 따라서 CPU 바운드 작업에서는 멀티스레딩 대신 멀티프로세싱(multiprocessing, joblib)을 사용해 병렬 처리를 구현한다.
  • 멀티프로세싱은 각 프로세스가 독립된 메모리 공간에서 실행되므로 GIL의 영향을 받지 않으며, CPU 코어를 효과적으로 활용한다.

다른 언어에서의 병렬 처리

  • C++: OpenMP를 사용해 반복문을 병렬화하거나, TBB(Threading Building Blocks)로 스레드 관리.
  • Java: Fork/Join 프레임워크 또는 ExecutorService로 작업을 병렬 처리.
  • Go: 고루틴(Goroutines)을 통해 경량 스레드로 병렬 처리 구현.

벡터화 vs 병렬 처리

항목벡터화병렬 처리
처리 방식단일 코어에서 배열 연산 최적화여러 CPU 코어 또는 GPU에 작업 분배
성능반복문 대비 수십~수백 배 빠름코어 수에 비례한 성능 향상
적합 데이터메모리 내 처리 가능한 데이터대규모 데이터 또는 독립적 작업
복잡성간단(코드 변경 최소화)복잡(작업 분배 및 결과 취합 필요)
오버헤드낮음(SIMD 활용)높음(프로세스 생성, 직렬화)

선택 기준

  • 벡터화: 데이터가 메모리에 맞고, 연산이 단순한 배열 기반(예: 수치 연산, 행렬 연산)일 때 최적. 예: 데이터프레임의 컬럼 값을 한 번에 변환.
  • 병렬 처리: 작업이 CPU 바운드(예: 복잡한 수치 계산, 머신러닝 모델 학습)이고 독립적으로 분할 가능할 때 적합. 예: 여러 모델을 동시에 학습.
  • 결합 전략: 대규모 데이터에서는 벡터화된 연산을 병렬 처리로 분배. 예: Spark의 Pandas UDF는 각 노드에서 벡터화 연산을 병렬로 실행.

파이썬에서의 벡터화 구현

NumPy

NumPy는 배열 연산에 최적화된 라이브러리로, 벡터화의 핵심 도구이다. C로 작성된 백엔드를 통해 SIMD를 활용하며, 반복문을 대체해 속도를 높인다.

  • 예제: 배열의 값을 2배로 만드는 연산.

    import numpy as np
    import time
    
    # 비효율적인 반복문
    start_time = time.time()
    data = [i * 2 for i in range(1000000)]
    print(f"Loop time: {time.time() - start_time:.4f} seconds")
    
    # 효율적인 벡터화
    start_time = time.time()
    data = np.arange(1000000) * 2
    print(f"Vectorized time: {time.time() - start_time:.4f} seconds")

    출력 예시:

    Loop time: 0.3200 seconds
    Vectorized time: 0.0020 seconds

    설명: 반복문은 각 요소를 개별적으로 처리하므로 CPU 캐시 활용이 비효율적이고, 실행 시간이 길다. NumPy는 배열 전체를 한 번에 처리하여 SIMD와 캐시 최적화를 활용한다.

Pandas

Pandas는 DataFrame을 사용해 대규모 데이터의 벡터화 연산을 지원한다. NumPy 기반으로 작동하며, 데이터프레임의 컬럼 단위로 연산을 최적화한다.

  • 예제: 컬럼 값을 2배로 변환.

    import pandas as pd
    import time
    
    # 데이터 생성
    df = pd.DataFrame({'value': range(1000000)})
    
    # 비효율적인 apply
    start_time = time.time()
    df['doubled'] = df['value'].apply(lambda x: x * 2)
    print(f"Apply time: {time.time() - start_time:.4f} seconds")
    
    # 효율적인 벡터화
    start_time = time.time()
    df['doubled'] = df['value'] * 2
    print(f"Vectorized time: {time.time() - start_time:.4f} seconds")

    출력 예시:

    Apply time: 0.4500 seconds
    Vectorized time: 0.0050 seconds

    설명: apply는 각 행을 개별적으로 처리하므로 느리다. 반면, 컬럼 단위 연산은 벡터화를 통해 빠르고, 메모리 접근과 CPU 연산을 최적화한다.


파이썬에서의 병렬 처리 구현: Joblib

Joblib은 간단한 병렬 처리를 지원하며, Scikit-learn에서 내부적으로 사용된다. 멀티프로세싱을 통해 작업을 분할하여 CPU 코어를 활용한다.

특징

  • 작업을 여러 프로세스로 분할하여 GIL의 영향을 받지 않는다.
  • 단점: k개 프로세스의 작업이 모두 완료될 때까지 다음 작업으로 넘어가지 않는다. 예: 20개 작업을 4개 프로세스로 처리할 때, 1~4번 작업이 모두 완료되어야 5~8번 작업이 시작된다. 이로 인해 작업 실행 시간 차이로 비효율이 발생할 수 있다.
  • 오버헤드: 프로세스 생성, 데이터 직렬화(Pickling), 결과 취합 과정에서 CPU와 메모리 부담이 발생한다. 특히, 빈번한 직렬화는 GC를 자주 유발하여 STD 시간이 길어지고, 응답 지연과 처리량 감소를 초래한다.

예제

from joblib import Parallel, delayed
import time

def process_task(i):
    time.sleep(1)  # 작업 시뮬레이션
    return i * 2

# 병렬 처리
start_time = time.time()
results = Parallel(n_jobs=4)(delayed(process_task)(i) for i in range(8))
print(f"Results: {results}")
print(f"Time taken: {time.time() - start_time:.4f} seconds")

출력 예시:

Results: [0, 2, 4, 6, 8, 10, 12, 14]
Time taken: 2.0500 seconds

설명: 8개 작업을 4개 프로세스로 나누어 처리. 각 프로세스가 2개 작업을 병렬로 수행하며, 작업 시간이 균일하지 않으면 대기 시간이 발생할 수 있다.


벡터화와 병렬 처리의 한계

벡터화

  • 메모리 제약: 데이터가 단일 머신의 메모리에 맞아야 한다. 예: 64GB 메모리 시스템에서 100GB 데이터는 처리 불가.
  • 복잡한 로직: 조건문이나 복잡한 함수가 포함된 연산은 벡터화가 어려울 수 있다. 예: 각 요소에 따라 다른 연산을 적용하는 경우.

병렬 처리

  • 오버헤드: 프로세스 생성, 직렬화(Pickling), 결과 취합 과정에서 CPU와 메모리 부담이 발생. 작업 단위가 너무 작으면 오버헤드가 성능 향상을 상쇄할 수 있다.
  • GC 문제: 빈번한 직렬화는 메모리 할당/해제를 반복하여 GC를 자주 유발한다. 이는 STD 시간을 늘리고, 응답 지연과 처리량 감소를 초래한다.

Apache Spark와의 비교

Apache Spark

  • 대규모 데이터를 여러 노드에 분산 처리하며, Pandas UDF를 통해 벡터화 연산을 통합한다.
  • 특징: 데이터를 파티션으로 나누어 각 노드에서 병렬 처리. 네트워크 셔플링(데이터 이동)과 클러스터 관리(YARN, Kubernetes 등)의 복잡성이 존재.
  • 장점: 메모리 초과 데이터 처리 가능, 확장성 우수.
  • 단점: 네트워크 셔플링은 GC를 유발하며, STD 시간 증가와 응답 지연을 초래할 수 있다.

파이썬 벡터화 + 병렬 처리

  • 단일 머신에서 간단하고 빠르게 실행.
  • 메모리 제약으로 대규모 데이터 처리에 한계.
  • 네트워크 오버헤드가 없어 소규모~중규모 데이터에 적합.

Spark Pandas UDF 예제

from pyspark.sql import SparkSession
from pyspark.sql.functions import pandas_udf
import pandas as pd

# 스파크 세션 초기화
spark = SparkSession.builder.appName("VectorizedExample").getOrCreate()

# 데이터프레임 생성
df = spark.createDataFrame([(i,) for i in range(1000000)], ["value"])

# Pandas UDF 정의
@pandas_udf("long")
def multiply_by_two(series: pd.Series) -> pd.Series:
    return series * 2

# 벡터화 연산 적용
result = df.select(multiply_by_two("value").alias("doubled"))
result.show(5)
spark.stop()

설명: Spark는 데이터를 파티션으로 나누어 각 노드에서 처리하며, Pandas UDF를 통해 파티션 내에서 벡터화 연산을 수행한다. 이는 단일 머신의 메모리 제약을 극복하며, 벡터화의 장점을 유지한다.


성능 최적화 팁

벡터화

  • apply 피하기: df['col'].apply(lambda x: x * 2) 대신 df['col'] * 2를 사용해 벡터화 연산 적용.
  • NumPy ufunc 활용: np.add, np.multiply 같은 함수로 연산 최적화. 이는 SIMD를 최대한 활용한다.
  • 메모리 모니터링: 대규모 데이터 처리 시 Out-of-Memory(OOM) 오류를 방지하기 위해 메모리 사용량 확인.

병렬 처리

  • 작업 단위 최적화: 작업 단위가 너무 작으면 프로세스 생성/종료 오버헤드가 성능을 저하시킨다. 예: 100만 개 작업을 1000개로 나누는 대신 10개로 나누어 오버헤드 감소.
  • Joblib 설정: backend='loky'로 안정적인 멀티프로세싱 구현. n_jobs를 CPU 코어 수에 맞게 설정.
  • 공유 메모리: multiprocessing.Array를 사용해 큰 데이터의 메모리 복사를 줄인다.

GC 최적화

  • 객체 생성 최소화: 불필요한 임시 객체 생성을 줄여 GC 호출 빈도를 낮춘다. 예: 리스트 컴프리헨션 대신 제너레이터 사용.
  • 직렬화 관리: joblibmax_nbytes로 직렬화 메모리 사용량을 조정하여 GC 부담 감소.
  • GC 문제 해결: 빈번한 GC는 STD 시간을 늘리고, 응답 지연과 처리량 감소를 유발한다. 메모리 할당을 최적화하여 GC 호출을 최소화해야 한다.

다른 언어 팁

  • C++: OpenMP의 #pragma omp parallel for로 반복문을 병렬화. 예: 행렬 연산을 여러 스레드로 분배.
  • Julia: @simd 지시어로 벡터화 연산 최적화. 예: 배열 연산에서 SIMD 자동 적용.

결론

  • 벡터화는 SIMD를 활용해 메모리 내 데이터 처리를 가속화한다.
  • 병렬 처리는 CPU 코어에 작업을 분배하여 처리 속도를 향상시킨다.
  • 파이썬의 강점: NumPy, Pandas, Joblib으로 직관적이고 효율적인 구현 가능.
  • 적용 전략:
    • 소규모 데이터: NumPy/Pandas 벡터화.
    • 중규모 데이터: Joblib 병렬 처리.
    • 대규모 데이터: Spark와 Pandas UDF 결합.
  • 주의사항:
    • 직렬화와 GC로 인한 STD 시간은 응답 지연과 처리량 감소를 유발한다.
    • 작업 단위와 메모리 사용을 최적화하여 성능 저하를 방지.
  • 실습을 통해 벡터화와 병렬 처리의 성능 차이를 체감하고, 데이터 처리 파이프라인을 최적화하자.
profile
Data Analytics Engineer 가 되

0개의 댓글