Delphiのみで人工ニューラルネットワーク(Artificial Neural Network)のクラスを作りました。
DLLなどは不要です。単純な回帰問題や分類問題なら解けるみたいです。
https://mam-mam.net/delphi/ann.html
参考までですが、このクラスを使って、日経平均株価終値を予測してみましが、
やはりダウ平均株価や、その他もろもろの情報も学習させないと、精度が悪いようです。
https://mam-mam.net/delphi/ann_stock.html
DelphiではGPUは使えないと思いますのでCPU処理になりますが、
処理速度を速くする方法や、精度を上げる方法などがありますでしょうか。
こんにちは。今手元に最近のDelphiがなく、動作させられないのです(TArrayはLazarusには実装されているのですが、TStreamのReadDataのような型自由なメソッドとかが無いので)。
高速化したいとすれば
function TMamAnn.Sigmoid(x: Single): Single;
begin //シグモイド関数
result:=1.0/(1.0+system.exp(-x))
end;
function TMamAnn.SigmoidDerivative(x:single): single;
begin //シグモイド関数の微分関数
result:=Sigmoid(x);
result:=result*(1.0-result);
end;
と RandG 関数くらいでしょうか
たぶん精度は必要なく、そこそこ2割程度の誤差で似た結果が得られれば使い物になるでしょうから
裏目小僧 様
お返事ありがとうございます。
RandG 関数は初期化時に使っているだけなので、高速化しても全体への影響が殆どないみたいなのを確認しています。
問題となるシグモイドとシグモイドの微分関数(導関数)で使用しているexp関数ですが、Delphiのライブラリ(system.pas)のソースコードを確認しますと、
アセンブラで記述されていまして浮動小数点スタックレジスタとFPUをつかってハードウェア(CPU)で処理を行っていて
FLDL2E CPU命令( x*log2e がハードウェアで高速に処理)
FXCH CPU命令(2乗処理をハードウェアで高速処理)
などなど、CPU命令(ハードウェア)で処理を行っている為、多少の誤差を無視したプレ計算などの高速化処理をソフトウェアで行ったところで到底速くならなさそうです・・・。
そこで、中間層の最適化関数にSigmoidではなく、ReLUも選択できるようにした改造版を公開しました。
https://mam-mam.net/delphi/ann.html
中間層及び中間層にニューロンが多い場合にはReLUを使用すると処理速度は少し速くなりました。
これ以上速くするにはオンライン学習ではなく、ミニバッチ学習にしてTTaskなどで並列処理を行うなど、根本から学習方式を変えるしかないのかもしれません。
ご意見ありがとうございます。
そうですね。 沢山呼ばれている関数を探して、それを高速化するとしても、今のコンパイラは必ずスタックフレームを作るので、それよりもインライン化する方がマシかもしれませんし、それにしても数%といった所でしょう。
細かな高速化としては
条件判断をループ内でしないで 関数毎分けてしまうとか
メソッドにしないで関数にするとか
動的配列の配列より2のべき乗サイズの固定配列が少しだけ軽いとか
これらも数%でしかありません。それでよければ来週あたりすこしやってみますが
それよりも並列処理化出来る部分をみつけて並列処理すれば 並列処理分時間は短くなるので何倍も高速になりますからね。
インラインアセンブラでSSE命令を使うとか(もっともLazarusとDelphiでインラインアセンブラの仕様が異なるので厄介ですが)
裏目小僧 様
レスありがとうございます。
>条件判断をループ内でしないで 関数毎分けてしまうとか
なるほどです。冗長化しますがif文を通る回数がかなり減ります。
>これらも数%でしかありません。
VBのif文は遅い印象がありますが、確かにDelphiのif文は高速動作します。
その他記載の工夫もほんの少しなんですね。
>よければ来週あたりすこしやってみますが
いえいえ、アドバイスだけでも十分有り難いので、これ以上お手数をおかけするわけにはいきません。
>並列処理化出来る部分をみつけて並列処理すれば
やっぱりそうなんですよね。
色々と貴重なアドバイス有難うございます。
素のpytonの50倍以上(numpy使っても10倍以上)の速さでdelphiでは処理できてますので、良しとして妥協するか、うーん。
そうですか。時間が出来たので見に来たのですが、余計なお世話似なるので止めておきましょう。
学習が遅いのなら、
学習が何をしてるかというとたぶん確率計算をしてるのでしょう。
ようは主成分分析のような事をして、どの確率が一番高いかを求めているのでしょうね。
だから、学習を早くするなら最初からある程度の方向性を与えてやるというのが早道かもしれません。
ニュートン法とかの解を得るのに近似値を先に与えると高速になるような感じでです。
最初の重みをランダムにするのではなく回転行列を設定しておくとか面白いかもしれませんね。
まあ自分がやらないから勝手な事を言えるのですが
主成分分析は、私もフリーソフトをdelphiで作って公開しています。(相関分析と主成分分析)
https://mam-mam.net/download/scatterplot/
ちなみに重回帰分析ができるエクセルのアドインも公開しています。
https://mam-mam.net/download/mam_statistics.html
ニューラルネットワークは確率統計学とは全く別のアプローチを行うものになります。(人工的な脳構造の再現ですね。)
重みの初期値を与える時は、正規分布の乱数でで与えるのが最も収束が速い(少ない学習回数で学習できる)らしいので、採用しています。
何れにせよ、貴重なご意見を頂けることは、どんなご意見でもヒントになる可能性が高いのでとても助かります。
そうだ SSE系命令で単精度並列命令としては
ADDPS xmm1, xmm2 xmm1+=xmm2
VADDPS xmm1, xmm2, xmm3 xmm1=xmm2+xmm3
MULPS xmm1, xmm2 xmm1*=xmm2
VMULPS xmm1, xmm2, xmm3 xmm1=xmm2*xmm3
VFMADD231PS xmm1, xmm2, xmm3 xmm1+=xmm2*xmm3
VFNMADD231PS xmm1, xmm2, xmm3 xmm1-=xmm2*xmm3
Vが付いてるのは AVX系なので一度に単精度8個同時に計算出来ますよ
リンク先はSIMD命令の例です Lazarusで試しているのでasmの書き方はDelphi用に直す必要があるかもしれません。一応アセンブラの文法は{$ASMMODE intel} にしてDelphiに寄せてはいるのですが(win64環境を想定しています)
// for i:=0 to Count-1 do arD[i]:=arD[i] + coff*arS[i];
procedure AVXcoffSum(var arD, arS: single; coff: single; Count: integer);
var
//single32bitを 1ループで32個処理する
MulData: array [0..7] of single;
pD, pS: ^single;
begin
pD := @arD;
pS := @arS;
MulData[0] := coff;//値渡しなのでローカル変数にcopy
while Count >= 32 do
begin
asm //以下は FMA+AVX 命令が使えればOK
LEA RCX, MulData[0];
VBROADCASTSS ymm5, [RCX] //AVX 256bitレジスタに32bit値を埋める
MOV RAX, pS // ソースアドレスをRAXに
MOV RDX, pD //加算してゆく先
VMOVUPS YMM0, [RDX] //AVX
VMOVUPS YMM1, [RDX+32]
VMOVUPS YMM2, [RDX+64]
VMOVUPS YMM3, [RDX+96]
VFMADD231PS YMM0, YMM5, [RAX] //FMA YMM0+=YMM5*[RAX]
VFMADD231PS YMM1, YMM5, [RAX+32]
VFMADD231PS YMM2, YMM5, [RAX+64]
VFMADD231PS YMM3, YMM5, [RAX+96]
VMOVUPS [RDX], ymm0
VMOVUPS [RDX+32], ymm1
VMOVUPS [RDX+64], ymm2
VMOVUPS [RDX+96], ymm3
end ['RAX', 'RDX', 'ymm0', 'ymm1', 'ymm2', 'ymm3', 'ymm5'];
Inc(pD, 32);
Inc(pS, 32);
Dec(Count, 32);
end;
while Count > 0 do
begin
pD^ := pD^ + MulData[0] * pS^;
Inc(pD);
Inc(pS);
Dec(Count, 32);
end;
end;
色々とありがとうございます。
SSEは512ビット演算が1クロックサイクルで出来るのですが、先ずは固定4個ずつのsingle配列又はTVec4レコード型などに代入したものをストアしないといけないので、その代入コストや余り処理を考えながら作るのは、私には敷居が高すぎて作れないです。
また、私の使っているDelphiでは
VFMADD231PS
等が未定義の識別子として認識されないですね。
movups xmm0, [a]
は大丈夫なのですが、avx系は全てダメみたいです。
誠に申し訳ございません。
TVec4を間に入れる必要は無いでしょう。
まずWin64なら少なくともメモリー確保時に 16byteアラインはされていると思います。それであれば遅くはなっても動くような記述があったような。
そして、それでもパフォーマンスが半減するだけで並列に8個計算できるのですから現状の何倍もの速度は出るはずです。
32byteアラインされているかどうかは、
https://urame.sakura.ne.jp/w/wiki.cgi/lazarus?page=Lazarus%A4%C7x64+SIMD%CC%BF%CE%E1%A4%F2%BB%C8%A4%C3%A4%C6%A4%DF%A4%EB でやってるように
if (integer(pD) and (32 - 1)) <> 0 then 32byteアラインされてないよ
と確認出来ます(ポインタの下位5bitが0ならアラインされているわけです)
16byteアラインされているかどうかは、このコードで32を16にしてやれば良いわけです
アラインされていなければ、SIMD部をスキップして
while Count > 0 do
begin
pD^ := pD^ + MulData[0] * pS^;
Inc(pD);
Inc(pS);
Dec(Count, 32);
end;
だけを実行させれば良いでしょうし、この部分だけでもWIN64だと
movss -0x40(%rbp),%xmm0
mulss (%rax),%xmm0
addss (%rdx),%xmm0
mov -0x48(%rbp),%rax
movss %xmm0,(%rax)
のようにx87FPUではなく SSE命令を使っていますから これを4word並列変更するだけで高速化は出来るのでしょう。
Delphiが32bit版ならごめんなさい。
で、Delphiが64だけど アセンブラが対応してない場合は Lazarus64等のコンパイル出来る環境でコンパイルして そこから命令を直接 DBで書いてゆけばいけます。
例えば VFMADD231PS YMM0, YMM5, [RAX] なら
000000010002F87C c4e255b800 vfmadd231ps (%rax),%ymm5,%ymm0
から最初は64bitアドレス、命令バイトの並びですから c4e255b800 の部分から
DB c4h,e2h,55h,b8h,00h のように書いてやればOK
なおアセンブラでの16進数は c4hではなく 0c4hであったり 0xC4 だったりでDelphiがどうかは判りません 一度 ブレークポイントをかけてCPU窓を見て判断して下さい
以下、該当部分のLazarus-アセンブラ窓(逆アセ表示はlinuxスタイルなので転送方向が逆です)
while Count >= 32 do
000000010002F850 eb68 jmp 0x10002f8ba <AVXCOFFSUM+314>
000000010002F852 660f1f440000 nopw 0x0(%rax,%rax,1)
LEA RCX, MulData[0];
000000010002F858 488d4dc0 lea -0x40(%rbp),%rcx
VBROADCASTSS ymm5, [RCX] //AVX 256bitレジスタに32bit値を埋める
000000010002F85C c4e27d1829 vbroadcastss (%rcx),%ymm5
MOV RAX, pS // ソースアドレスをRAXに
000000010002F861 488b45b0 mov -0x50(%rbp),%rax
MOV RDX, pD //加算してゆく先
000000010002F865 488b55b8 mov -0x48(%rbp),%rdx
VMOVUPS YMM0, [RDX] //AVX
000000010002F869 c5fc1002 vmovups (%rdx),%ymm0
VMOVUPS YMM1, [RDX+32]
000000010002F86D c5fc104a20 vmovups 0x20(%rdx),%ymm1
VMOVUPS YMM2, [RDX+64]
000000010002F872 c5fc105240 vmovups 0x40(%rdx),%ymm2
VMOVUPS YMM3, [RDX+96]
000000010002F877 c5fc105a60 vmovups 0x60(%rdx),%ymm3
VFMADD231PS YMM0, YMM5, [RAX] //FMA YMM0+=YMM5*[RAX]
000000010002F87C c4e255b800 vfmadd231ps (%rax),%ymm5,%ymm0
VFMADD231PS YMM1, YMM5, [RAX+32]
000000010002F881 c4e255b84820 vfmadd231ps 0x20(%rax),%ymm5,%ymm1
VFMADD231PS YMM2, YMM5, [RAX+64]
000000010002F887 c4e255b85040 vfmadd231ps 0x40(%rax),%ymm5,%ymm2
VFMADD231PS YMM3, YMM5, [RAX+96]
000000010002F88D c4e255b85860 vfmadd231ps 0x60(%rax),%ymm5,%ymm3
VMOVUPS [RDX], ymm0
000000010002F893 c5fc1102 vmovups %ymm0,(%rdx)
VMOVUPS [RDX+32], ymm1
000000010002F897 c5fc114a20 vmovups %ymm1,0x20(%rdx)
VMOVUPS [RDX+64], ymm2
000000010002F89C c5fc115240 vmovups %ymm2,0x40(%rdx)
VMOVUPS [RDX+96], ymm3
000000010002F8A1 c5fc115a60 vmovups %ymm3,0x60(%rdx)
Inc(pD, 32);
000000010002F8A6 488145b880000000 addq $0x80,-0x48(%rbp)
Inc(pS, 32);
000000010002F8AE 488145b080000000 addq $0x80,-0x50(%rbp)
Dec(Count, 32);
000000010002F8B6 836de020 subl $0x20,-0x20(%rbp)
while Count >= 32 do
000000010002F8BA 837de020 cmpl $0x20,-0x20(%rbp)
000000010002F8BE 7d98 jge 0x10002f858 <AVXCOFFSUM+216>
while Count > 0 do
ちょっとダラダラ書いて訳が判らないかもしれませんが
SSE命令はアライメントを合わせないと例外で落ちるけど
Vの付いた命令は同じ内容の命令(xmmであっても)例外は出ません。
ですからアライメントの心配なく使えます。速度が最速より落ちるだけです。
それでも並列化しないより数倍早くなります。CPUのキャッシュ内なら倍違っても、最終的にはDRAMに読み書きしないといけないから、大量の配列相手だと、それで制限されるんで。
ただCPUの普及状態を考えると FMA+AVX2あたりまでかな
AVX512は私のPCでも使えないし
後半のwhile分の中は
× Dec(Count, 32);
〇 Dec(Count, 1);
でした
裏目小僧さま
よくわかっていないのですが、Delphiの64Bit制約のため以下のように2つの関数にしてみました。
使用しますと以下の■※の個所でACCESS_VIOLATIONエラーになります。
私にはもはや全く分からずです。すいません。
procedure CoffSum(pD,pS:PSingle;MulData:TMulSingle);
asm
LEA RCX, MulData[0]
//VBROADCASTSS ymm5, [RCX] //AVX 256bitレジスタに32bit値を埋める
db $c4,$e2,$7d,$18,$29
MOV RAX, pS // ソースアドレスをRAXに
MOV RDX, pD //加算してゆく先
//VMOVUPS YMM0, [RDX] //AVX
db $c5,$fc,$10,$02
//VMOVUPS YMM1, [RDX+32]
db $c5,$fc,$10,$4a,$20
//VMOVUPS YMM2, [RDX+64]
db $c5,$fc,$10,$52,$40
//VMOVUPS YMM3, [RDX+96]
db $c5,$fc,$10,$5a,$60
//VFMADD231PS YMM0, YMM5, [RAX] //FMA YMM0+=YMM5*[RAX]
db $c4,$e2,$55,$b8,$00
//VFMADD231PS YMM1, YMM5, [RAX+32]
db $c4,$e2,$55,$b8,$48,$20
//VFMADD231PS YMM2, YMM5, [RAX+64]
db $c4,$e2,$55,$b8,$50,$40
//VFMADD231PS YMM3, YMM5, [RAX+96]
db $c4,$e2,$55,$b8,$58,$60
//VMOVUPS [RDX], ymm0
db $c5,$fc,$11,$02 ─────────────■※ここでエラーになります
//VMOVUPS [RDX+32], ymm1
db $c5,$fc,$11,$4a,$20
//VMOVUPS [RDX+64], ymm2
db $c5,$fc,$11,$52,$40
//VMOVUPS [RDX+96], ymm3
db $c5,$fc,$11,$5a,$60
end;
// for i:=0 to Count-1 do arD[i]:=arD[i] + coff*arS[i];
procedure AVXcoffSum(var arD, arS: single; coff: single; Count: integer);
var
//single32bitを 1ループで32個処理する
MulData: TMulSingle;
pD, pS: PSingle;
begin
pD := @arD;
pS := @arS;
MulData[0] := coff;//値渡しなのでローカル変数にcopy
while Count >= 32 do
begin
CoffSum(pD,pS,MulData);
Inc(pD, 32);
Inc(pS, 32);
Dec(Count, 32);
end;
while Count > 0 do
begin
pD^ := pD^ + MulData[0] * pS^;
Inc(pD);
Inc(pS);
Dec(Count, 1);
end;
end;
asmをいちなり書くと、アセンブラ関数になります。 その場合、ローカル変数が定義出来ないのでbeginの後にasmを書くインラインアセンブラにした方が良いでしょう。
もう一つの注意は壊してはいけないレジスタは保存しなければいけない事です。
Lazarusの場合は asm end 時にasm内で使用したレジスタを報告するので勝手にやってくれますが、Delphiはそうではなかったように記憶しています。
https://docwiki.embarcadero.com/RADStudio/Alexandria/ja/%E3%82%A4%E3%83%B3%E3%83%A9%E3%82%A4%E3%83%B3_%E3%82%A2%E3%82%BB%E3%83%B3%E3%83%96%E3%83%AA_%E3%82%B3%E3%83%BC%E3%83%89%E3%81%AE%E4%BD%BF%E7%94%A8
壊してはいけないレジスタに XMM5 があるので ymm5を使っているので XMM5を保存しなければいけません。
https://docwiki.embarcadero.com/RADStudio/Alexandria/ja/%E3%82%A2%E3%82%BB%E3%83%B3%E3%83%96%E3%83%AA%E3%81%AE%E6%89%8B%E7%B6%9A%E3%81%8D%E3%81%A8%E9%96%A2%E6%95%B0
.SAVENV XMM5 が先頭に必要になるのでしょう
一度 ブレークポイントで止めて CPU迄で配置されたアセンブラコードを見るべきです。
例外で止まってしまう原因ですが、アセンブラ関数としていきなり引数を渡している点になると思われます。
上のリンク先にもありますが 64bitの引数は RCX、RDX、R8、 R9 という並びになります。
https://learn.microsoft.com/ja-jp/cpp/build/x64-calling-convention
よって
procedure CoffSum(pD,pS:PSingle;MulData:TMulSingle);
RCX pD,
RDX pS
R8 MulData
となっているはずです。 TMulSingleが配列なら MulDataは配列へのポインタです
よって 最初のアセンブラ行
LEA RCX, MulData[0]
により pDの値が上書きされていると思われます。
ブレークポイントで止めてCPU窓でかくにんしてください
MOV RDX, pD //加算してゆく先
が
MOV RDX, RCX
となっていませんか?
その場合、 RDXは RCXつまりMulDataに化けていて MulDataはポインタなので呼び出し元のMulData配列に書き込んでいるのしょう。MulDataが arra[0..31] of doubleでないなら、エラーになると思います。
対策は、私のコードのように、いちどローカル変数を定義して、そこに引数をcopyしてやるかprocedure の引数の順番を変えてやる事です。
元のようにループ内をインラインアセンブラにした方が良さそうに思えます。
その場合はxmm5が保存されているかCPU窓で確認してください。
保存されていないならarray [0..3]of singleの領域に xmm5を保存し復元して下さい VMOVUPS は無くても MOVUPSは使える筈(64bitなら)
なお、
MulData: array [0..7] of single;
としたのは VMOVUPS でcoffを渡すために 8個同じ値を入れようと思ったからで VBROADCASTSS という便利命令を使ったので 配列にする必要は後から考えると無かったのです。
あ、xmm5 を保存するために MOVUPSを使う場合、領域は16byteアラインされていないといけません。
ローカル変数の宣言順でアラインが崩れるとVMOVUPSと違って例外が出ます。
そのためにwin64ではスタックフレームや動的確保でも16byteアラインを強制されますか
例外が出たら他の変数宣言を1個だけ入れ替えるか
array [0..7] of single; で32byte分確保して
p:Integerに代入して p:=(p+8) and -16;として pをRAXとかに代入してやれば16byteアラインされます
お勧めは例外が出たら入れ替える方針です。一度順番が決まってしまえばOKですからね
いけない。MOVUPS はアライメントが崩れても例外は出ない MOVDQUもそうだった。
アライメントが必要なのは movDQA
つまりMOVUPSなら保存時にアラインは考えなくていい。
それから保存なら浮動小数点のMOVUPSより 整数のMOVDQUが安全
スタックへの保存や復元なら
sub rsp,16
movdqu [rsp],xmm5
とループ外で保存してから
ループ内コードを書いて
復元は
movdqu xmm5,[rsp]
add rsp,16
の方がいいかな。ローカル変数定義しなくていいし
裏目小僧 様
Delphiの64ビットは制約があり、
procedure hoge();
begin
asm
end;
end;
は記述できません。
procedure hoge();
asm
end;
のみになります。
32ビットなら可能なのですが・・・。
なるほど。それは面倒ですね。
なら、とりあえずAVX命令は諦めてSSE命令で並列化してみては?
こんな感じです。
2つのアセンブラ関数は関数内関数にするとレジスタの先頭に隠れたポインタが渡されるので関数の外にある必要があります。
これで高速化するなら AVXでさらに高速化出来るぞと.
2つにしたのは callのオーバーヘッドが大きいので4並列を3つ関数内で書いて12回分としてcall回数を減らそうという事です。
さらに長く続けても速くなるなら問題ないですよ。
procedure coffSum4(var pD, pS, coff4: single);
asm // RCX RDX
MOV RAX,coff4
MOVUPS XMM0, [RAX] // coffは
MOVUPS XMM1, [RDX] //4word一度に処理する
MOVUPS XMM2, [RCX] //4word一度に処理する
MULPS XMM1, XMM0 //coff*pS^
ADDPS XMM2, XMM1
MOVUPS [RCX],XMM2
end;
procedure coffSum12(var pD, pS, coff4: single);
asm // RCX RDX R8
MOV RAX,coff4
MOVUPS XMM0, [RAX] // coffは
MOVUPS XMM1, [RDX] //4word一度に処理する
MOVUPS XMM2, [RCX] //4word一度に処理する
MULPS XMM1, XMM0 //coff*pS^
ADDPS XMM2, XMM1
MOVUPS [RCX],XMM2
MOVUPS XMM1, [RDX+16] //4word一度に処理する
MOVUPS XMM2, [RCX+16] //4word一度に処理する
MULPS XMM1, XMM0 //coff*pS^
ADDPS XMM2, XMM1
MOVUPS [RCX+16],XMM2
MOVUPS XMM1, [RDX+32] //4word一度に処理する
MOVUPS XMM2, [RCX+32] //4word一度に処理する
MULPS XMM1, XMM0 //coff*pS^
ADDPS XMM2, XMM1
MOVUPS [RCX+32],XMM2
end;
// RCX RDX xmm2 R9
procedure AVXcoffSum(var arD, arS: single; const coff: single; Count: integer);
var Coff4: array[0..3] of single; //SSE命令の為の4word(16byte)の入れ物
pD, pS: ^single;
i: integer;
begin
pD := @arD;//レジスタで
pS := @arS;
for i := 0 to High(Coff4) do coff4[i] := coff; // shufps にしてくれるかな
while Count >= 12 do //SSE命令はWin64なら必須だから4個一度に計算可能
begin
coffSum12(pD^, pS^,coff4[0]);
Inc(pD, 12);
Inc(pS, 12);
Dec(Count, 12);
end;
while Count >= 4 do //SSE命令はWin64なら必須だから4個一度に計算可能
begin
coffSum4(pD^, pS^,coff4[0]);
Inc(pD, 4);
Inc(pS, 4);
Dec(Count, 4);
end;
while Count > 0 do //残りは最大3個だから
begin
pD^ := pD^ + coff * pS^;
Inc(pD);
Inc(pS);
Dec(Count, 1);
end;
end;
SSE並列化命令を使っても思った程早くならない場合次はキャッシュを意識したコードにしなければいけません。
たとえばFFTのような処理なら1層づつ回転変換を処理してゆくと、キャッシュサイズを超えるととたんに遅くなります
その場合、キャッシュ内のデータが書きだされる前にそれを使う上層に使わせるという工夫が必要になります。
もちろんFFTはブロック同士が交差しますから上の層は下の層の半分しか処理できませんが
私のpcのキャッシュは3Mバイトですから 単精度なら複素数30万個あたりで分割する必要が出てきますね。
キャッシュサイズもCPUIDで調べる事が出来るので、高速化を考えるならそれを取得してやるという事になるでしょう。
その次の壁はメモリーサイズです。PCに内臓してるDRAMの範囲を超えると、これはもうどうしようもありません。
仮想記憶でHDDが使われるようになると、それがネックとなりSSEとかAVXとか無意味になってきます。
まあデータを8BITにしてバイトで保持してやれば1/4にはなります。
大昔、8BITで対数的に保持して掛け算 足し算はテーブル引きで処理した事はありますが
まあそれでも、そこそこの結果が得られたように覚えています。
当時は掛け算さえ遅かった時代なので 速度面で使った手法であり。今では無意味ですが
メモリ面では見直される手法かもしれませんね。
計算速度よりもメモリ消費量が速度を決める領域なれば有効でしょう。
学習さえ出来ればokで計算精度とか無意味な領域のように思えるので
裏目小僧様
for i:=0 to Count-1 do arD[i]:=arD[i] + coff*arS[i];
の処理を行っていて、繰り返しが多くて簡単に置き換え可能な箇所はBackProp関数(逆伝播)に2か所あるので置き換えてみました。
(AMD Ryzen3 3200G 3.60GHz 4C L1キャッシュ384KB L2キャッシュ2MB L3キャッシュ4MB、RAM:24.0GB)
排他論理和のNN学習で1万回の繰り返し学習、
入力層2ニューロン、中間層128ニューロン、中間層32ニューロン、出力層1として学習個所だけをTStopWatchで処理時間を測って比較してみました。
アセンブラに置き換えない(64ビットコンパイル)の場合の処理時間は2.821秒でした。
アセンブラに置き換えた(64ビットコンパイル)場合は2.218秒でした。
入力層2ニューロン、中間層128ニューロン、中間層64ニューロン、出力層1の場合、
アセンブラに置き換えない(64ビットコンパイル)の場合の処理時間は5.121秒でした。
アセンブラに置き換えた(64ビットコンパイル)場合は4.045秒でした。
アセンブラに置き換えたほうが20%程度速くなっています。
お疲れ様でした。
win64はSSE命令で浮動小数点を扱う(といっても並列ではなく単項で)ので、並列化すれば計算部だけは並列具合だけ高速になる筈。つまり
積和部分だけに限れば4倍高速になる筈なので
A もともとDelphiがある程度並列演算をしていた(Lazarusはしてくれない)
B 置き換えられる部分が全体から見て少なかった
C キャッシュから外れる程メモリを食っていた
という感じでしょうか
2割程度の改善だと さらに FMA+AVXを使っても大きな改善は望めませんね。
積和以外の部分で並列化可能な部分を探すしかないでしょう。
https://urame.sakura.ne.jp/w/wiki.cgi/lazarus?page=Lazarus%A4%C7RDTSC%A4%F2%BB%C8%A4%A6}
私の方でもfor文 AVX SSEで処理時間の差を見てみました.256個のテストです
結果は結構バラツクのですが
こんな感じです。 AVX命令が方が遅いのかと思いましたが
no, cyc,全体の,st回数,ed回数,ed/st ,"name"
0, 2258, 51.09, 1, 1,100.00,"for文による場合"
1, 844, 19.10, 1, 1,100.00,"AVXによる場合"
2, 620, 14.03, 1, 1,100.00,"SSEによる場合"
, 4420cyc=0.00ms このCPUは1msで2326315.8cyc実行
試験する順番を入れ替えると
no, cyc,全体の,st回数,ed回数,ed/st ,"name"
0, 2556, 55.52, 1, 1,100.00,"for文による場合"
1, 366, 7.95, 1, 1,100.00,"AVXによる場合"
2, 864, 18.77, 1, 1,100.00,"SSEによる場合"
, 4604cyc=0.00ms このCPUは1msで2423157.9cyc実行
命令キャッシュも影響してるのかもしれませんね
裏目小僧様
掲示して頂いたアセンブラで、他に適用出来そうな部分を探して試してみたのですが、速度はあまり変わらなかった(そもそもループ内で多く使われてないところなので当然なのですが)です。オンライン処理からミニバッチ処理にして並列化しかなさそうな気がしています。でもミニバッチ処理だと使い勝手が少し悪くなるのと、メモリー消費が大きくなるので、これで妥協します。
Delphiの64ビットアセンブラの制限も痛いです…(日本語のフォルダを使うとCPUウィンドウが表示出来ない、プロシージャ内にdelphiとアセンブラの両方を記述できない、XE11以降しかAVX命令に対応していないためハンドアセンブルでdbで機械語記述)
色々とありがとうございます。
裏目小僧様
とてもお世話になっていましてありがとうございます。
掲載頂いたアセンブラsseで、25%、場合によっては35%速くなりますので、私のホームページにソースコードを掲載させて頂いても宜しいでしょうか。
厚かましいお願いですいません。
ソースの公開、問題ありませんよ。 誰でも使って下さいというスタンスですから。
あれから私の方でもDelphi5で使ってみたくて SSE/AVX命令をDB分にして動かせるようにしました。 Win64でのDelphiでのテストは出来ていませんが、Lazarus64では動いています。
ここを見た人で欲しい人が出るかもしれないので、リンクを掲載しますね。
https://urame.sakura.ne.jp/w/wiki.cgi/lazarus?page=Lazarus%A4%C7%BA%EE%A4%C3%A4%BFSIMD%B4%D8%BF%F4%A4%F2Delphi%A4%C7%BB%C8%A4%A6
このページの AVX_func_delphi.inc にあります
裏目小僧様
いろいろとありがとうございました。
ツイート | ![]() |