배열 프로그래밍

컴퓨터 과학에서 배열 프로그래밍(Array programming, 어레이 프로그래밍)은 전체 값 집합에 대해 한 번에 연산을 적용할 수 있는 솔루션을 의미한다. 이러한 솔루션은 일반적으로 계산과학 및 엔지니어링 환경에서 사용된다.

배열 프로그래밍을 지원하는 최신 프로그래밍 언어(또는 벡터 또는 다차원 언어라고도 함)는 스칼라에 대한 연산을 벡터, 행렬, 그리고 고차원 배열에 투명하게 적용하도록 특별히 설계되었다. 여기에는 APL, J, 포트란, 매트랩, Analytica, GNU 옥타브, R, 실크 플러스, 줄리아, 펄 데이터 언어(PDL), 라쿠가 포함된다. 이 언어들에서 전체 배열에 대해 작동하는 연산을 벡터화된 연산이라고 부를 수 있으며, 벡터 프로세서에서 실행되는지 여부와는 관계없이 벡터 명령을 구현한다. 배열 프로그래밍 프리미티브는 데이터 조작에 대한 광범위한 아이디어를 간결하게 표현한다. 간결성의 수준은 특정 경우에 극적일 수 있다. 객체 지향 코드 몇 페이지를 요구하는 배열 프로그래밍 언어 원 라이너를 흔히 볼 수 있다.

배열의 개념
배열 프로그래밍의 기본 아이디어는 연산이 한 번에 전체 값 집합에 적용된다는 것이다. 이는 프로그래머가 개별 스칼라 연산의 명시적 루프에 의존할 필요 없이 전체 데이터 집합을 생각하고 조작할 수 있도록 해주기 때문에 고급 프로그래밍 모델이 된다.

케네스 아이버슨은 배열 프로그래밍(실제로는 APL을 지칭)의 근거를 다음과 같이 설명했다.

배열 프로그래밍 및 사고의 기본은 개별 요소가 유사하거나 인접한 데이터의 속성을 찾아 활용하는 것이다. 데이터를 구성 요소(또는 스칼라 양)로 암묵적으로 분해하는 객체 지향과 달리, 배열 지향은 데이터를 그룹화하고 균일하게 처리하려고 한다.

함수 랭크는 일반적으로 배열 프로그래밍 언어에 중요한 개념이며, 수학의 텐서 랭크에 비유할 수 있다. 데이터에 대해 작동하는 함수는 작동하는 차원의 수에 따라 분류될 수 있다. 예를 들어, 일반적인 곱셈은 0차원 데이터(개별 숫자)에 대해 작동하므로 스칼라 랭크 함수이다. 벡터곱 연산은 스칼라가 아닌 벡터에 대해 작동하므로 벡터 랭크 함수의 예이다. 행렬 곱셈은 2차원 객체(행렬)에 대해 작동하므로 2랭크 함수의 예이다. 축소 연산자는 입력 데이터 배열의 차원을 하나 이상 줄인다. 예를 들어, 요소에 대한 합계는 입력 배열을 1차원 줄인다.

사용법
배열 프로그래밍은 암묵적 병렬화에 매우 적합하다. 이는 오늘날 많은 연구가 진행되는 주제이다. 또한, 1997년 이후 개발 및 생산된 인텔 및 호환 CPU는 MMX부터 시작하여 SSSE3 및 3D나우!까지 다양한 명령어 집합 확장을 포함했으며, 이는 기본적인 단일 명령 다중 데이터 배열 기능을 포함한다. 이는 2020년대까지 AVX-512와 같은 명령어 집합으로 이어져 현대 CPU를 정교한 벡터 프로세서로 만들고 있다. 배열 처리는 병렬 처리와 구별된다. 배열 처리에서는 하나의 물리적 프로세서가 항목 그룹에 대해 동시에 연산을 수행하는 반면, 병렬 처리는 더 큰 문제를 더 작은 문제(다중 명령 다중 데이터)로 분할하여 여러 프로세서가 조각별로 해결하는 것을 목표로 한다. 2023년 현재 다중 코어 프로세서와 수천 개의 범용 컴퓨팅 코어를 갖춘 GPU가 일반적이다.

