Juliaで行列計算を高速化!hpmv!()の基礎と活用法

2025-05-26

hpmv は "Hermitian Packed Matrix Vector" の略で、Hermitian行列(エルミート行列)とベクトルの積を計算し、その結果を既存のベクトルに加算する操作を行います。末尾の ! は、この関数が入力引数の一つ(この場合は y ベクトル)を「インプレース」(その場で)変更することを示します。

具体的には、以下の計算を実行します。

y:=α⋅A⋅x+β⋅y

ここで:

  • β: スカラー値です。既存の y ベクトルに乗算されます。
  • α: スカラー値です。A⋅x の結果に乗算されます。
  • y: 出力ベクトルであり、計算結果が上書きされるベクトルです。
  • x: 入力ベクトルです。
  • A: エルミート行列です。特に、この関数は「パック形式」(packed format)で格納されたエルミート行列を扱います。パック形式では、行列の対称性(共役転置関係)を利用して、行列の半分(上三角または下三角)のみがメモリに格納されます。これによりメモリ使用量を節約できます。

hpmv!() の主な特徴と用途

  1. BLAS Level 2 ルーチン: hpmv! はBLAS Level 2ルーチンに分類されます。これは、行列とベクトルの積(O(N2) 程度の演算)のような操作を効率的に実行するために設計されています。
  2. インプレース操作: 関数名の ! が示すように、y ベクトルが上書きされます。これにより、新しいメモリ割り当てが不要になり、パフォーマンスが向上します。
  3. Hermitian行列: エルミート行列(複素数の場合 A=AH、実数の場合 A=AT で対称行列)に特化しています。これにより、行列の半分の要素のみを計算に利用することで効率化を図ります。
  4. パック形式: 行列 A はパック形式で渡されます。これは、行列の要素を一次元配列に効率的に格納する方法です。BLASのドキュメントで詳細な格納方法が定義されています。
  5. パフォーマンス: BLASルーチンは、線形代数演算を非常に高速に実行するために高度に最適化された低レベルのライブラリ(通常はFortranやCで書かれている)のラッパーです。Juliaからこれらの関数を直接呼び出すことで、CやFortranで書かれたコードに近い高性能な数値計算を行うことができます。

実際の hpmv! 関数の呼び出しは、uplo (上三角または下三角のどちらが格納されているかを示す文字)、パック形式の行列 AP、ベクトル xy、そしてスカラー alphabeta などの引数を必要とします。

using LinearAlgebra

# エルミート行列 A (例として)
# hpmv! はパック形式の行列を必要とするため、ここではその変換は省略します。
# 実際の使用では、Aをhpmv!が要求するパック形式の一次元配列APに変換する必要があります。
# 例えば、BLAS.hpmv!('U', alpha, AP, x, beta, y) のように使います。
# 'U' は上三角部分を使うことを示します。

A_full = [2.0+0.0im 1.0-1.0im; 1.0+1.0im 3.0+0.0im] # Hermitian行列の例
x = [1.0+0.0im, 2.0+0.0im]
y = [0.5+0.0im, 1.5+0.0im]
alpha = 1.0
beta = 0.0 # beta = 0.0 の場合、y = alpha * A * x となります

# パック形式のA (概念的な説明のため、実際の変換コードは省略)
# BLAS.hpmv! は直接Matrix型を受け取るわけではないことに注意してください。
# 実際には、A_full をパック形式に変換した AP を渡します。
# ここでは直接的な呼び出し例は示しませんが、目的は上記の数式計算です。

# もし LinearAlgebra.hpmv! がより高レベルなインターフェースを提供している場合:
# y_result = LinearAlgebra.hpmv!(uplo, alpha, A_full, x, beta, y)
# ただし、BLAS.hpmv! は生のBLAS呼び出しなので、より詳細な引数が必要です。
# Juliaのより高レベルな mul! 関数などが、内部で適切なBLAS関数を呼び出します。


