BLAS.hemv!()だけじゃない!Juliaで行列ベクトル積を効率化する代替手法
JuliaにおけるLinearAlgebra.BLAS.hemv!()
は、線形代数演算の基本的なルーチンを提供するBLAS (Basic Linear Algebra Subprograms) の関数の一つで、特にエルミート行列とベクトルの積を計算し、その結果を既存のベクトルに上書きするためのものです。
以下、詳しく説明します。
BLASとは?
BLASは、ベクトルとベクトルの積、行列とベクトルの積、行列と行列の積など、基本的な線形代数演算を高速に実行するための標準化されたインターフェースです。これらのルーチンは通常、CPUのアーキテクチャに最適化されており、非常に高いパフォーマンスを発揮します。Juliaは内部的にBLASライブラリ(通常はOpenBLASなど)を利用しており、LinearAlgebra.BLAS
モジュールはそのBLASルーチンへの低レベルなインターフェースを提供します。
hemv!()
の機能
hemv
は "Hermitian matrix-vector multiplication" の略です。
- Matrix-vector multiplication (行列とベクトルの積)
行列とベクトルの積を計算します。 - Hermitian matrix (エルミート行列)
複素共役転置(随伴行列)が元の行列と等しい正方行列を指します。実数の場合は対称行列と同じです。
!
が関数名の末尾についているのは、Juliaの慣習で、関数が引数として与えられたオブジェクトをインプレース(in-place)で変更することを意味します。つまり、hemv!()
は計算結果を新しいメモリ領域に格納するのではなく、引数として渡された出力ベクトルを直接変更します。
具体的には、以下の計算を行います:
y←αAx+βy
ここで:
- β: スカラー(ベクトルyにかけられる係数)
- α: スカラー(行列Aにかけられる係数)
- y: 入力/出力ベクトル(計算結果がこのyに上書きされます)
- x: 入力ベクトル
- A: エルミート行列
hemv!()
の典型的な呼び出し方
Juliaでのhemv!()
の典型的なシグネチャは以下のようになります(データ型はFloat64, Float32, ComplexF64, ComplexF32などに対応しています):
LinearAlgebra.BLAS.hemv!(uplo, alpha, A, x, beta, y)
引数の意味:
y
: 出力ベクトル(Vector
型)。計算結果がこのベクトルに上書きされます。beta
: スカラー値。上記のβに対応します。x
: 入力ベクトル(Vector
型)。A
: エルミート行列(Matrix
型)。alpha
: スカラー値。上記のαに対応します。uplo
:Char
型で、行列A
のどの部分(上三角または下三角)を使用するかを指定します。'U'
または'u'
: 上三角部分を使用'L'
または'l'
: 下三角部分を使用 エルミート行列はその性質上、上三角部分か下三角部分のどちらかがあれば全体を定義できるため、計算効率のためにこの指定をします。
なぜLinearAlgebra.BLAS.hemv!()
を使うのか?
- 低レベルな制御
特定の線形代数アルゴリズムを実装する際に、BLASの低レベルな関数を直接呼び出すことで、より細かい制御が可能になります。 - インプレース操作
!
が付いていることからわかるように、結果を新しいメモリに割り当てることなく、既存のベクトルy
に直接書き込みます。これにより、メモリ割り当てのオーバーヘッドが削減され、特に繰り返し演算を行う場合にパフォーマンスが向上します。 - パフォーマンス
BLASは高度に最適化されたルーチンであり、Juliaの標準的な行列演算(例:A * x
)と比較して、特に大規模な行列の場合に非常に高速な計算が可能です。これは、CPUのキャッシュ効率、SIMD命令、マルチスレッド処理などを最大限に活用するように設計されているためです。
using LinearAlgebra
# エルミート行列 A を定義 (実数の場合は対称行列)
A = [2.0 + 0.0im 3.0 + 1.0im; 3.0 - 1.0im 4.0 + 0.0im] # 例として複素数を使用
# A = [2.0 3.0; 3.0 4.0] # 実数の対称行列の場合
# 入力ベクトル x を定義
x = [1.0 + 2.0im, 5.0 - 1.0im]
# 出力ベクトル y を定義(初期値は計算に影響するため、通常はゼロベクトルや既存の値)
y = [0.0 + 0.0im, 0.0 + 0.0im]
alpha = 1.0 + 0.0im # スカラー α
beta = 0.0 + 0.0im # スカラー β
# hemv! を呼び出して A * x を計算し、y に上書きする
# uplo は 'U' または 'L' を指定
LinearAlgebra.BLAS.hemv!('U', alpha, A, x, beta, y)
println("A * x の結果 (yに上書き):")
println(y)
# 参考: 通常の行列-ベクトル積
println("A * x の結果 (通常の演算):")
println(A * x)
LinearAlgebra.BLAS.hemv!()
の一般的なエラーとトラブルシューティング
LinearAlgebra.BLAS.hemv!()
はBLAS (Basic Linear Algebra Subprograms) の低レベルな関数であるため、引数の型、次元、値などに厳密な要件があります。これらの要件が満たされていない場合にエラーが発生します。
DimensionMismatch (次元の不一致)
最も一般的なエラーの一つです。行列とベクトルの積のルールに従って、引数A
, x
, y
の次元が一致しない場合に発生します。
エラーメッセージ例
ERROR: DimensionMismatch("matrix A has dimensions (M, N) but vector x has length L")
ERROR: DimensionMismatch("output vector y has length L but expected M")
考えられる原因
- BLAS関数は通常、正方行列を期待しますが、
hemv
はエルミート行列(正方行列である必要があります)とベクトルの積です。 - 出力ベクトル
y
の長さが、行列A
の行数と一致しない。 - 行列
A
が正方行列ではない、またはx
の長さとA
の行/列数が一致しない。
トラブルシューティング
- 例:
A = rand(ComplexF64, 3, 3) # 3x3 行列 x = rand(ComplexF64, 3) # 長さ3のベクトル y = zeros(ComplexF64, 3) # 長さ3のベクトル # 正しい呼び出し LinearAlgebra.BLAS.hemv!('U', 1.0, A, x, 0.0, y) # 誤った呼び出し (xの長さが異なる) # x_bad = rand(ComplexF64, 2) # LinearAlgebra.BLAS.hemv!('U', 1.0, A, x_bad, 0.0, y) # DimensionMismatch
- 次元の確認
- 行列
A
がN×Nの正方行列であることを確認します。 - 入力ベクトル
x
の長さがN
であることを確認します。 - 出力ベクトル
y
の長さがN
であることを確認します。
- 行列
MethodError (引数の型が正しくない)
BLAS関数は、特定の数値型(Float64
, Float32
, ComplexF64
, ComplexF32
など)を期待します。異なる型の引数を渡すとMethodError
が発生することがあります。
エラーメッセージ例
ERROR: MethodError: no method matching hemv!(::Char, ::Int64, ::Matrix{Int64}, ::Vector{Int64}, ::Float64, ::Vector{Int64})
(上記は例であり、実際の型は異なる場合があります)
考えられる原因
- 行列
A
が複素数型なのに、x
やy
が実数型になっている。BLAS関数は、同じ種類の数値型(実数か複素数か)で統一されていることを期待します。 - 行列
A
、ベクトルx
、ベクトルy
の要素の型がBLASがサポートしていない型(例:Int
)である。 alpha
やbeta
に整数型(Int
)を渡している。これらは浮動小数点型または複素数型である必要があります。
トラブルシューティング
- 例:
# 良い例: すべての型が Float64 A_float = rand(3, 3) A_float = A_float + A_float' # 対称行列にする x_float = rand(3) y_float = zeros(3) LinearAlgebra.BLAS.hemv!('U', 1.0, A_float, x_float, 0.0, y_float) # 悪い例: alphaがInt # LinearAlgebra.BLAS.hemv!('U', 1, A_float, x_float, 0.0, y_float) # MethodError # 悪い例: 行列がInt # A_int = [1 2; 3 4] # x_int = [1, 2] # y_int = zeros(2) # LinearAlgebra.BLAS.hemv!('U', 1.0, A_int, x_int, 0.0, y_int) # MethodError
- 型の整合性
alpha
とbeta
は、行列とベクトルの要素の型に合わせた浮動小数点型(Float64
,Float32
)または複素数型(ComplexF64
,ComplexF32
)であることを確認します。A
,x
,y
の要素の型がすべて同じであり、かつBLASがサポートする数値型であることを確認します。特に、エルミート行列を扱うため、複素数型が一般的です。
InexactError (不正確な型変換)
これはhemv!()
の直接的なエラーというよりは、引数を準備する過程で発生する可能性があります。例えば、Int
型を期待する場所にFloat
型を渡そうとしたり、その逆を行おうとしたりする場合です。
考えられる原因
- データフレームなどから抽出したデータで、意図しない型になっている場合。
- 特に
uplo
引数に文字列ではなく、数値などを渡そうとした場合。
トラブルシューティング
- 引数の型が厳密に正しいことを確認します。
uplo
はChar
型(例:'U'
,'L'
)である必要があります。
ArgumentError (不正な引数)
BLAS関数は、引数の値に対しても特定の制約を課すことがあります。
考えられる原因
- 行列
A
がエルミート行列ではない場合(ただし、hemv!
はエルミート性を仮定して計算を行うため、実際にエルミート行列でなくてもエラーにはなりにくいですが、結果が意味をなさなくなります)。 uplo
引数に'U'
または'L'
以外の文字を渡した場合。
トラブルシューティング
- エルミート行列の定義(A=AH, AHは共役転置)を満たす行列を渡しているか確認します。BLAS関数は内部的に上三角または下三角のみを使用するため、行列全体がエルミート性を持っていることを保証するのはユーザーの責任です。
uplo
の値が正しいことを確認します。
マルチスレッド環境での問題 (BLASの競合)
JuliaはBLASライブラリを内部で利用しており、BLAS自体もマルチスレッドで動作することがあります。JuliaのマルチスレッドとBLASのマルチスレッドが同時に有効になっている場合、パフォーマンスの低下やセグメンテーション違反(SIGSEGV
)などの予期しない挙動を引き起こす可能性があります。これはhemv!()
に限らず、全てのBLAS関数で起こりえます。
考えられる原因
- Juliaを
julia -t auto
などでマルチスレッドモードで起動し、かつBLASライブラリもデフォルトで複数のスレッドを使用するように設定されている場合。
- パフォーマンスの監視
パフォーマンスが期待通りでない場合、@time
やBenchmarkTools
パッケージを使って、どこで時間がかかっているか、メモリ割り当てが多くないかなどを確認します。 - 環境変数の確認
OPENBLAS_NUM_THREADS
やMKL_NUM_THREADS
などの環境変数が設定されていないか確認します。これらが設定されていると、JuliaのBLAS.set_num_threads()
よりも優先されることがあります。 - BLASスレッド数の設定
Juliaのコード内でBLASのスレッド数を1に制限することを検討します。これにより、JuliaのマルチスレッドとBLASのマルチスレッドが競合するのを防ぎます。using LinearAlgebra.BLAS BLAS.set_num_threads(1) # BLASスレッドを1に設定
- @code_warntype
パフォーマンス問題が疑われる場合、@code_warntype
マクロを使用して、関数が型安定であるかを確認します。型不安定なコードは、hemv!()
のような低レベルのBLAS関数を使うメリットを打ち消すほど遅くなることがあります。 - 型アノテーションの活用
関数や変数の型を明示的に指定することで、コンパイラが型推論を正確に行えるようになり、予期しない型変換エラーやパフォーマンスの問題を防ぐことができます。 - ドキュメントの参照
Juliaの公式ドキュメントやBLASの仕様(LAPACKのドキュメントにBLASのインターフェースも記載されていることが多い)を参照し、引数の正確な要件を確認します。 - MWE (Minimal Working Example) の作成
エラーが発生した場合、問題のコードを最小限に絞り込んだ実行可能な例(MWE)を作成します。これにより、問題の原因を特定しやすくなります。
LinearAlgebra.BLAS.hemv!()
とは?
まず、簡単におさらいです。
LinearAlgebra.BLAS.hemv!()
は、BLAS (Basic Linear Algebra Subprograms) のエルミート行列とベクトルの積を計算する関数です。具体的には、y←αAx+βy という計算を行います。
- β: スカラー係数
- α: スカラー係数
y
: 出力ベクトル(結果が上書きされます)x
: 入力ベクトルA
: エルミート行列(または対称行列)
関数名の!
は、y
がインプレース(in-place)で変更されることを意味します。
例1: 最も基本的な使用例(y←Ax)
この例では、エルミート行列 A
とベクトル x
の積を計算し、その結果を初期化されたベクトル y
に格納します。これは最も一般的な使用方法です。
using LinearAlgebra
# エルミート行列を定義(例として複素数を使用)
# エルミート行列は対角成分が実数、非対角成分が共役複素数になります。
# 実数の場合は対称行列です。
A = [2.0 + 0.0im 3.0 + 1.0im;
3.0 - 1.0im 4.0 + 0.0im]
println("行列 A:\n", A)
# 入力ベクトル x
x = [1.0 + 2.0im,
5.0 - 1.0im]
println("\nベクトル x:\n", x)
# 出力ベクトル y をゼロで初期化
# 計算結果がこの y に上書きされます。
y = zeros(ComplexF64, 2)
println("\n初期のベクトル y:\n", y)
# スカラー係数
# y <- 1.0 * A * x + 0.0 * y となるように設定
alpha = 1.0 + 0.0im
beta = 0.0 + 0.0im
# BLAS.hemv! の呼び出し
# 'U' は A の上三角部分を使用することを示します。
# エルミート行列なので、上三角または下三角のみで十分です。
LinearAlgebra.BLAS.hemv!('U', alpha, A, x, beta, y)
println("\n計算後のベクトル y (BLAS.hemv!による):\n", y)
# 比較のため、通常の行列-ベクトル積も計算
y_standard = A * x
println("\n通常の A * x (参考):\n", y_standard)
# 結果が一致することを確認
println("\n結果の一致: ", isapprox(y, y_standard))
解説
using LinearAlgebra
:LinearAlgebra
モジュールをインポートします。BLAS
関数はこのモジュール内にあります。A
,x
,y
の定義: それぞれエルミート行列、入力ベクトル、出力ベクトルを定義します。hemv!
は複素数をサポートしているため、ComplexF64
型を使用しています。alpha
とbeta
の定義: それぞれスカラー係数αとβを設定します。ここではalpha = 1.0 + 0.0im
、beta = 0.0 + 0.0im
とすることで、y←Ax の計算になります。LinearAlgebra.BLAS.hemv!('U', alpha, A, x, beta, y)
:'U'
: 行列A
の上三角部分のみを使って計算を行うことを指定します。'L'
(下三角)も使用できます。alpha
: スカラーαです。A
: エルミート行列です。x
: 入力ベクトルです。beta
: スカラーβです。y
: 計算結果が上書きされる出力ベクトルです。
例2: 既存のベクトルに結果を加算する (y←αAx+βy)
この例では、y
に初期値があり、その初期値にA*x
のalpha
倍を加算するケースを示します。
using LinearAlgebra
# 対称行列 (実数のエルミート行列)
A = [1.0 2.0;
2.0 3.0]
println("行列 A:\n", A)
# 入力ベクトル x
x = [10.0, 20.0]
println("\nベクトル x:\n", x)
# 初期値を持つ出力ベクトル y
y = [100.0, 200.0]
println("\n初期のベクトル y:\n", y)
# スカラー係数
alpha = 0.5 # A*x の結果に 0.5 をかける
beta = 1.0 # y の初期値はそのまま残す
# BLAS.hemv! の呼び出し
# 今回は実数なので 'U' も 'L' も結果は同じ
LinearAlgebra.BLAS.hemv!('U', alpha, A, x, beta, y)
println("\n計算後のベクトル y (BLAS.hemv!による):\n", y)
# 比較のため、通常の計算も実行
# まず A*x を計算
Ax = A * x
println("\nA * x:\n", Ax)
# その結果を alpha 倍し、y の初期値に beta 倍して加算
y_expected = alpha * Ax + beta * [100.0, 200.0] # yの初期値を使う
println("\n通常の計算 (alpha * A * x + beta * y_initial):\n", y_expected)
# 結果が一致することを確認
println("\n結果の一致: ", isapprox(y, y_expected))
解説
A
,x
,y
の定義: 実数の対称行列とベクトルを使用しています。y
には初期値が入っています。alpha = 0.5
,beta = 1.0
:alpha = 0.5
: Ax の結果が0.5倍されます。beta = 1.0
: 既存のベクトルyが1.0倍されるので、そのまま残ります。 これにより、y←0.5⋅Ax+1.0⋅y の計算が行われます。
例3: 実数と複素数の混合(hemv!
は通常、型が統一されている必要があります)
hemv!
は、通常、実数型(Float64
, Float32
)または複素数型(ComplexF64
, ComplexF32
)のいずれかで引数の型が統一されている必要があります。異なる種類を混ぜるとMethodError
が発生します。
ただし、実数行列で複素数ベクトルを扱うケースを考えてみます。これはhemv!
の直接の機能ではありませんが、BLASの別の関数(例: gemv!
)や、Juliaのより高レベルな演算子オーバーロードが適切に処理します。hemv!
はエルミート行列を前提としているため、実数行列であれば自動的に対称行列として扱われ、複素数ベクトルとの積も計算できます。
using LinearAlgebra
# 実数対称行列 A
A_real = [1.0 2.0;
2.0 4.0]
println("実数行列 A_real:\n", A_real)
# 複素数入力ベクトル x
x_complex = [1.0 + 1.0im, 2.0 - 2.0im]
println("\n複素数ベクトル x_complex:\n", x_complex)
# 出力ベクトル y は複素数型にする必要がある
y_complex = zeros(ComplexF64, 2)
println("\n初期の複素数ベクトル y_complex:\n", y_complex)
# スカラー係数も複素数型に合わせる
alpha = 1.0 + 0.0im
beta = 0.0 + 0.0im
# BLAS.hemv! の呼び出し
# 実数行列A_realはHermitian(対称)なので、hemv!が使える
LinearAlgebra.BLAS.hemv!('U', alpha, A_real, x_complex, beta, y_complex)
println("\n計算後のベクトル y_complex (BLAS.hemv!による):\n", y_complex)
# 比較のため、通常の行列-ベクトル積も計算
y_complex_standard = A_real * x_complex
println("\n通常の A_real * x_complex (参考):\n", y_complex_standard)
println("\n結果の一致: ", isapprox(y_complex, y_complex_standard))
解説
A_real
: 実数型の対称行列です。実対称行列はエルミート行列の特殊なケースです。x_complex
: 複素数型のベクトルです。y_complex
: 計算結果が複素数になるため、複素数型のゼロベクトルで初期化します。alpha
とbeta
も、x
やy
の型に合わせてComplexF64
で定義しています。LinearAlgebra.BLAS.hemv!
: 実数行列A_real
と複素数ベクトルx_complex
の組み合わせでも問題なく動作します。これは、hemv!
が内部で型の変換や適切なBLASルーチンのディスパッチを行うためです。
BLAS.hemv!()
のようなBLAS関数は、通常、複数のCPUスレッドを使用して計算を高速化します。BLAS.set_num_threads()
を使用して、使用するスレッド数を制御できます。これは、特に大規模な計算でパフォーマンスをチューニングする際に重要です。
using LinearAlgebra
using BenchmarkTools # パフォーマンス測定用
# 非常に大きな実数対称行列を生成
N = 2000
A_large = rand(N, N)
A_large = A_large + A_large' # 対称行列にする
x_large = rand(N)
y_large = zeros(N)
alpha = 1.0
beta = 0.0
println("=========================================")
println("BLASスレッド数によるパフォーマンス比較 (N=$N)")
println("=========================================")
# BLASスレッド数を1に設定
println("\nBLASスレッド数: 1")
BLAS.set_num_threads(1)
@btime LinearAlgebra.BLAS.hemv!('U', $alpha, $A_large, $x_large, $beta, $y_large);
# BLASスレッド数をシステムが利用可能な最大数に設定
# これは環境によって異なる場合があります
num_threads_default = BLAS.get_num_threads()
println("\nBLASスレッド数: $num_threads_default (デフォルトまたは最大利用可能数)")
# BLAS.set_num_threads()を呼び出す必要がない場合もありますが、明示的に設定
BLAS.set_num_threads(num_threads_default)
@btime LinearAlgebra.BLAS.hemv!('U', $alpha, $A_large, $x_large, $beta, $y_large);
# 例として、スレッド数を2に設定(もしシステムが2スレッド以上利用可能なら)
if num_threads_default >= 2
println("\nBLASスレッド数: 2")
BLAS.set_num_threads(2)
@btime LinearAlgebra.BLAS.hemv!('U', $alpha, $A_large, $x_large, $beta, $y_large);
end
# 計算結果をリセットして再利用するための関数(必要に応じて)
function reset_y!(y_vec)
fill!(y_vec, 0.0)
end
解説
using BenchmarkTools
:BenchmarkTools
パッケージは、コードの実行時間を正確に測定するのに役立ちます。@btime
マクロを使用しています。N = 2000
: 非常に大きな行列とベクトルを作成し、パフォーマンスの違いが顕著になるようにします。BLAS.set_num_threads(1)
: BLASが使用するスレッド数を1に制限します。これにより、シングルスレッドでのパフォーマンスを確認できます。BLAS.get_num_threads()
: 現在BLASが使用しているスレッド数を取得します。これは通常、デフォルトでCPUコアの数に設定されています。- 異なるスレッド数で
@btime
を使ってhemv!
の実行時間を測定し、比較します。
- 最適なスレッド数
必ずしも最大のスレッド数が最速とは限りません。オーバーヘッドやキャッシュの競合により、最適なスレッド数はシステムの構成によって異なります。 - ジュリアのマルチスレッドとの競合
Julia自体をjulia -t auto
などでマルチスレッドで起動している場合、BLASのマルチスレッドと競合して、パフォーマンスが低下したり、デッドロックが発生したりする可能性があります。このような場合、BLASのスレッド数を1に設定するか、環境変数(例:OPENBLAS_NUM_THREADS=1
)で制御することが推奨されます。
以下に、LinearAlgebra.BLAS.hemv!()
の代替方法とその特徴を説明します。
標準の行列-ベクトル乗算演算子 (*)
Juliaで最も一般的で直感的な方法は、標準の乗算演算子*
を使用することです。Juliaの*
演算子は、内部的にBLAS関数を賢くディスパッチするため、多くの場合、BLAS.hemv!()
と同等か非常に近いパフォーマンスを発揮します。
特徴
- メモリ割り当て
通常、新しいベクトルが作成され、計算結果がその新しいベクトルに格納されます。hemv!()
のようなインプレース操作ではないため、メモリ割り当てのオーバーヘッドが発生します。 - 高レベルな抽象化
内部で適切なBLASルーチン(例えば、エルミート行列に対してはhemv
、一般行列に対してはgemv
など)が自動的に選択される。ユーザーがBLAS関数名を意識する必要がない。 - シンプルさ
コードが読みやすく、書きやすい。
使用例
using LinearAlgebra
A = [2.0 + 0.0im 3.0 + 1.0im;
3.0 - 1.0im 4.0 + 0.0im] # エルミート行列
x = [1.0 + 2.0im, 5.0 - 1.0im]
# 新しいベクトルに結果を格納
y_new = A * x
println("A * x (標準演算子):\n", y_new)
# A*x + y のような計算も可能
y_initial = [10.0 + 1.0im, 20.0 + 2.0im]
alpha = 0.5 + 0.0im
beta = 1.0 + 0.0im
y_combined = alpha * (A * x) + beta * y_initial
println("\nalpha * (A * x) + beta * y_initial:\n", y_combined)
いつ使うべきか
- BLASの低レベルな詳細を意識せずに、行列-ベクトル積を計算したい場合。
- 新しいメモリ割り当てが許容される場合。
- ほとんどの場合、これが推奨される方法です。コードの可読性が最も高く、パフォーマンスも十分です。
インプレース行列-ベクトル乗算関数 (mul!)
mul!
関数は、標準の*
演算子と同じ計算を行いますが、結果を既存のベクトルにインプレースで格納します。これはBLAS.hemv!()
のインプレース性に対応する高レベルなインターフェースです。
特徴
- 柔軟な引数
C←αAB+βC のような形式で、行列-行列積や行列-ベクトル積の両方を扱える。 - 高レベルな抽象化
*
演算子と同様に、内部で適切なBLASルーチンが選択される。 - インプレース操作
メモリ割り当てを回避できるため、繰り返し計算を行う場合にパフォーマンス上の利点がある。
使用例
using LinearAlgebra
A = [2.0 + 0.0im 3.0 + 1.0im;
3.0 - 1.0im 4.0 + 0.0im] # エルミート行列
x = [1.0 + 2.0im, 5.0 - 1.0im]
# 結果を格納するベクトルを事前に確保
y = zeros(ComplexF64, 2)
println("初期の y:\n", y)
# mul!(y, A, x) は y <- A * x
LinearAlgebra.mul!(y, A, x)
println("\nmul!(y, A, x) による計算後の y:\n", y)
# mul!(y, A, x, alpha, beta) は y <- alpha * A * x + beta * y
y_initial_mul = [10.0 + 1.0im, 20.0 + 2.0im]
println("\n初期の y_initial_mul:\n", y_initial_mul)
alpha_mul = 0.5 + 0.0im
beta_mul = 1.0 + 0.0im
LinearAlgebra.mul!(y_initial_mul, A, x, alpha_mul, beta_mul)
println("\nmul!(y_initial_mul, A, x, alpha, beta) による計算後の y_initial_mul:\n", y_initial_mul)
いつ使うべきか
BLAS.hemv!()
と同等のパフォーマンスが必要だが、より高レベルなインターフェースを好む場合。- 大規模な反復計算で、同じ出力バッファを使い回したい場合。
- 計算結果を新しいメモリに割り当てたくない場合(メモリ効率が重要、ガベージコレクションの頻度を減らしたい場合)。
LoopVectorization.jl
パッケージは、Juliaのループを自動的に最適化し、SIMD命令(単一命令複数データ)とマルチスレッディングを利用してパフォーマンスを向上させます。手動でBLAS関数を呼び出すよりも、より柔軟なカスタム操作を最適化するのに役立ちます。
特徴
- 学習曲線
パッケージの概念と使用法を学ぶ必要がある。常にBLAS関数よりも速いとは限らない。 - インプレース操作
自然とインプレースなコードを書くことができる。 - 高い柔軟性
任意のカスタム計算や非標準的な行列形式に対応できる。 - 自動最適化
@turbo
マクロを使用することで、ループが自動的にベクトル化され、可能であれば並列化される。
使用例
LoopVectorization.jl
をインストールする必要があります: Pkg.add("LoopVectorization")
using LinearAlgebra
using LoopVectorization
using BenchmarkTools
N = 1000
A = rand(N, N)
A = A + A' # 対称行列
x = rand(N)
y = zeros(N)
# hemv!()の等価な手動ループ
# Aは対称行列なので、A[i,j] = A[j,i]
# 通常の行列-ベクトル積 y_i = sum_j (A_ij * x_j) を実装
# y_i = sum_j (A_ji * x_j) とも書ける
# BLAS.hemv!()の動作を模擬したループ (対称性を利用)
# 実際のhemv!はもっと複雑な最適化をしているが、概念を示す
function custom_hemv!(y_out, A_mat, x_vec)
fill!(y_out, 0.0) # yをゼロにリセット
@turbo for j in eachindex(x_vec)
for i in eachindex(y_out)
# A_mat[i, j] のみを使い、A_mat[j, i] は使わないように工夫することで、
# エルミート性を利用する
# しかし、手動でこれをBLASと同等に最適化するのは困難
# 以下はA*xの単純なループ実装
y_out[i] += A_mat[i, j] * x_vec[j]
end
end
return y_out
end
println("=========================================")
println("LoopVectorization と BLAS.hemv! の比較 (N=$N)")
println("=========================================")
# BLAS.hemv! のベンチマーク
y_blas = zeros(N)
print("BLAS.hemv! : ")
@btime LinearAlgebra.BLAS.hemv!('U', 1.0, $A, $x, 0.0, $y_blas);
# LoopVectorization を使った手動実装のベンチマーク
y_custom = zeros(N)
print("custom_hemv! (Turbo): ")
@btime custom_hemv!($y_custom, $A, $x);
# 結果の確認 (誤差許容範囲内で一致するか)
println("\n結果の一致 (BLAS vs Custom): ", isapprox(y_blas, y_custom, rtol=1e-9))
BLAS.hemv!()
よりもさらに細かく、カスタムなメモリレイアウトや計算パターンを最適化したい場合。- Juliaの強力な最適化機能を学習し、活用したい場合。
- BLAS関数が利用できない型の組み合わせ(例: GPUArrayなど)で、手動でパフォーマンスを最適化したい場合(ただし、GPUArrayには専用のBLASライブラリがあることが多い)。
- 標準のBLAS関数ではカバーできないような、非常に特殊な線形代数演算を実装する必要がある場合。
代替方法 | 特徴 | メモリ割り当て | 高レベル/低レベル | 推奨されるケース |
---|---|---|---|---|
A * x (標準演算子) | 直感的、高レベル、自動でBLASをディスパッチ | あり | 高レベル | ほとんどの一般的な用途、可読性重視 |
mul!(y, A, x, alpha, beta) | インプレース、高レベル、自動でBLASをディスパッチ | なし | 高レベル | メモリ効率が重要、反復計算、hemv! の直接的代替 |
LoopVectorization.jl | 自動最適化、SIMD、並列化、カスタム操作 | なし | 中レベル | 特殊な演算、最大の柔軟性、BLASがカバーしないケース |
BLAS.hemv!() (オリジナル) | 最適化済み、低レベル、直接制御 | なし | 低レベル | 既存のBLASインターフェースの知識がある、特定の最適化が必要 |