언어
배열 프로그래밍 언어의 전형적인 예는 포트란, APL, J이다. 다른 언어로는 A+, Analytica, 채플, 대화형 데이터 언어, 줄리아, K, Klong, Q, 매트랩, GNU 옥타브, 싸이랩, FreeMat, 펄 데이터 언어(PDL), R, 라쿠, S-Lang, SAC, Nial, ZPL, Futhark, 그리고 TI-BASIC이 있다.

스칼라 언어
C 및 파스칼과 같은 스칼라 언어에서는 연산이 단일 값에만 적용되므로 a+b는 두 숫자의 덧셈을 표현한다. 이러한 언어에서 한 배열을 다른 배열에 더하려면 인덱싱과 루프가 필요하며, 이는 코딩하기 번거롭다.
for (i = 0; i < n; i++)
 for (j = 0; j < n; j++)
 a[i][j] += b[i][j];

배열 기반 언어, 예를 들어 포트란에서는 위의 중첩된 for-루프를 한 줄로 배열 형식으로 작성할 수 있다.
a = a + b
또는 객체의 배열 속성을 강조하기 위해 다음과 같이 작성할 수도 있다.
a(:,:) = a(:,:) + b(:,:)

C와 같은 스칼라 언어는 언어 자체의 기본 배열 프로그래밍 요소를 가지고 있지 않지만, 그렇다고 해서 이 언어로 작성된 프로그램이 벡터화의 기본 기술(즉, CPU에 벡터 기반 명령어가 있다면 이를 활용하거나 여러 CPU 코어를 사용하는 것)을 활용하지 않는다는 의미는 아니다. GCC와 같은 일부 C 컴파일러는 특정 최적화 수준에서 휴리스틱에 따라 이점을 얻을 수 있는 코드 섹션을 감지하고 벡터화한다. 또 다른 접근 방식은 OpenMP API에서 제공되며, 이를 통해 여러 CPU 코어를 활용하여 해당 코드 섹션을 병렬화할 수 있다.

배열 언어
배열 언어에서는 연산이 스칼라와 배열 모두에 적용되도록 일반화된다. 따라서 a+b는 a와 b가 스칼라일 경우 두 스칼라의 합을 표현하고, 배열일 경우 두 배열의 합을 표현한다.

배열 언어는 프로그래밍을 단순화하지만, 추상화 패널티라는 비용이 발생할 수 있다. 덧셈이 코딩의 나머지 부분과 분리되어 수행되기 때문에 최적으로 효율적인 코드를 생성하지 못할 수 있다. (예를 들어, 동일한 배열의 다른 요소에 대한 덧셈이 동일한 실행 중에 나중에 발생하여 불필요한 반복 조회를 야기할 수 있다.) 가장 정교한 최적화 컴파일러조차도 프로그래머가 쉽게 수행할 수 있는 두 개 이상의 분명히 다른 함수를 통합하는 것은 매우 어려울 것이다. (프로그래머는 배열에 대한 동일한 패스에서 합계를 집계하여 계산 오버헤드를 최소화할 수 있다.)

에이다
이전 C 코드는 에이다 언어에서 다음과 같이 변경될 것이다. 이 언어는 배열 프로그래밍 구문을 지원한다.
A := A + B;

APL
APL은 구문적 설탕 없이 단일 문자 유니코드 심볼을 사용한다.
A ← A + B
이 연산은 임의의 랭크(랭크 0 포함)의 배열, 그리고 스칼라와 배열에 대해 작동한다. Dyalog APL은 증가 할당으로 원래 언어를 확장한다.
A +← B

Analytica
Analytica는 에이다와 동일한 표현의 경제성을 제공한다.
A := A + B;

BASIC
다트머스 베이직은 세 번째 에디션(1966년)에 행렬 및 배열 조작을 위한 MAT 문을 가지고 있었다.
DIM A(4),B(4),C(4)
MAT A = 1
MAT B = 2 * A
MAT C = A + B
MAT PRINT A,B,C

Mata
Stata의 행렬 프로그래밍 언어 Mata는 배열 프로그래밍을 지원한다. 아래에서는 덧셈, 곱셈, 행렬과 스칼라의 덧셈, 요소별 곱셈, 첨자, 그리고 Mata의 많은 역행렬 함수 중 하나를 보여준다.
. mata:

: A = (1,2,3) \(4,5,6)

