多次元配列 完全ガイド:行優先・列優先から言語別実装と性能最適化まで

はじめに — 「多次元配列」とは何か

多次元配列(たじげんはいれつ、multidimensional array)は、一次元の連続した要素を一次元インデックスだけで表す「配列」を拡張して、2次元・3次元以上の格子状(行列やテンソル)に要素を配置・参照できるデータ構造の総称です。画像(高さ×幅×チャンネル)、行列演算、科学技術計算におけるテンソル表現など、様々な用途で用いられます。

基本概念と定義

  • 次元(rank): 配列の軸の数。2次元なら行と列、3次元なら奥行き(depth)など。
  • 形状(shape): 各次元の要素数。たとえば 3×4 の2次元配列の形状は (3,4)。
  • インデックス: 要素を特定するための座標(例: a[i][j])。
  • 一次元化(linearization): 多次元添字を一次元のメモリインデックスに変換する方法。代表的なのは行優先(row-major)と列優先(column-major)。

行優先と列優先(メモリレイアウト)

コンピュータのメモリは一次元であるため、多次元配列は何らかの規則で線形化されて格納されます。代表的な方式は次の2つです。

  • 行優先(row-major): CやC++(標準配列)、PythonのNumPy(デフォルト)などで使われることが多く、同じ行の要素が連続してメモリに並ぶ。2次元配列 a[i][j] の線形インデックスは i * cols + j。
  • 列優先(column-major): FortranやMATLABで使われる方式で、同じ列の要素が連続して格納される。線形インデックスは j * rows + i。

この違いはキャッシュ効率やループの書き方に影響するため、高速化やベクトル化を考える際には重要です。

配列の種類 — 連続メモリ vs 配列の配列(ジャグ配列)

多次元配列の実装には大きく分けて2種類のアプローチがあります。

  • 固定サイズの連続メモリ(contiguous block): すべての要素を一つの連続領域に確保する方式。インデックス計算で位置が求まり、メモリ局所性に優れる(例: Cの int a[3][4]、NumPy ndarray)。
  • 配列の配列(array-of-arrays、ジャグ配列): 各行(次元)の配列を個別に確保し、上位の配列がそれらへの参照を持つ方式。各行の長さが異なる「不揃い(ジャグ)配列」を自然に表現できるが、必ずしも連続メモリでないためキャッシュ効率が落ちる(例: Java の int[][]、Python の list of lists、PHP の配列)。

言語別の扱い(例と注意点)

C / C++(標準配列と動的割当)

Cの固定長多次元配列は連続メモリに格納され、a[i][j] はコンパイラが i*cols+j を用いて計算します。動的に2次元配列を作る場合は、(1) まとめて1ブロックで確保してインデックスを自力で計算する、(2) 行ごとにポインタ配列を確保する、の2通りがあります。前者が高速かつメモリ効率が良い場合が多いです。

// 固定長:連続メモリ
int a[3][4];
a[1][2] = 42;

// 動的(連続ブロック)
int *buf = malloc(3 * 4 * sizeof(int));
int *row1 = buf + 1 * 4;
row1[2] = 42;

// 動的(ポインタ配列)
int **arr = malloc(3 * sizeof(int*));
for (int i=0;i<3;i++) arr[i] = malloc(4 * sizeof(int));

Java

Javaの多次元配列は「配列の配列」であり、各行は別オブジェクト(参照)です。すべて同じ列数に固定したければ new int[rows][cols] と書くが、内部的には可変長(ジャグ)を許す設計です。コピー操作は浅いコピーと深いコピーに注意が必要です。

Python

組み込みの list を使ったリストのリストは参照の配列で、深いコピーの問題と性能の点で数値計算には不向きです。数値計算では NumPy の ndarray が推奨され、これが多次元配列(テンソル)として効率的に動作します。NumPy は基本的にメモリが連続(C-order)か列優先(Fortran-order)のどちらかで持たれ、ストライド情報でアクセスを制御します。

import numpy as np
a = np.zeros((3,4))  # 3x4 の多次元配列(float64)
a[1,2] = 42

JavaScript / PHP

