[Numpy 강좌 – 7] 넘파이(NumPy)로 데이터를 더 빠르게 처리하는 법: 벡터화 연산과 메모리 접근 패턴 이해하기

1. 벡터화 연산

 

벡터화 연산은 데이터 배열에 대해 반복적인 연산을 빠르고 효율적으로 수행할 수 있는 방법을 말합니다. 이는 NumPy의 핵심 기능 중 하나이며, 특히 대용량 데이터 처리에서 훨씬 더 빠른 결과를 제공합니다. 이는 'for'문이나 다른 반복 구조를 사용하지 않고도 배열의 모든 요소에 대해 연산을 수행하므로, 코드가 간결해지고 실행 시간이 단축됩니다.

 

아래 코드에서 두 개의 NumPy 배열 'a'와 'b'를 생성하였습니다. 그 다음에는 '+' 연산자를 사용하여 'a'와 'b'를 더하는 벡터화 연산을 수행합니다. 이 경우 '+' 연산자는 배열 'a'와 'b'의 모든 요소 간에 요소별로(add element-wise) 작동합니다. 따라서 이렇게 하면 'c'라는 새로운 배열이 생성되며, 이 배열의 각 요소는 'a'와 'b'의 해당 요소의 합입니다. 이에 반해 'for'문을 사용하여 같은 연산을 수행하는 경우, 각 요소를 개별적으로 더해주어야 합니다. 이 방식은 코드가 더 복잡하고 실행 시간이 더 길어집니다. 벡터화 연산을 사용하면 이러한 반복 구조를 사용하지 않고도 동일한 결과를 얻을 수 있습니다.

 

# 벡터화 연산 예제
a = np.array([1, 2, 3, 4, 5])
b = np.array([6, 7, 8, 9, 10])

# 벡터화 연산
c = a + b
print("c: ", c)

# for loop를 사용한 경우
c_loop = np.zeros(5)
for i in range(5):
    c_loop[i] = a[i] + b[i]
print("c_loop: ", c_loop)

 

 

하지만 여기서는 큰 차이를 느끼지 못할 것입니다. 왜냐하면 코드에서 다루는 데이터의 크기가 아직 충분히 크지 않기 때문입니다. 수천, 수만 또는 그 이상의 대규모 데이터셋에서 벡터화 연산의 효율성을 명확하게 확인할 수 있습니다.

 


2. 메모리 접근 패턴과 캐시 최적화

 

Numpy는 내부적으로 메모리 접근 패턴을 최적화하여 연산 속도를 향상시킵니다. 이는 큰 데이터를 처리하는 경우, 데이터를 효과적으로 조작하고, 연산의 성능을 향상시키는 데 중요합니다.

 

NumPy 배열은 메모리에 연속적으로 저장됩니다. 예를 들어, 2차원 배열에서, 각 행은 메모리의 연속적인 위치에 저장되며, 이는 행 기반 접근이 열 기반 접근보다 빠르게 될 수 있도록 합니다. 이것이 바로 "행 우선" (row-major) 저장 방식입니다. 이러한 메모리 배치는 CPU 캐시의 활용을 극대화하는데 도움이 됩니다.행 우선 방식에서, 데이터의 인접한 요소들이 종종 함께 처리되므로 캐시 효율이 높아집니다.

 

아래 코드는 이러한 원리를 활용하여 2차원 배열에서 행과 열의 합을 계산하는 예제입니다. np.sum(arr, axis=0)은 각 열의 합을 계산하고, np.sum(arr, axis=1)은 각 행의 합을 계산합니다. NumPy의 행 기반 메모리 배치 덕분에, 행의 합을 계산하는 것이 열의 합을 계산하는 것보다 빠릅니다.

 

import time

# 2차원 배열 생성
arr = np.random.rand(10000, 10000)

# 열의 합을 계산
start = time.time()
column_sum = np.sum(arr, axis=0)
print("Time for column sum: ", time.time() - start)

# 행의 합을 계산
start = time.time()
row_sum = np.sum(arr, axis=1)
print("Time for row sum: ", time.time() - start)