: A
 1 2 3
 +-------------+
 1 | 1 2 3 |
 2 | 4 5 6 |
 +-------------+

: B = (2..4) \(1..3)

: B
 1 2 3
 +-------------+
 1 | 2 3 4 |
 2 | 1 2 3 |
 +-------------+

: C = J(3,2,1) // A 3 by 2 matrix of ones

: C
 1 2
 +---------+
 1 | 1 1 |
 2 | 1 1 |
 3 | 1 1 |
 +---------+

: D = A + B

: D
 1 2 3
 +-------------+
 1 | 3 5 7 |
 2 | 5 7 9 |
 +-------------+

: E = A*C

: E
 1 2
 +-----------+
 1 | 6 6 |
 2 | 15 15 |
 +-----------+

: F = A:*B

: F
 1 2 3
 +----------------+
 1 | 2 6 12 |
 2 | 4 10 18 |
 +----------------+

: G = E :+ 3

: G
 1 2
 +-----------+
 1 | 9 9 |
 2 | 18 18 |
 +-----------+

: H = F[(2\1), (1, 2)] // Subscripting to get a submatrix of F and

: // switch row 1 and 2
: H
 1 2
 +-----------+
 1 | 4 10 |
 2 | 2 6 |
 +-----------+

: I = invsym(F'*F) // Generalized inverse (F*F^(-1)F=F) of a

: // symmetric positive semi-definite matrix
: I
[symmetric]
 1 2 3
 +-------------------------------------------+
 1 | 0 |
 2 | 0 3.25 |
 3 | 0 -1.75 .9444444444 |
 +-------------------------------------------+

: end

매트랩
매트랩에서의 구현은 포트란 언어에서 허용되는 것과 동일한 경제성을 허용한다.
A = A + B;

매트랩 언어의 변형인 GNU 옥타브 언어는 원래 언어를 증강 할당으로 확장한다.
A += B;

매트랩과 GNU 옥타브 모두 행렬 곱셈, 역행렬, 연립 일차 방정식의 수치 해법과 같은 선형대수학 연산을 기본적으로 지원하며, 무어-펜로즈 유사역행렬도 사용한다.

두 배열의 내적에 대한 Nial 예제는 기본 행렬 곱셈 연산자를 사용하여 구현할 수 있다. a가 [1 n] 크기의 행 벡터이고 b가 [n 1] 크기의 해당 열 벡터인 경우.

 a * b;

대조적으로, 요소별 곱은 다음과 같이 구현된다.

 a .* b;

동일한 수의 요소를 가진 두 행렬 간의 내적은 주어진 행렬을 열 벡터로 변형하는 보조 연산자 (:)와 전치 행렬 연산자 '를 사용하여 구현할 수 있다.

 A(:)' * B(:);

rasql
rasdaman 쿼리 언어는 데이터베이스 지향적인 배열 프로그래밍 언어이다. 예를 들어, 두 배열은 다음 쿼리로 추가될 수 있다.
SELECT A + B
FROM A, B

R
R 언어는 기본적으로 배열 패러다임을 지원한다. 다음 예제는 두 행렬의 곱셈과 스칼라(실제로는 한 요소 벡터) 및 벡터의 덧셈 과정을 보여준다.
> A <- matrix(1:6, nrow=2) # !!this has nrow=2 ... and A has 2 rows
> A
 [,1] [,2] [,3]
[1,] 1 3 5
[2,] 2 4 6
> B <- t( matrix(6:1, nrow=2) ) # t() is a transpose operator !!this has nrow=2 ... and B has 3 rows --- a clear contradiction to the definition of A
> B
 [,1] [,2]
[1,] 6 5
[2,] 4 3
[3,] 2 1
> C <- A %*% B
> C
 [,1] [,2]
[1,] 28 19
[2,] 40 28
> D <- C + 1
> D
 [,1] [,2]
[1,] 29 20
[2,] 41 29
> D + c(1, 1) # c() creates a vector
 [,1] [,2]
[1,] 30 21
[2,] 42 30

라쿠
라쿠는 메타 연산자를 통해 배열 패러다임을 지원한다. 다음 예제는 더하기 연산자와 함께 하이퍼 연산자를 사용하여 배열 @a와 @b를 더하는 것을 보여준다.

[0] > my @a = [[1,1],[2,2],[3,3]];
[[1 1] [2 2] [3 3]]

[1] > my @b = [[4,4],[5,5],[6,6]];
[[4 4] [5 5] [6 6]]

[2] > @a »+« @b;
[[5 5] [7 7] [9 9]]

수학적 추론과 언어 표기법
행렬 왼쪽 나눗셈 연산자는 행렬의 일부 의미론적 속성을 간결하게 표현한다. 스칼라 등가물과 마찬가지로, 계수 (행렬) A의 (행렬식이) 널이 아니면 양변에 A의 역행렬 A−1 (매트랩 및 GNU 옥타브 언어에서 A^-1)를 왼쪽에서 곱하여 (벡터) 방정식 A * x = b를 풀 수 있다. 다음 수학적 진술은 A가 풀 랭크 정사각 행렬일 때 유효하다.
A^-1 *(A * x)==A^-1 * (b)
(A^-1 * A)* x ==A^-1 * b       (행렬 곱셈 결합성)
x = A^-1 * b
여기서 ==는 동등 관계연산자이다.
이전 진술은 세 번째가 다른 것들보다 먼저 실행되면 유효한 매트랩 표현식이기도 하다 (반올림 오류로 인해 수치 비교가 거짓일 수 있음).

만약 시스템이 과결정되어 A가 열보다 행이 많다면, 유사역행렬 A+ (매트랩 및 GNU 옥타브 언어에서는 pinv(A))가 역행렬 A−1를 대체할 수 있다.

       (행렬 곱셈 결합성)

그러나 이러한 해결책은 가장 간결하지도 않고 (예를 들어, 과결정된 시스템을 표기상으로 구별해야 하는 필요성이 여전히 남아 있음) 계산적으로도 가장 효율적이지 않다. 후자의 점은 스칼라 등가물 a * x = b를 다시 고려할 때 쉽게 이해할 수 있다. 이 경우 해결책 x = a^-1 * b는 더 효율적인 x = b / a 대신 두 가지 연산이 필요할 것이다.
문제는 일반적으로 행렬 곱셈이 교환적이지 않다는 점이다. 이는 스칼라 해결책을 행렬 사례로 확장할 때 필요할 것이다.
(a * x)/ a ==b / a
(x * a)/ a ==b / a       (교환성은 행렬에 대해 성립하지 않는다!)
x * (a / a)==b / a       (결합성은 행렬에 대해서도 성립한다)
x = b / a

매트랩 언어는 스칼라 사례와의 유사성의 본질적인 부분을 유지하기 위해 왼쪽 나눗셈 연산자 \를 도입하여 수학적 추론을 단순화하고 간결성을 보존한다.
A \ (A * x)==A \ b
(A \ A)* x ==A \ b       (결합성은 행렬에 대해서도 성립하며, 교환성은 더 이상 필요하지 않다)
x = A \ b
이는 코딩 관점에서 간결한 배열 프로그래밍의 예일 뿐만 아니라 계산 효율성 측면에서도 그러하다. 여러 배열 프로그래밍 언어에서는 ATLAS 또는 LAPACK과 같은 매우 효율적인 선형 대수 라이브러리의 이점을 누린다.

아이버슨의 이전 인용으로 돌아가면, 그 뒤에 숨겨진 근거는 이제 명백해질 것이다. 

타사 라이브러리
더 간결한 추상화를 제공하기 위해 특수하고 효율적인 라이브러리를 사용하는 것은 다른 프로그래밍 언어에서도 흔히 볼 수 있다. C++에서는 여러 선형대수 라이브러리가 언어의 연산자 오버로딩 기능을 활용한다. 어떤 경우에는 파이썬의 NumPy 확장 라이브러리, 아르마딜로 및 Blitz++ 라이브러리가 그러하듯이, 이러한 언어에서 매우 간결한 추상화가 배열 프로그래밍 패러다임의 명시적인 영향을 받는다.

 같이 보기 
 배열 슬라이싱
 배열 프로그래밍 언어 목록
 자동 벡터화

 각주 

 외부 링크 
 "No stinking loops" 프로그래밍
 배열 언어의 발견
 "배열 유형" 프로그래밍

분류:배열 프로그래밍 언어
분류:프로그래밍 패러다임