JavaScript の Array は可変長で要素は実体の参照やボックス化された値を保持します。多次元配列は配列の配列で表現するのが普通です。PHP の配列は連想配列(ordered map)であり、数値インデックスの数値配列として使えるが、内部構造がハッシュ基盤のため数値計算用途にはあまり向いていません。

操作とアルゴリズム

多次元配列に対する典型的な操作と設計上のポイントを挙げます。

  • 走査(トラバース): 最も内側のループがメモリ上で連続した次元に沿うようにループを設計するとキャッシュ効率がよく高速化できる。
  • 行列演算: 行列乗算などのアルゴリズムはメモリレイアウトとループ順序に強く依存する。BLAS やベクトル化ライブラリ(Eigen、MKL)を使うのが一般的。
  • スライスとビュー: NumPy のように配列の一部を「ビュー」で扱うとコピーなしで部分操作が可能。言語によってはスライスが常にコピーとなる場合があるため注意。
  • ブロードキャスト: NumPy に見られる概念で、形状の異なる配列をルールに従って形を合わせて演算できる。言語によってサポートの有無やルールが異なる。

性能面の考慮事項

多次元配列を扱う際に性能に影響する主要点:

  • メモリ局所性: 連続メモリに沿ったアクセスはキャッシュヒット率を高める。隣接要素のアクセスが多いなら連続配置を選ぶ。
  • アラインメントとパディング: SIMD 命令を用いる際には適切なアラインメントが重要。構造体や複雑な型を含むとパディングが入りうる。
  • ストライドの存在: ストライド(次元ごとのメモリ間隔)が大きいと単純ループでも性能が下がる。
  • コピーの回避: 不要なコピーはメモリと時間を浪費する。ビューや参照渡しを活用する。

高次元(テンソル)と応用例

多次元配列は3次元以上の「テンソル」としても扱われます。主な応用:

  • 機械学習(バッチ×チャンネル×高さ×幅の画像テンソル)
  • 科学技術計算(3D グリッド、時系列を含む配列)
  • 画像処理(高さ×幅×RGB チャンネル)
  • グラフや格子データ(疎な配列を使う場合も多い)

疎行列・圧縮表現

多くの要素がゼロ(またはデフォルト値)の場合、通常の多次元配列はメモリと計算の無駄になります。代表的な圧縮表現:

  • CSR / CSC(Compressed Sparse Row / Column): 行方向(または列方向)の非ゼロ要素だけを格納する方式。線形代数ライブラリで広く使われる。
  • 座標リスト(COO): 非ゼロ要素の座標と値を列挙する形式。
  • 特殊フォーマット: ブロック疎行列や対称行列専用の格納など、用途に合わせたフォーマット。

よくある落とし穴と注意点

  • 浅いコピーと深いコピー: 配列の配列(リストのリストなど)をコピーすると内部参照が共有される場合がある。mutable 要素の変更が別の参照に影響することがある。
  • 境界チェック: 言語によっては境界チェックがない(C)か自動で行われる(Java, Python)。安全性と速度のトレードオフに注意。
  • メモリ消費の見積: 要素数が爆発的に増えるため、高次元化はメモリ消費を急激に増やす。必要なら疎表現やストレージに分割する。
  • データの永続化: 大規模配列は単純にテキスト化すると肥大化する。効率的なフォーマットとして HDF5、NetCDF、Parquet などを検討する。

実践的なベストプラクティス

  • 数値計算には言語組み込みの配列より専門ライブラリ(NumPy、Eigen、MKL、BLAS/LAPACK)を使う。
  • パフォーマンス重視なら連続メモリ(contiguous)と適切なループ順序を選ぶ。
  • 可変長や不揃い行列が必要ならジャグ配列を使う。ただし性能面での劣化を理解する。
  • 大規模データはメモリ上だけでなくファイルフォーマットやストレージ戦略(チャンク、圧縮)を設計する。
  • 並列化・ベクトル化を検討する際、メモリ配置とデータアクセスパターンを最初に設計する。

まとめ

多次元配列は、単に「配列の配列」なのか、あるいは「連続メモリ上の格子表現」なのかで実装と性能が大きく変わります。用途(科学計算、画像処理、一般アプリケーション)や言語の特性に応じて、正しい表現とライブラリを選び、メモリレイアウト・ストライド・コピーの挙動に注意することが重要です。

参考文献