目次

7. 2次元配列の計算

7.1 2次元配列の計算

2次元配列の計算方法には各種あり、その方法によって計算時間に大きな差がでます。
配列演算を用いるとプログラムが簡単になり、計算時間も短縮されます。 さらに、プロードキャストという技術を用いるとプログラムがより簡単になります。
一方、Numbaを用いるとfor文のままで高速化することができます。 さらに、Numbaは配列演算と併用することもできます。
リスト7-1では同じ処理(cij=ai+bj) を15の方法で記述しています。

(1)~(5)が配列演算です。
配列全体をベクトル処理するには":"を用います。
(5)はブロードキャストの一例です。 (i, j)の2重ループにブロードキャストを適用するには、 iに関する配列aは a.reshape((n, 1)) とし、 jに関する配列bは b.reshape((1, n)) とします(n : 配列の大きさ)。

(6)~(15)ではNumbaを使用しています。
すべて、"@jit(cache=True, nopython=True, nogil=True, parallel=True)" を使用しています。
for文の処理には通常の"range"と並列処理をする"prange"があります。

リスト7-1 2次元配列の計算 (array2d.py)


# array2d.py
# test of 2D array

import numpy as np
import time
from numba import jit, prange, set_num_threads

# (1) i, j, loop
def fn1(a, b, c):
    n = len(a)
    for i in range(n):
        for j in range(n):
            c[i, j] = a[i] + b[j]

# (2) j, i, loop
def fn2(a, b, c):
    n = len(a)
    for j in range(n):
        for i in range(n):
            c[i, j] = a[i] + b[j]

# (3) j loop (array)
def fn3(a, b, c):
    n = len(a)
    for j in range(n):
        c[:, j] = a + b[j]

# (4) i loop (array)
def fn4(a, b, c):
    n = len(a)
    for i in range(n):
        c[i, :] = a[i] + b

# (5) broadcast
def fn5(a, b, c):
    n = len(a)
    c[:] = a.reshape((n, 1)) + b.reshape((1, n))

# (6) i, j, loop (JIT, range+range)
@jit(cache=True, nopython=True, nogil=True, parallel=True)
def fn6(a, b, c):
    n = len(a)
    for i in range(n):
        for j in range(n):
            c[i, j] = a[i] + b[j]

# (7) i, j, loop (JIT, prange+range)
@jit(cache=True, nopython=True, nogil=True, parallel=True)
def fn7(a, b, c):
    n = len(a)
    for i in prange(n):
        for j in range(n):
            c[i, j] = a[i] + b[j]

# (8) i, j, loop (JIT, range+prange)
@jit(cache=True, nopython=True, nogil=True, parallel=True)
def fn8(a, b, c):
    n = len(a)
    for i in range(n):
        for j in prange(n):
            c[i, j] = a[i] + b[j]

# (9) j, i, loop (JIT, range+range)
@jit(cache=True, nopython=True, nogil=True, parallel=True)
def fn9(a, b, c):
    n = len(a)
    for j in range(n):
        for i in range(n):
            c[i, j] = a[i] + b[j]

# (10) j, i, loop (JIT, prange+range)
@jit(cache=True, nopython=True, nogil=True, parallel=True)
def fn10(a, b, c):
    n = len(a)
    for j in prange(n):
        for i in range(n):
            c[i, j] = a[i] + b[j]

# (11) j, i, loop (JIT, range+prange)
@jit(cache=True, nopython=True, nogil=True, parallel=True)
def fn11(a, b, c):
    n = len(a)
    for j in range(n):
        for i in prange(n):
            c[i, j] = a[i] + b[j]

# (12) j loop (JIT, j : range, i : array)
@jit(cache=True, nopython=True, nogil=True, parallel=True)
def fn12(a, b, c):
    n = len(a)
    for j in range(n):
        c[:, j] = a + b[j]

# (13) j loop (JIT, j : prange, i : array)
@jit(cache=True, nopython=True, nogil=True, parallel=True)
def fn13(a, b, c):
    n = len(a)
    for j in prange(n):
        c[:, j] = a + b[j]

# (14) i loop (JIT, i : range, j : array)
@jit(cache=True, nopython=True, nogil=True, parallel=True)
def fn14(a, b, c):
    n = len(a)
    for i in range(n):
        c[i, :] = a[i] + b

# (15) i loop (JIT, i : prange, j : array)
@jit(cache=True, nopython=True, nogil=True, parallel=True)
def fn15(a, b, c):
    n = len(a)
    for i in prange(n):
        c[i, :] = a[i] + b

# parameters
N = 20000
fn = 1  # = 1...11
L = 1
thread = 8
dtype= 'f4'  # 'f4' or 'f8'

# setup
a = np.arange(N).astype(dtype)
b = np.arange(N).astype(dtype)
c = np.zeros((N, N), dtype)

# Numba threads
if fn >= 6:
    set_num_threads(thread)

t0 = time.time()

# calculation
for _ in range(L):
    if   fn == 1:
        fn1(a, b, c)
    elif fn == 2:
        fn2(a, b, c)
    elif fn == 3:
        fn3(a, b, c)
    elif fn == 4:
        fn4(a, b, c)
    elif fn == 5:
        fn5(a, b, c)
    elif fn == 6:
        fn6(a, b, c)
    elif fn == 7:
        fn7(a, b, c)
    elif fn == 8:
        fn8(a, b, c)
    elif fn == 9:
        fn9(a, b, c)
    elif fn == 10:
        fn10(a, b, c)
    elif fn == 11:
        fn11(a, b, c)
    elif fn == 12:
        fn12(a, b, c)
    elif fn == 13:
        fn13(a, b, c)
    elif fn == 14:
        fn14(a, b, c)
    elif fn == 15:
        fn15(a, b, c)

