はじめまして。私は大学の研究室で現在ちょっとした電磁界の
シミュレーションプログラムを作っているのですが、VB6からVB.NET2003に乗り換えたところ、
実行速度が2〜3分の一程度にまで遅くなってしまったので、
皆さんにお知恵を拝借させていただきたいと思います。
プログラムの内容自体はとても単純なもので、ひたすら電界、磁界のxyz成分を計算してゆくものです。
条件分岐すらほとんどありません。(最後の境界条件の計算に一回だけ)
コードの概要を書かせていただくと、
For t=1 to Mt
For x=0 to Mx
For y=0 to My-1
For z=0 to Mz-1
'磁界x成分Hx(x,y,z)の計算
next
next
next
'以下同様にHy(x,y,z),Hz(x,y,z),Ex(x,y,z),Ey(x,y,z),Ez(x,y,z)を計算。
'同じForループに入れないのは各々の座標の終端が(-1だけ)違うため。
'境界条件の計算。配列は参照渡し。
call Mur(Ex,Ey,Ez,Hx,Hy,Hz,'以下係数の配列境界の座標など)
Next
という具合になります。Mt=8000,電磁界の配列はMx=My=Mz=50の3次元配列です。
VB6の場合は最適化の詳細チェックをすべて入れた条件で、
VB.NETでは整数のオーバーフローチェックオフをオンにしました。
.NETは起動は遅くてもJITコンパイルを行ったあとの
演算自体は高速だと聞いていたので期待していたのですが、
目に見えて遅くなってしまったのでちょっと残念な気分です。
何か高速化するテクニックはございませんでしょうか?
頭が悪いもので数学や理科は苦手なのですが・・・
ロジック的な面で見ると
Mtはどの成分でも8000固定であれば
Mtループの中に各要素全て書けば8000×6回判定よりも
若干(目に見えるかどうかは別)早くなるかもしれない
あくまでも座標のループ回数が違うといっているので
それでもいいのかな?と思った
移植の際、Long型→Long型のような事はしていませんよね?
>ぴろあきさん
書き忘れてしまいましたが、そこはきちんと直しています。整数型は
VB6がLong、.NETがInteger、電磁界の配列は倍精度浮動小数でどちらもDoubleとしています。
>葉月さん
>Mtはどの成分でも8000固定であれば
Mtは時間のカウンタの上限値(何秒間の電磁界をシミュレーションするか)
でこの値自体は配列の要素にも計算にも使われていません。
ですからループ展開をしたりとかはちょっと無理なんです。
コーディング自体は変わっていないし、きちんとリリースモードでビルドしているので
こんなものなのでしょうか。でもVB6の半分以下の速度じゃちょっと・・・
.NETで「整数オーバーフローのチェックを解除」以外に
高速化に有効なオプションがあればいいのですが。
For t=1 to Mt
For x=0 to Mx
For y=0 to My-1
For z=0 to Mz-1
の部分を
Dim aaa1 as Integer
Dim aaa2 as Integer
aaa1=My-1
aaa2=Mz-1
For t=1 to Mt
For x=0 to Mx
For y=0 to aaa1
For z=0 to aaa2
のようにしてみてはどうでしょうか?
少しくらいは速度が変わるのではないでしょうか?
>マグさん
やってみましたけど、速度は変わりませんでした。
そういう最適化はどうもJITコンパイラの方がやってしまっていると思います。
他にもサブプロシージャーを使わずにすべてをメインに書いてみたり、
動的配列で確保していたのを静的に確保してみても大して速くなりませんでした。
純粋な数値計算でVB6より遅くなるということも考えにくいですし、
多次元配列の操作が遅いということなのでしょうか。
>多次元配列の操作が遅いということなのでしょうか。
そのようなことは無いようです。
計算の部分をコメントアウトして、代わりにHx(x,y,z) = 1
とでもして、実行時間を比べれば、どこで遅いかはすぐ判るのでは。
>ねろさん
ループの中を
Hx(x,y,z) = Hx(x,y,z) + 1
などとした場合にはさすがにVB6よりも速くなりました。
実際には、
DEx = (Ex(x, y + 1, z) - Ex(x, y, z)) * iDy
DEy = (Ey(x + 1, y, z) - Ey(x, y, z)) * iDx
Phyz(x, y, z) = PTerm(x, y, z) * Hz(x, y, z) + ExpDtt0 * Phyz(x, y, z)
Hz(x, y, z) = HTerm1(x, y, z) * Hz(x, y, z) _
+ HTerm2(x, y, z) * Phyz(x, y, z) _
+ HTerm3(x, y, z) * (DEx - DEy)
という具合になっていて、これに戻すと遅い状態に戻ってしまいます。
HTerm1,2,3,ExpDtt0,iDy,iDxはループ内で変化していません。
すべてDouble型だから型変換のコストもかからないはずなのに、
どうしてこうも速度が落ちてしまうのか・・・
ちょっと違うかもしれませんが配列が沢山使われていて物理メモリ上に収まりきっていなさそうなので、ちょっとしたヒントを。
配列が巨大で物理メモリ上に収まり切らない場合は、配列のメモリ上の配置順と次元の並び順とループのネスト順にも気を付けた方が良いです。
VB.NETはよくわかりませんが、
もし、メモリ上に
Hz(0,0,0)
Hz(1,0,0)
・・・
Hz(0,1,0)
Hz(1,1,0)
・・・
Hz(0,0,1)
Hz(1,0,1)
と配置される仕様になっているなら、
ループのネスト順を入れ替えて影響がないなら
> For x=0 to Mx
> For y=0 to My-1
> For z=0 to Mz-1
を
For z=0 to Mz-1
For y=0 to My-1
For x=0 to Mx
に入れ替える方が速くなるかもしれません。
もし、ループのネスト順を入れ替える事が出来ないなら、配列の配列として
Hz(x).yz(y,z)の様な使い方にするか、
Hz(x,y,z) -->HzRev(z,y,x)
のように添え字の並び順を変える事も検討してみて下さい。
ポイントは、
「続けてアクセスする対象データは同じメモリページ上に収まるように配置する」
ということです。
そういう意味では、VB6の書き方になってしまいますが
Type DataRecord
Hz As Double
Ey As Double
Phyz As Double
PTerm As Double
HTerm1 As Double
HTerm2 As Double
HTerm3 As Double
End Type
Dim DataArrar As DataRecored(8000,8000,8000)
の方が良いかもしれません。
VB6で
Private Declare Function timeGetTime Lib "winmm.dll" () As Long
Private Sub Command1_Click()
Dim n As Long
Dim a As Double
Dim b As Double
Dim c As Double
Dim t As Long
t = timeGetTime
a = 1234567890
b = 1234567
For n = 1 To 10000000
c = a * b
Next
Text1.Text = c & "時間=" & (timeGetTime - t)
End Sub
VB.NETで
Private Sub Command1_Click(ByVal eventSender As System.Object, ByVal eventArgs As System.EventArgs) Handles Command1.Click
Dim n As Integer
Dim a As Double
Dim b As Double
Dim c As Double
Dim t As Integer
t = timeGetTime
a = 1234567890
b = 1234567
For n = 1 To 10000000
c = a * b
Next
Text1.Text = c & "時間=" & (timeGetTime - t)
End Sub
ともに最適化しています
処理時間は
VB.NET----32mSec
VB6-------62mSec
でした
Doubleの単純計算ではVB.NETの方が倍のスピードです。
たぶんVB6からのアップグレードウィザードを使われていると思いますが、
ループカウンターの設定が数値ではなくObjectになっていませんか?
VB6で型宣言が無いとアップグレードウィザードでObject型に変換されます。
Dim n As Objectの様に。
>ひろさん
>Dim DataArrar As DataRecored(8000,8000,8000)
Mt=8000と言うのは、たとえば1ps=1e-12secごとの電磁界を計算した場合に、
8ナノ秒間の電磁界を計算するというわけですが、実際に出力するのは
ある観測点の電磁界で、配列要素を8000も確保しているわけでは無いんです。
配列として確保しているのは空間要素としての50*50*50の3次元ですから、
メモリが不足すると言うことはちょっと考えられないのです。
研究室のPCは搭載メモリが2GBもある贅沢なものですし。
>ねろさん
アップグレードウィザードを使うと、自分の知らないところでいろいろ変更されて気分が悪いので、
一から打ち直しました。確認したところ変数の型で間違えていると
思うところはございません。IntegerとDouble型しか使っていません。
ちなみにDoubleをSingleに変更してもVB6のDoubleに負けてしまいます。
ねろさんの書かれたコードですとcの値がループ中で一定なので
もしかしたらその違いはコンパイラの最適化の影響によるような気もしますが、
今計算中なのでちょっと確認が取れません。
VB6には「配列の範囲チェックを削除」のオプションがあるのでこれのおかげで
速くなっているのかも…?
…かなりの妥協になるが…問題の部分だけ外部関数として作るとか、
C#で作り直してunsafe使ってポインタ操作にするとか。
再現可能な最小限のコードを見てみたい気もするなぁ…>(дll
3次元配列なので配列の境界チェックの影響がかなり大きそうですね。
1次元配列に置き換ると境界チェックの回数が1/3になりますから、
Dim Hz(0 to Xcount-1, 0 to Count-1, 0 to Zcount-1)
--->
Dim Hz(0 to Xcount*Ycount*Zcount)
Const YZcount=Zcount*Ycount
Hz(x,y,z) --> Hz(X*YZcount + y*Zcount + z)
のようにしてはどうでしょうか?
さらに、2のべき乗の最適化でシフト演算に置換できる可能性があるので
Zcount,Ycount を2のべき乗(64,128,256,512,...)にすると
少し改善できるかもしれません
>ガッさん
そもそも数値計算でVBを使う利点というのはそんなにないはずなんですけど、
C言語はどうも取っ付き難くて。
>再現可能な最小限のコードを見てみたい気もするなぁ…>(дll
ここにすべてのコードを書き写すわけにもいかないですし、
現在はあまりコードをいじっている状態でもないですけど、
要素数50〜100程度の3次元配列を複数確保して、それぞれを
参照させながら何か計算させてゆくと、遅くなってしまうような感じです。
ちなみに配列の動的確保がまずいかと思って静的確保で実行しても遅いままでした。
>ひろさん
それは一回試してみる必要があるかもしれませんね。
ただ、いい加減シミュレーションを行っていかなければならないので、
しばらくはVB6で作ったプログラムで計算を行っていこうと思います。
もし何か原因がわかりましたら報告させていただきたいと思います。
ツイート | ![]() |