よくあるエラーとトラブルシューティング

    • エラー: MethodError: no method matching hpmv!(...)DimensionMismatch
    • 原因: hpmv! は特定の型の引数(例: ComplexF64Float64Vector)を期待します。また、行列とベクトルの次元が数学的に整合している必要があります。特に、hpmv! は「パック形式」のエルミート行列 AP を受け取るため、通常のMatrix型をそのまま渡すことはできません。
    • トラブルシューティング:
      • : xy ベクトル、そして AP に格納される要素の型が一致していることを確認します。複素数演算には ComplexF32 または ComplexF64 を、実数演算には Float32 または Float64 を使用します。
      • 次元: AP が N×N のエルミート行列に対応する場合、xy は長さ N のベクトルである必要があります。
      • パック形式: 最も一般的な間違いは、AP に通常の2次元配列(Matrix)を渡してしまうことです。hpmv! はエルミート行列の要素を効率的に格納した1次元配列(ベクトル)を期待します。パック形式への変換は手動で行う必要がありますが、通常はLinearAlgebra.Hermitian型をBLAS.hpmv!が内部的に処理する際に自動的に行われます。直接BLAS.hpmv!を呼び出す場合は、[LAPACK.syrk_LAPACK.hetrf_ など、パック形式を生成する関数や、自身で要素を詰めるロジックが必要になります。
        • 例: N×N エルミート行列の上三角部分をパック形式(N(N+1)/2 個の要素を持つ1次元配列)で格納する場合、要素 Aijは特定のインデックスにマッピングされます。
  1. uplo 引数の誤り (Incorrect uplo Argument)

    • エラー: 計算結果が期待と異なる、またはエラーメッセージが表示される。
    • 原因: hpmv! はエルミート行列のどの部分(上三角 U または下三角 L)がパック形式で格納されているかを示す文字(Char)引数 uplo を取ります。これが実際に格納されているデータと一致しない場合、誤った計算が行われます。
    • トラブルシューティング:
      • AP を生成する際に、上三角部分をパック形式で格納した場合は 'U' を、下三角部分を格納した場合は 'L' を指定します。
  2. スカラー引数 alpha および beta の誤り (Incorrect alpha and beta Arguments)

    • エラー: 予期しない結果、または数値的な問題。
    • 原因: alphabeta は、計算 y:=α⋅A⋅x+β⋅y におけるスケーリング係数です。これらの値が意図しない場合、結果が大きく変わります。特に、beta0.0 以外の値に設定した場合、y の初期値が結果に影響します。
    • トラブルシューティング:
      • 計算したい線形代数演算に合わせて、alphabeta を適切に設定します。例えば、A⋅x を計算したいだけであれば、alpha = 1.0beta = 0.0 とし、y は適切なサイズのゼロベクトルで初期化します。
  3. スレッドの問題 (Threading Issues)

    • エラー: 期待したパフォーマンスが出ない、またはデッドロック。
    • 原因: BLASライブラリ(OpenBLAS, MKLなど)は通常、内部でマルチスレッド処理を行います。Juliaも自身のスレッド(Threads.@spawn など)を持っています。これらが適切に設定されていない場合、BLASスレッドとJuliaスレッドがコアを奪い合い(オーバーサブスクリプション)、パフォーマンスが低下したり、競合状態が発生したりすることがあります。
    • トラブルシューティング:
      • BLASスレッド数の設定: Juliaでマルチスレッドを使用する場合 (julia -t auto など)、BLASのスレッド数を1に設定することが強く推奨されます。これにより、Juliaがスレッドの管理を行い、BLASは各Juliaスレッド内でシングルスレッドで実行されます。
        using LinearAlgebra
        BLAS.set_num_threads(1)
        
      • 環境変数: Juliaの起動前に環境変数 OPENBLAS_NUM_THREADS=1 または MKL_NUM_THREADS=1 を設定することもできます。
      • スレッドピンニング: ThreadPinning.jl パッケージのようなツールを使用して、JuliaスレッドとBLASスレッドのCPUコアへの割り当てを明示的に制御することで、パフォーマンスをさらに最適化できる場合があります。
      • プロファイリング: パフォーマンスの問題を特定するために、@timeBenchmarkTools.jlProfile.jl を使用して、どこで時間がかかっているかを特定します。
  4. BLASライブラリの問題 (BLAS Library Issues)

    • エラー: ERROR: LoadError: could not find function ... in library ... や数値計算の誤り。
    • 原因: Juliaが使用しているBLASライブラリが壊れているか、互換性がないか、または特定のハードウェアで既知のバグがある可能性があります。特にWindowsや特定のCPUアーキテクチャでOpenBLASにバグが報告されることがあります。
    • トラブルシューティング:
      • Juliaの再インストール: Juliaを再インストールすることで、デフォルトで提供されるBLASライブラリが適切に設定されることがあります。
      • BLASライブラリの切り替え: MKL.jl パッケージを使用して、Intel MKL BLASライブラリに切り替えることを検討します。MKLは通常、非常に高いパフォーマンスと安定性を提供します。
        # Pkg.add("MKL") # 必要であればインストール
        using MKL # これをロードするとMKLがデフォルトのBLASになる
        
      • BLASのバージョン確認: versioninfo() を実行して、どのBLASライブラリが使用されているか(OpenBLAS, MKLなど)を確認し、そのバージョンで既知の問題がないか調べます。
      • 環境変数の確認: LD_LIBRARY_PATH (Linux) や PATH (Windows) など、システムが参照するライブラリパスに問題がないか確認します。
  5. 複素数演算におけるNaN/Inf (NaN/Inf in Complex Operations)

    • エラー: 計算結果が NaN (Not a Number) や Inf (Infinity) になる。
    • 原因: 入力データにすでに NaNInf が含まれている場合、または非常に大きな/小さな値の計算でオーバーフロー/アンダーフローが発生した場合。複素数型での NaNInf は、実数部または虚数部のどちらかが NaNInf になると発生します。
    • トラブルシューティング:
      • 入力データ A, x, yNaNInf が含まれていないか確認します (any(isnan, A), any(isinf, A) など)。
      • 数値安定性の観点から、alphabeta の値を調整するか、スケールを調整することを検討します。
      • より高い精度(ComplexF64 の代わりに BigComplex)の使用を検討します(ただし、パフォーマンスは低下します)。
  • ドキュメントの参照: BLAS関数の正確な仕様は、Juliaのドキュメント(?BLAS.hpmv!)や、より詳細なBLASの公式ドキュメントで確認できます。特に、パック形式の行列の格納方法については、BLAS仕様書を確認することが重要です。
  • @code_lowered, @code_typed, @code_llvm, @code_native: これらのマクロを使って、Juliaが hpmv! 呼び出しをどのようにコンパイルしているかを確認できます。これにより、型推論の問題や、予期せぬジェネリックなディスパッチが発生していないかを確認できます。
  • 高レベル関数との比較: LinearAlgebra.mul!(y, Hermitian(A_full, uplo_char), x, alpha, beta) のように、Juliaのより高レベルな関数を使用して同じ計算を行い、結果を比較します。これにより、低レベルのBLAS呼び出しの問題か、データ準備の問題かを切り分けられます。
  • 簡単なケースから始める: 非常に小さい行列とベクトルで hpmv! をテストし、期待される結果を手計算で確認します。


ここでは、hpmv!() の使用例をいくつか示します。BLAS関数は通常、Float64ComplexF64 などの特定の数値型に特化しているため、その点も考慮します。

実数エルミート行列 (実対称行列) の場合

実数の場合、エルミート行列は対称行列になります。hpmv! は複素数を想定していますが、実数を渡すことも可能です。この場合、hpmv は実対称行列の動作をします。

ただし、hpmv!パック形式の行列を受け取るため、通常の2次元配列をそのまま渡すことはできません。パック形式とは、対称性(この場合は実対称性)を利用して、行列の上三角部分または下三角部分の要素のみを1次元配列に格納する方法です。

パック形式のインデックス付けは少し複雑ですが、以下の例では手動で AP を構築しています。

using LinearAlgebra

# 行列のサイズ
N = 3

# エルミート行列 (実数の場合は対称行列)
A_full = [
    1.0  2.0  3.0
    2.0  4.0  5.0
    3.0  5.0  6.0
]

# ベクトル x
x = [1.0, 2.0, 3.0]

# 結果を格納するベクトル y (初期値)
y = [0.0, 0.0, 0.0]

# スカラー
alpha = 1.0
beta = 0.0 # y = alpha * A * x としたいので beta = 0.0

# パック形式の行列 AP を作成
# これは上三角部分を列優先で格納する一般的な方法です。
# A = [ A11 A12 A13 ]
#     [     A22 A23 ]
#     [         A33 ]
# AP = [A11, A21(A12), A31(A13), A22, A32(A23), A33] のように詰める
# または上三角部分を列優先で: A11, A12, A13, A22, A23, A33 の順

# N=3 の場合、パック形式のサイズは N*(N+1)/2 = 3*4/2 = 6
AP = zeros(Float64, N * (N + 1) ÷ 2)

# 上三角部分をパック形式に手動で格納
# FortranのBLASルーチンは通常、列優先(Column-Major)です。
# 1-based indexing for Julia arrays
# AP[1] = A[1,1]
# AP[2] = A[1,2]
# AP[3] = A[1,3]
# AP[4] = A[2,2]
# AP[5] = A[2,3]
# AP[6] = A[3,3]

# uplo = 'U' (Upper triangle) の場合
# APのインデックス k は (j-1)*j/2 + i (for i <= j)
# あるいは、より一般的な公式は k = i + j*(j-1)/2 - j*(j-1)/2 + (i-1)
# 別の直感的な詰め方:
# A11
# A12 A22
# A13 A23 A33
# これを列方向に並べると [A11, A12, A13, A22, A23, A33] となります。
# (1,1) -> AP[1]
# (1,2) -> AP[2]  (2,2) -> AP[3]
# (1,3) -> AP[4]  (2,3) -> AP[5]  (3,3) -> AP[6]

# これは正しいインデックス付けではない可能性があります。BLASの正確なパック形式の定義を参照すべきです。
# BLASのhpmvのPacked Storageに関するLAPACKのドキュメント (DPF for packed Hermitian storage)
# 例えば、LAPACKのdpfは、上三角を詰める場合、A[i,j] (i<=j) の要素を
# j = 1: N
#   i = 1: j
#     AP(k) = A(i,j)
# という順で格納します。
# For N=3:
# j=1: i=1 -> AP[1] = A[1,1]
# j=2: i=1 -> AP[2] = A[1,2]
#      i=2 -> AP[3] = A[2,2]
# j=3: i=1 -> AP[4] = A[1,3]
#      i=2 -> AP[5] = A[2,3]
#      i=3 -> AP[6] = A[3,3]

# このルールに従ってAPを埋める
AP[1] = A_full[1,1]
AP[2] = A_full[1,2]
AP[3] = A_full[2,2]
AP[4] = A_full[1,3]
AP[5] = A_full[2,3]
AP[6] = A_full[3,3]

# BLAS.hpmv! の呼び出し
# uplo: 'U' (Upper triangle) または 'L' (Lower triangle)
# N: 行列の次数
# alpha: スカラー
# AP: パック形式の行列 (1次元配列)
# x: 入力ベクトル
# incx: xの要素間のストライド (通常は1)
# beta: スカラー
# y: 出力ベクトル (上書きされる)
# incy: yの要素間のストライド (通常は1)

# BLAS.hpmv! のシグネチャ (引数の順序)
# hpmv!(uplo::AbstractChar, n::Integer, alpha::Real, AP::StridedVector, x::StridedVector, incx::Integer, beta::Real, y::StridedVector, incy::Integer)

println("--- 実数エルミート行列 (対称行列) の場合 ---")
println("元の行列 A_full:\n", A_full)
println("入力ベクトル x: ", x)
println("初期 y: ", y)
println("構築されたパック形式 AP: ", AP)

BLAS.hpmv!('U', N, alpha, AP, x, 1, beta, y, 1)

println("BLAS.hpmv! 後の y: ", y)

# 期待される結果を手動で計算 (または LinearAlgebra.mul! で確認)
y_expected = A_full * x
println("手計算/mul! による期待される y: ", y_expected)

# 比較
println("結果が一致するか?: ", y ≈ y_expected)

複素数エルミート行列の場合

エルミート行列は、共役転置 (conjugate transpose) を取っても元の行列と等しい行列です。 A=AH(Aij​=Aji​​)

using LinearAlgebra

# 行列のサイズ
N_comp = 2

# 複素数エルミート行列
# A = [ A11  A12 ]
#     [ A21  A22 ]
# ここで A21 = conj(A12) かつ A11, A22 は実数 (エルミート性の定義より)
A_comp_full = [
    2.0+0.0im   1.0-1.0im
    1.0+1.0im   3.0+0.0im
]

# ベクトル x
x_comp = [1.0+0.0im, 2.0+0.0im]

# 結果を格納するベクトル y (初期値)
y_comp = [0.0+0.0im, 0.0+0.0im]

# スカラー
alpha_comp = 1.0+0.0im
beta_comp = 0.0+0.0im

# パック形式の行列 AP_comp を作成
# N=2 の場合、サイズは N*(N+1)/2 = 2*3/2 = 3
AP_comp = zeros(ComplexF64, N_comp * (N_comp + 1) ÷ 2)

# 上三角部分をパック形式に格納 (BLAS/LAPACKの慣例に従う)
# A = [A11 A12 ]
#     [    A22 ]
# AP = [A11, A12, A22] (列優先で上三角を詰める)
AP_comp[1] = A_comp_full[1,1] # A11
AP_comp[2] = A_comp_full[1,2] # A12
AP_comp[3] = A_comp_full[2,2] # A22

println("\n--- 複素数エルミート行列の場合 ---")
println("元の行列 A_comp_full:\n", A_comp_full)
println("入力ベクトル x_comp: ", x_comp)
println("初期 y_comp: ", y_comp)
println("構築されたパック形式 AP_comp: ", AP_comp)

BLAS.hpmv!('U', N_comp, alpha_comp, AP_comp, x_comp, 1, beta_comp, y_comp, 1)

println("BLAS.hpmv! 後の y_comp: ", y_comp)

# 期待される結果を手動で計算
y_comp_expected = A_comp_full * x_comp
println("手計算/mul! による期待される y_comp: ", y_comp_expected)

# 比較
println("結果が一致するか?: ", y_comp ≈ y_comp_expected)

beta を非ゼロにすると、y の初期値が結果に加算されます。

using LinearAlgebra

N = 2
A_full = [1.0 0.5; 0.5 2.0] # 対称行列
x = [1.0, 2.0]
y_initial = [10.0, 20.0] # yの初期値
y_beta = copy(y_initial) # hpmv! はインプレースなのでコピーする

alpha = 1.0
beta = 0.5 # y := 1.0 * A * x + 0.5 * y_initial

# パック形式 AP (上三角)
AP = zeros(Float64, N * (N + 1) ÷ 2)
AP[1] = A_full[1,1]
AP[2] = A_full[1,2]
AP[3] = A_full[2,2]


println("\n--- beta を非ゼロにする例 ---")
println("元の行列 A_full:\n", A_full)
println("入力ベクトル x: ", x)
println("初期 y_initial: ", y_initial)
println("構築されたパック形式 AP: ", AP)
println("alpha: ", alpha, ", beta: ", beta)

BLAS.hpmv!('U', N, alpha, AP, x, 1, beta, y_beta, 1)

println("BLAS.hpmv! 後の y_beta: ", y_beta)

# 期待される結果
y_expected_beta = alpha * (A_full * x) + beta * y_initial
println("手計算/mul! による期待される y_expected_beta: ", y_expected_beta)

println("結果が一致するか?: ", y_beta ≈ y_expected_beta)

これらの例は、LinearAlgebra.BLAS.hpmv!() を直接使用する方法を示しています。重要なポイントは以下の通りです。

  • インプレース操作: ! が示すように、y ベクトルは関数内で変更されます。元の y の値が必要な場合は、事前にコピーを作成する必要があります。
  • incx, incy: ベクトルの要素間のストライドです。通常は連続したメモリのため 1 を指定します。疎行列や部分配列を扱う場合に 1 以外の値を使用することがあります。
  • uplo 引数: パック形式で格納されている行列のどの部分(上三角 'U' または下三角 'L')を使うかを示します。
  • 引数の型: Float64ComplexF64 など、適切な数値型を使用します。
  • パック形式の行列 AP の準備: これが最もトリッキーな部分です。BLAS/LAPACKの仕様に従って、行列の要素を1次元配列に正しく詰める必要があります。上記例では上三角部分を列優先で詰める一般的な慣例に従いました。


BLAS.hpmv!() の代替となる主な方法は以下の通りです。

LinearAlgebra.mul!() を使用する(最も推奨される方法)

Julia の LinearAlgebra モジュールは、線形代数演算のための高レベルなインターフェースを提供しており、内部で適切なBLASルーチンを呼び出すようになっています。エルミート行列とベクトルの積 y:=αAx+βy を計算する最も一般的な方法は、LinearAlgebra.mul!() 関数を使用することです。

mul!() は、さまざまな行列とベクトルの積に対応する柔軟な関数です。エルミート行列を扱う場合は、Hermitian 型を引数として渡します。

using LinearAlgebra

# 行列のサイズ
N = 3

# 複素数エルミート行列 (または実対称行列)
A_full = [
    2.0+0.0im   1.0-1.0im   0.5+0.5im
    1.0+1.0im   3.0+0.0im   0.0-2.0im
    0.5-0.5im   0.0+2.0im   4.0+0.0im
]

# Hermitian 型に変換(BLASは内部でこれをパック形式に変換します)
# 'U' は上三角部分を使うことを示します
A_herm = Hermitian(A_full, 'U') 

# 入力ベクトル x
x = [1.0+0.0im, 2.0+0.0im, 3.0+0.0im]

# 結果を格納するベクトル y (初期値)
y = [10.0+0.0im, 20.0+0.0im, 30.0+0.0im]

# スカラー
alpha = 1.0+0.0im
beta = 0.5+0.0im

println("--- mul!() を使用する例 ---")
println("元の行列 A_full:\n", A_full)
println("Hermitian 型 A_herm:\n", A_herm) # Hermitian型は表示上も簡潔
println("入力ベクトル x: ", x)
println("初期 y: ", y)
println("alpha: ", alpha, ", beta: ", beta)

# mul!(Y, A, B, alpha, beta) は Y = alpha * A * B + beta * Y を計算します
mul!(y, A_herm, x, alpha, beta)

println("mul! 後の y: ", y)

# 期待される結果を手動で確認
y_expected = alpha * (A_full * x) + beta * [10.0+0.0im, 20.0+0.0im, 30.0+0.0im]
println("期待される y (手計算): ", y_expected)
println("結果が一致するか?: ", y ≈ y_expected)

mul!() を推奨する理由

  • 柔軟性: 他の行列やベクトル型(例えば、Adjoint型など)に対しても一貫したインターフェースを提供します。
  • 型の安全性: 型の不一致によるエラーが減ります。
  • 自動最適化: Julia は Hermitian 型を認識し、内部で最適なBLASルーチン(この場合は hpmv!)を自動的に呼び出します。これにより、手動で BLAS.hpmv! を呼び出すのと同等のパフォーマンスが得られます。
  • 抽象化と簡潔性: 行列をパック形式に変換する手間を省き、コードが非常に読みやすくなります。

* 演算子を使用する(最も直感的で一般的な方法)

もし、y:=Ax のように単純な行列とベクトルの積を計算したいだけで、既存の y に結果を加算する必要がない(つまり β=0 の場合)のであれば、Julia の標準的な * 演算子を使用するのが最も直感的です。

using LinearAlgebra

N = 3
A_full = [
    2.0+0.0im   1.0-1.0im   0.5+0.5im
    1.0+1.0im   3.0+0.0im   0.0-2.0im
    0.5-0.5im   0.0+2.0im   4.0+0.0im
]
A_herm = Hermitian(A_full, 'U')
x = [1.0+0.0im, 2.0+0.0im, 3.0+0.0im]

println("\n--- * 演算子を使用する例 ---")
println("A_herm:\n", A_herm)
println("x: ", x)

# A * x は A_herm * x を計算します
# これは新しいベクトルを割り当てます
y_star = A_herm * x 

println("A_herm * x の結果 y_star: ", y_star)

# 期待される結果
y_expected_star = A_full * x
println("期待される y (手計算): ", y_expected_star)
println("結果が一致するか?: ", y_star ≈ y_expected_star)

* 演算子を推奨する理由

  • 新しい割り当て: * 演算子は新しいベクトルを割り当てるため、元のベクトルが変更されることを心配する必要がありません(ただし、大きなデータセットでは追加のメモリ割り当てがパフォーマンスに影響する可能性があります)。
  • 自動最適化: Hermitian 型と * 演算子の組み合わせも、内部で最適なBLASルーチンを効率的に呼び出します。
  • 利便性: 短い式で計算を記述できます。
  • 最高の可読性: 数学的な表記に最も近いため、コードが非常に読みやすいです。

Hermitian 以外にも、Julia の LinearAlgebra には Symmetric, UpperTriangular, LowerTriangular, Diagonal, Tridiagonal などの特殊な行列型があります。これらもまた、適切なBLASルーチンを内部的に利用することで、効率的な計算を可能にします。

例えば、実対称行列の場合は Symmetric 型を使用できます。

using LinearAlgebra

N_sym = 3
A_sym_full = [
    1.0  2.0  3.0
    2.0  4.0  5.0
    3.0  5.0  6.0
]
A_sym = Symmetric(A_sym_full, 'U') # 実対称行列
x_sym = [1.0, 2.0, 3.0]
y_sym = [0.0, 0.0, 0.0]
alpha_sym = 1.0
beta_sym = 0.0

println("\n--- Symmetric 型と mul!() を使用する例 ---")
println("A_sym:\n", A_sym)
println("x_sym: ", x_sym)

mul!(y_sym, A_sym, x_sym, alpha_sym, beta_sym)

println("mul! 後の y_sym: ", y_sym)
println("期待される y_sym: ", A_sym_full * x_sym)
println("結果が一致するか?: ", y_sym ≈ A_sym_full * x_sym)

Julia でエルミート行列(または対称行列)とベクトルの積を計算する場合、LinearAlgebra.mul!()Hermitian (または Symmetric) 型を組み合わせるのが、パフォーマンスとコードの可読性のバランスが最も取れた方法です。単純な積で新しいベクトルが必要な場合は * 演算子を使用します。これらの高レベルな代替手段は、BLASの低レベルな詳細を意識することなく、高速な数値計算を可能にします。