BLAS.hemv!()だけじゃない!Juliaで行列ベクトル積を効率化する代替手法

2025-05-27

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が複素数型なのに、xyが実数型になっている。BLAS関数は、同じ種類の数値型(実数か複素数か)で統一されていることを期待します。
  • 行列A、ベクトルx、ベクトルyの要素の型がBLASがサポートしていない型(例: Int)である。
  • alphabetaに整数型(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
    
  • 型の整合性
    • alphabetaは、行列とベクトルの要素の型に合わせた浮動小数点型(Float64, Float32)または複素数型(ComplexF64, ComplexF32)であることを確認します。
    • A, x, yの要素の型がすべて同じであり、かつBLASがサポートする数値型であることを確認します。特に、エルミート行列を扱うため、複素数型が一般的です。

InexactError (不正確な型変換)

これはhemv!()の直接的なエラーというよりは、引数を準備する過程で発生する可能性があります。例えば、Int型を期待する場所にFloat型を渡そうとしたり、その逆を行おうとしたりする場合です。

考えられる原因

  • データフレームなどから抽出したデータで、意図しない型になっている場合。
  • 特にuplo引数に文字列ではなく、数値などを渡そうとした場合。

トラブルシューティング

  • 引数の型が厳密に正しいことを確認します。uploChar型(例: '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ライブラリもデフォルトで複数のスレッドを使用するように設定されている場合。
  • パフォーマンスの監視
    パフォーマンスが期待通りでない場合、@timeBenchmarkToolsパッケージを使って、どこで時間がかかっているか、メモリ割り当てが多くないかなどを確認します。
  • 環境変数の確認
    OPENBLAS_NUM_THREADSMKL_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))

解説

  1. using LinearAlgebra: LinearAlgebraモジュールをインポートします。BLAS関数はこのモジュール内にあります。
  2. A, x, y の定義: それぞれエルミート行列、入力ベクトル、出力ベクトルを定義します。hemv!は複素数をサポートしているため、ComplexF64型を使用しています。
  3. alphabetaの定義: それぞれスカラー係数αとβを設定します。ここではalpha = 1.0 + 0.0imbeta = 0.0 + 0.0imとすることで、y←Ax の計算になります。
  4. LinearAlgebra.BLAS.hemv!('U', alpha, A, x, beta, y):
    • 'U': 行列Aの上三角部分のみを使って計算を行うことを指定します。'L'(下三角)も使用できます。
    • alpha: スカラーαです。
    • A: エルミート行列です。
    • x: 入力ベクトルです。
    • beta: スカラーβです。
    • y: 計算結果が上書きされる出力ベクトルです。

例2: 既存のベクトルに結果を加算する (y←αAx+βy)

この例では、yに初期値があり、その初期値にA*xalpha倍を加算するケースを示します。

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))

解説

  1. A, x, y の定義: 実数の対称行列とベクトルを使用しています。yには初期値が入っています。
  2. 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))

解説

  1. A_real: 実数型の対称行列です。実対称行列はエルミート行列の特殊なケースです。
  2. x_complex: 複素数型のベクトルです。
  3. y_complex: 計算結果が複素数になるため、複素数型のゼロベクトルで初期化します。
  4. alphabetaも、xyの型に合わせてComplexF64で定義しています。
  5. 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

解説

  1. using BenchmarkTools: BenchmarkToolsパッケージは、コードの実行時間を正確に測定するのに役立ちます。@btimeマクロを使用しています。
  2. N = 2000: 非常に大きな行列とベクトルを作成し、パフォーマンスの違いが顕著になるようにします。
  3. BLAS.set_num_threads(1): BLASが使用するスレッド数を1に制限します。これにより、シングルスレッドでのパフォーマンスを確認できます。
  4. BLAS.get_num_threads(): 現在BLASが使用しているスレッド数を取得します。これは通常、デフォルトでCPUコアの数に設定されています。
  5. 異なるスレッド数で@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インターフェースの知識がある、特定の最適化が必要