Source-linked AI summary
The NumPy array: a structure for efficient numerical computation
Stefan Van Der Walt, S. Chris Colbert, Gaël Varoquaux
TL;DR
Python의 고수준 데이터 구조는 고성능 수치 계산에 적합하지 않다. 이 논문은 NumPy array를 소개하고, vectorization, 복사 없는 memory sharing, broadcasting을 통해 데이터셋이 커질수록 큰 성능 향상과 효율적인 계산이 가능함을 보인다.
문제
Python의 고수준 데이터 구조는 고성능 수치 계산에 적합하지 않다.
방법
이 논문은 NumPy array를 소개하고, vectorization, 복사 없는 memory sharing, broadcasting을 사용해 효율적인 수치 계산을 구현한다.
결과
NumPy는 Python-loop overhead를 줄이고 데이터 복사를 피하며 operation count를 줄여, 데이터셋이 커질수록 큰 성능 향상을 가능하게 한다.
시사점 및 한계
NumPy array는 간결한 수치 계산을 위한 고수준 구조를 제공하면서도 memory allocation과 성능을 제어할 수 있게 한다.
시사점 및 한계
Vectorization과 broadcasting이 항상 최적인 것은 아니다. 매우 큰 memory 영역에 대한 반복 연산에서는 vectorized inner loop를 둔 outer loop가 더 유리할 수 있다.
Abstract
from arXiv · showhide
In the Python world, NumPy arrays are the standard representation for numerical data. Here, we show how these arrays enable efficient implementation of numerical computations in a high-level language. Overall, three techniques are applied to improve performance: vectorizing calculations, avoiding copying data in memory, and minimizing operation counts. We first present the NumPy array structure, then show how to use it for efficient computation, and finally how to share array data with other libraries.
서론
Python의 범용 데이터 구조는 고성능 수치 계산에 적합하지 않아 다차원 NumPy array가 개발되었다. NumPy array는 익숙한 Python 표기법으로 원소를 인덱싱하고 슬라이싱할 수 있는 균일한 shape의 collection을 제공한다.
- Python의 list와 dictionary는 고성능 수치 계산에 적합하지 않기 때문에 효율적인 array 계산을 위한 데이터 구조로 NumPy array가 개발되었다.
- NumPy array는 원소의 type과 shape으로 특징지어지는 다차원의 균일한 collection이다.array는 행렬과 더 높은 차원의 데이터를 표현할 수 있으며, 여기에는 수, boolean, 날짜가 포함된다.
- array 원소와 subarray는 0부터 시작하는 인덱싱을 사용해 대괄호 인덱싱과 표준 start:stop:step 슬라이싱으로 접근할 수 있다.예로는 처음 몇 개의 행, 열 범위, 두 행마다 하나씩 선택하는 경우가 있다.
NumPy array의 구조: 메모리를 보는 view
NumPy array는 데이터의 시작 위치, 포함된 요소, shape, 요소 탐색 방식을 지정하는 metadata로 메모리를 설명한다. 이 metadata를 변경하면 NumPy는 데이터를 복사하지 않고 동일한 메모리를 서로 다른 array로 재해석할 수 있어 이러한 연산을 매우 효율적으로 수행한다.
- NumPy array의 구조: 메모리를 보는 view: NumPy array는 data pointer, data type, shape, strides, 그리고 수정과 memory layout을 제어하는 flags를 사용해 메모리를 설명한다.Shape은 array의 차원을 지정하고, strides는 다음 요소에 도달하는 데 필요한 byte 건너뛰기 간격을 지정한다.
- NumPy array의 구조: 메모리를 보는 view: NumPy의 strided memory model을 사용하면 여러 array view가 데이터를 복사하지 않고 동일한 underlying memory를 서로 다르게 해석할 수 있다.Strides를 변경하면 view가 두 번째 요소마다 하나씩만 선택할 수 있으며, 수정 내용은 view와 원래 array 사이에서 공유된다.
- NumPy array의 구조: 메모리를 보는 view: 호환되는 metadata가 동일한 메모리를 계속 참조한다면 strides를 변경해 array를 전치하거나 reshape할 수 있으며, 추가 비용이 없다.Shape, strides, data type을 수동으로 지정해 underlying data를 다양하게 해석할 수도 있다.
- NumPy array의 구조: 메모리를 보는 view: 결과 array들은 shape, strides, data type에 따른 해석은 서로 다르지만 모두 동일한 메모리를 가리키며 복사가 필요하지 않다.이러한 연산은 메모리 복사를 피하므로 매우 효율적이다.
배열의 수치 연산: vectorization
NumPy는 명시적 loop 대신 배열 전체에 연산을 적용하는 vectorization으로 원소별 계산을 가속한다. 또한 확장된 배열을 실제로 구성하지 않고 호환 가능한 shape 사이에 broadcasting을 지원해 메모리를 절약한다.
- Vectorization: Vectorization은 원소별 연산을 묶어 NumPy가 대규모 데이터셋에서 전통적인 for-loop보다 훨씬 빠르게 계산하도록 한다.NumPy는 vectorized operation을 C로 구현해 계산 속도를 크게 향상한다.
- Vectorization: NumPy는 두 배열 간 뺄셈을 포함해 산술 연산을 배열의 각 원소에 적용한다.예를 들어 b - a는 array([2, 6, 10])을 생성한다.
- Broadcasting: 배열의 shape이 호환되지만 서로 다를 때 NumPy는 공통 차원 전체에 연산을 broadcasting한다.Broadcasting은 배열을 개념적으로 확장해 연산이 가능해지도록 한다.
- Broadcasting: Broadcasting된 배열은 실제로 구성되지 않으며, NumPy는 메모리를 절약하기 위해 계산 중 필요한 원소에 접근한다.이 연산은 NumPy의 broadcasting 규칙을 따를 때만 유효하다.
브로드캐스팅 규칙
NumPy는 대응하는 차원이 같거나 어느 한쪽 차원이 1 또는 None일 때 두 배열을 브로드캐스팅한다. 이 경우 출력 차원은 더 큰 크기로 확장되며, 예시 배열의 shape은 (2, 4, 3)이 된다.
- 브로드캐스팅 규칙: 브로드캐스팅에서는 대응하는 각 차원 쌍이 같거나 어느 한쪽 차원이 1 또는 None이어야 한다.차원이 1 또는 None이면 NumPy는 해당 차원을 대응하는 더 큰 차원으로 확장한다.
- 브로드캐스팅 규칙: shape이 (2, 4, 3)과 (4, 1)인 예시 배열은 z = x + y 연산에 호환된다.
- 브로드캐스팅 규칙: 브로드캐스팅 연산의 출력 배열 shape은 (2, 4, 3)이다.
벡터화와 브로드캐스팅 예제
NumPy 벡터화와 브로드캐스팅은 간결한 코드와 메모리 할당 제어를 유지하면서 배열 계산을 크게 빠르게 한다. 또한 슬라이싱, 제자리 업데이트, 최적화된 내부 루프가 성능을 향상시키지만, 매우 큰 반복 연산에서는 캐시 사용에 한계가 있음을 보여준다.
- 벡터화 함수 평가: 1밀리초 대 약 500밀리초: NumPy 배열에 함수를 적용하면 빠른 벡터화 루프를 사용하지만, 큰 임시 배열은 확장성을 떨어뜨릴 수 있다.입력 크기가 증가하면 계산 과정에서 여러 임시 배열이 생성된다.
- 제자리 벡터화: 600마이크로초: NumPy의 제자리 연산은 새 메모리를 할당하지 않고 순진한 벡터화보다 속도를 거의 두 배로 높인다.3*x를 제자리에서 계산하면 원래 입력 배열이 수정되므로 이 예제에서는 이를 피한다.
- 슬라이싱과 차분: 100배 빠르다: NumPy 슬라이싱은 1000개 원소 배열의 전진 유한 차분을 Pure Python for-loop에 비해 간결하게 만든다.인접한 슬라이스를 빼면 전진 나눈 차분의 분자 배열과 분모 배열이 생성된다.
- 행렬 연산과 브로드캐스팅: 9밀리초: NumPy는 내적, 원소별 나눗셈, 브로드캐스팅을 결합해 100000개의 3차원 점을 픽셀 좌표로 투영하며, Python for-loop보다 70배 속도 향상을 달성한다.나눗셈에서는 각 행이 세 번째 좌표를 기준으로 브로드캐스팅된다.
- 한계: 벡터화와 브로드캐스팅은 보편적인 해법이 아니다: 매우 큰 메모리 영역에서 연산을 반복할 때는 벡터화된 내부 루프를 포함한 외부 for-loop가 시스템 캐시를 더 효과적으로 사용할 수 있다.이 한계는 배열 코드의 명확성이나 간결성이 아니라 최적의 캐시 사용과 관련된다.
데이터 공유
이 절에서는 NumPy가 외부 메모리 및 디스크 기반 배열을 다룰 때 데이터 복사를 피하는 방법을 보여준다. 또한 array interface가 외부에서 할당된 메모리를 NumPy에 노출하고 공유 업데이트를 가능하게 하는 방식도 설명한다.
- Memory mapping: Memory mapping을 사용하면 NumPy가 디스크에 저장된 매우 큰 배열 전체를 메모리에 로드하지 않고도 그 일부에만 접근할 수 있다.Memory-mapped array는 다른 NumPy array와 동일한 interface를 사용하며, 변경된 데이터는 flush를 통해 디스크에 다시 기록할 수 있다.
- Foreign memory: NumPy는 __array_interface__ protocol을 통해 외부 C++ 또는 Fortran library가 할당한 메모리를 포함한 foreign memory를 복사 없이 사용할 수 있다.이 protocol은 data address와 shape를 포함한 field로 메모리 블록을 기술하므로, 유효한 __array_interface__ dictionary를 가진 object를 array로 볼 수 있다.
- Foreign memory: NumPy view를 수정하면 underlying foreign memory가 업데이트될 수 있으며, byte value를 변경해 MutableString을 abcde에서 cdefg로 변환하는 예에서 이를 보여준다.이 예에서는 ctypes로 할당한 string buffer를 uint8 array로 노출하고, array 업데이트가 원래 string에 전파됨을 보여준다.
- Foreign memory: 필요한 metadata를 __array_interface__ dictionary에 제공하면 NumPy의 array interface는 any block of memory도 해석할 수 있다.MutableString 예는 외부에서 할당된 메모리를 NumPy array로 노출해 이 일반적 메커니즘을 보여준다.
- Structured arrays: Structured arrays는 homogeneous compound element를 저장하며 timestamp와 position 같은 field를 포함한 record를 표현할 수 있다.여러 typed field를 포함하는 record로 구성된 복잡한 binary file을 읽을 때 유용하다.
결론
NumPy의 N차원 배열은 vectorization, 복사 없는 데이터 공유, broadcasting을 통해 효율적인 수치 계산을 지원하는 고수준 구조다. 이러한 기능은 데이터셋이 커질수록 상당한 성능 향상을 제공한다.
- NumPy의 N차원 배열은 고수준 데이터 구조에서 for-loop의 vectorization을 가능하게 한다.
- 배열의 메모리 기술 방식은 데이터를 복사하지 않고도 많은 연산을 수행하게 해 데이터셋이 커질수록 성능을 향상한다.
- Broadcasting은 다차원 배열을 결합해 수치 계산에 필요한 연산 횟수를 줄인다.