t1 = time.time()

# output
exact = N**2 * (N - 1)
print('(%d) N=%d(%d), %.2f[sec], %e, %e'
    % (fn, N, L, (t1 - t0) / L, np.sum(c), exact))

# free
a = None
b = None
c = None

7.2 計算時間

表7-1にそれぞれの手法の計算時間を示します。
計算時間の短いケースでは繰り返し回数Lを十分大きくとって総計算時間をLで割っています。
計算時間の短いケースに色をつけています。

表7-1 2次元配列の計算時間 (N=20000、実数、Numba:8スレッド)
No. 処理内容 単精度 倍精度
(1)i,j 2重forループ 143.79秒 142.69秒
(2)j,i 2重forループ 180.84秒 183.00秒
(3)j forループ, i 配列演算 8.95秒 8.29秒
(4)i forループ, j 配列演算 0.23秒 0.42秒
(5)ブロードキャスト 0.45秒 0.90秒
(6)i,j 2重forループ (i:range, j:range, Numba) 0.15秒 0.30秒
(7)i,j 2重forループ (i:prange, j:range, Numba) 0.12秒 0.23秒
(8)i,j 2重forループ (i:range, j:prange, Numba) 0.96秒 0.79秒
(9)j,i 2重forループ (j:range, i:range, Numba) 7.71秒 7.74秒
(10)j,i 2重forループ (j:prange, i:range, Numba) 2.99秒 3.28秒
(11)j,i 2重forループ (j:range, i:prange, Numba) 2.40秒 2.16秒
(12)j forループ, i 配列演算 (j:range, Numba) 2.45秒 2.12秒
(13)j forループ, i 配列演算 (j:prange, Numba) 3.17秒 3.52秒
(14)i forループ, j 配列演算 (i:range, Numba) 0.96秒 0.61秒
(15)i forループ, j 配列演算 (i:prange, Numba) 0.12秒 0.23秒

表7-1から以下のことがわかります。

7.3 C版の計算時間

参考までに同じ計算をCで行った結果を調べます。
リスト7-2にソースコードを示します。

リスト7-2 C版のソースコード (array2d.c)


/*
array2d.c
test program of 2D array

$ cl.exe array2d.c /O2 /openmp
$ array2d.exe [fn N L threads]
*/
# include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include <omp.h>

// (1) i, j loop, i : OpenMP
static void fn1(int n, float *a, float *b, float **c)
{
	int i;
#pragma omp parallel for
	for (i = 0; i < n; i++) {
		for (int j = 0; j < n; j++) {
			c[i][j] = a[i] + b[j];
		}
	}
}

// (2) i, j loop, j : OpenMP
static void fn2(int n, float *a, float *b, float **c)
{
	for (int i = 0; i < n; i++) {
		int j;
#pragma omp parallel for
		for (j = 0; j < n; j++) {
			c[i][j] = a[i] + b[j];
		}
	}
}

// (3) j, i loop, j : OpenMP
static void fn3(int n, float *a, float *b, float **c)
{
	int j;
#pragma omp parallel for
	for (j = 0; j < n; j++) {
		for (int i = 0; i < n; i++) {
			c[i][j] = a[i] + b[j];
		}
	}
}

// (4) j, i loop, i : OpenMP
static void fn4(int n, float *a, float *b, float **c)
{
	for (int j = 0; j < n; j++) {
		int i;
#pragma omp parallel for
		for (i = 0; i < n; i++) {
			c[i][j] = a[i] + b[j];
		}
	}
}

int main(int argc, char **argv)
{
	int fn      = 1;
	int N       = 2000;
	int L       = 1;
	int threads = 1;

	clock_t t0, t1;

	// arguments
	if (argc > 4) {
		fn      = atoi(argv[1]);
		N       = atoi(argv[2]);
		L       = atoi(argv[3]);
		threads = atoi(argv[4]);
	}

	// OpenMP
	omp_set_num_threads(threads);

	// alloc
	float *a = (float *)malloc(N * sizeof(float));
	float *b = (float *)malloc(N * sizeof(float));
	float **c = (float **)malloc(N * sizeof(float *));
	for (int i = 0; i < N; i++) {
		c[i] = (float *)malloc(N * sizeof(float));
	}

	// setup
	for (int i = 0; i < N; i++) {
		a[i] = i;
		b[i] = i;
	}

	t0 = clock();

	// calculation
	for (int l = 0; l < L; l++) {
		if      (fn == 1) {
			fn1(N, a, b, c);
		}
		else if (fn == 2) {
			fn2(N, a, b, c);
		}
		else if (fn == 3) {
			fn3(N, a, b, c);
		}
		else if (fn == 4) {
			fn4(N, a, b, c);
		}
	}

	t1 = clock();

	// check sum
	double sum = 0;
	for (int i = 0; i < N; i++) {
		for (int j = 0; j < N; j++) {
			sum += c[i][j];
		}
	}

	// output
	const double cpu = (double)(t1 - t0) / CLOCKS_PER_SEC / L;
	const double exact = (double)N * N * (N - 1);
	printf("(%d) N=%d(%d), %.2f[sec], %e, %e\n", fn, N, L, cpu, sum, exact);

	return 0;
}

表7-2にそれぞれの手法の計算時間を示します。

表7-2 C版の計算時間 (N=20000、実数単精度、OpenMP)
No.処理内容1スレッド8スレッド
(1)i,j 2重forループ, i 並列計算0.22秒0.13秒
(2)i,j 2重forループ, j 並列計算 0.32秒 0.14秒
(3)j,i 2重forループ, j 並列計算 8.22秒 3.13秒
(4)j,i 2重forループ, i 並列計算 8.27秒 0.33秒

表7-2から以下のことがわかります。