V言語でアプリ完成
v-mode
前回の終わりに、V語の事で質問とか出来る所無いかなと思った。それはきっと本家のHPに掲載されているだろうってんで、改めて眺めてみた。
今まで気にしてなかったけど、中央にアイコンがづらっと並んでた。一番左のはとのマークのついたーは認識してたけど、どうせそれの類だろうと高を括ってたんだ。でも、よく見るとemacsマークも有るじゃない。
クリックしたら、v-modeがうんたら、かんたらと出てきた。今まで気が付かなかったぞ(そうさ、オイラーの目は節穴さ)。
折角見つけたんだから、入れてみるか。
で、件のご相談コーナーは、git族さんとか、知らないサービスに登録しないと駄目みたいで、敷居が高そう。画して、孤独のdeubgが続くよ。
read(write)_file
前回ちょっと気になっていた、いきなりの読み書き、どうやってるか見ておく。って、単なる観光旅行ですが。知見を広める良い機会ですよ。主舞台は、vlib/osの下。
まずは基本のos.v
// write_file writes `text` data to a file in `path`. pub fn write_file(path string, text string) ? { mut f := create(path) ? f.write_string(text) ? f.close() }
たったこれだけーー。隣には、 write_file_array
なんてのも置いてあった。
read_file
の方は、 os_c.v
の中
unsafe { mut str := malloc(fsize + 1) nelements := int(C.fread(str, fsize, 1, fp)) if nelements == 0 && fsize > 0 { free(str) return error('fread failed') } str[fsize] = 0 return str.vstring_with_len(fsize) }
主要部分はこれ。fseek/ftellを使って、fsizeを得てて、ファイルのサイズ分を確保。それから一気読みと言う。豪快な事をやっている。昔のunixみたいにメモリーが不足してる所でも動くようにって言う配慮は、微塵も感じられない。現代っ子の、富豪プログラミングである。
mallocで確保したエリアは、何時解放されるのだろう? 使えば使いっぱなし(で、メモリーリーク)の後片付けしない現代っ子なのかな。それとも特性mallocで、使用終了を察知して始末するように、執事機能を持ってるのかな? ここだけ見てたんじゃ判断出来ないな。
読書百遍
前回の不可解な挙動を引きずっている。もう基本に帰って、説明書を読み直そう。今までは分かってる積りの流し読みだったからなあ。読書百遍意義自ずから通じって言うじゃない。
配列は大きく分けて2種類ある。長さが後で追加できるやつと、宣言時に長さを固定にしちゃうやつ。可変の方が使い勝手が良いけどスピードが遅くなる(場合が有る)と言うペナルティがある。固定長の場合はそういうのが無い。
便利に使う一番の目的は、追加してくってやつ。オイラーもこれになびいて便利に追加していっている。
固定長の方の説明をみると、昔からあるありきたりの利用方法しか説明されていない。そんなものなのかと、当たり前に思っていたけど、オイラーの使い方は、こうだった。
struct AAy { mut: bld [][4]int // main data type }
サイズ4と言う固定長の配列を、可変長の配列の中へ入れるってやつだ。ひょっとしてこういう混合は許していないの? だから、こういう事例は紹介していない?
多次元配列の例も出てきてるけど、よく見ると、サイズが固定になってる。ここはもう、 忖度してくださいって事かな。
って事で、サイズの4を外してしまった。
struct AAy { pub mut: bld [][]int // main data type }
それに伴い、既存のコードを多少変更した。
これで、ちゃんとデータがam,pmに分離出来て、PDFなグラフが作成出来た。
next error
気をよくしたオイラーは、データの入力系をチェックする事にした。
sakae@pen:/tmp/nbldv$ ./nbldv --ire 2104 2104> 104 135 68 49 V panic: array.set: index out of range (i == 0, a.len == 0) /tmp/v/nbldv.13414080679450532490.tmp.c:6195: at v_panic: Backtrace /tmp/v/nbldv.13414080679450532490.tmp.c:5922: by array_set /tmp/v/nbldv.13414080679450532490.tmp.c:13424: by main__ire /tmp/v/nbldv.13414080679450532490.tmp.c:13662: by main__main /tmp/v/nbldv.13414080679450532490.tmp.c:13781: by main
あえなく撃沈! こうなったら、伝家の宝刀のgdbの出番だな。
sakae@pen:/tmp/nbldv$ v -g nbldv.v sakae@pen:/tmp/nbldv$ gdb -q nbldv Reading symbols from nbldv...done. (gdb) b array_set Breakpoint 1 at 0x41f71b: file /tmp/v/../../../../../../home/sakae/src/v/vlib/builtin/array.v, line 407. (gdb) r --ire 2104 Starting program: /tmp/nbldv/nbldv --ire 2104 [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". 2104> 104 135 68 49 Breakpoint 1, array_set (val=0x7fffffffdc64, i=0, a=0x7fffffffde00) at /tmp/v/../../../../../../home/sakae/src/v/vlib/builtin/array.v:407 407 if i < 0 || i >= a.len {
ふむ、配列アクセスの境界チェックか。インデックス番号は0から、長さ未満である事ってののチェックに引っかかっているな。お決まりのbtもしておく。
(gdb) bt #0 array_set (val=0x7fffffffdc64, i=0, a=0x7fffffffde00) at /tmp/v/../../../../../../home/sakae/src/v/vlib/builtin/array.v:407 #1 0x000000000044d06e in main__ire (ds=0x6b1a60, ym=2104) at /tmp/v/../../../../../../tmp/nbldv/nbldv.v:68 #2 0x000000000044f17e in main__main () at /tmp/v/../../../../../../tmp/nbldv/nbldv.v:242 #3 0x00000000004624a5 in main (___argv=0x7fffffffe4d8, ___argc=3) at /tmp/v/../../../../../../tmp/v/nbldv.14819027952117069156.tmp.c:17048
upして問題のコードを見ると
(gdb) up #1 0x000000000044d06e in main__ire (ds=0x6b1a60, ym=2104) at /tmp/v/../../../../../../tmp/nbldv/nbldv.v:68 68 rs[ymdh] = strconv.atoi(ar[0]) ?
ymdhは0なんで、長さの指定の方で引っかかっているんだな。
(gdb) p rs $1 = {element_size = 4, data = 0x0, len = 0, cap = 0}
コードを遡ってみると
// mut rs := []int{} mut rs := [0, 0, 0, 0]
確かに、宣言だけしてて、実データが入っていなかった。ダミーを入れてみた。
(gdb) p rs $2 = {element_size = 4, data = 0x6d4eb0, len = 4, cap = 4} (gdb) p rs[0] Structure has no component named operator[].
今度は、ちゃんと長さが4になってるな。それはいいんだけど、gdbで一番欲しい値の検査が拒否された。残念だ脳。
gdbと併用する時のオプションとして、-gの他に-cgってのが有る。物は試しとばかり使ってみる。
(gdb) up #1 0x000000000044d094 in main__ire (ds=0x6b1a60, ym=2104) at /tmp/v/nbldv.15363799929195393443.tmp.c:13445 13445 array_set(&rs, _const_main__ymdh, &(int[]) { *(int*)_t322.data }); (gdb) l 13440 if (_t322.state != 0) { /*or block*/ 13441 Option_void _t323; 13442 memcpy(&_t323, &_t322, sizeof(Option)); 13443 return _t323; 13444 } 13445 array_set(&rs, _const_main__ymdh, &(int[]) { *(int*)_t322.data }); 13446 Option_int _t324 = strconv__atoi((*(string*)/*ee elem_typ */array_get(ar, 1))); 13447 if (_t324.state != 0) { /*or block*/ 13448 Option_void _t325; 13449 memcpy(&_t325, &_t324, sizeof(Option));
今度はC語がそのまま見えて、debug対象になった。丹念に追いかけて行ったら、勉強になるな。
last error ?
sakae@pen:/tmp/nbldv$ ./nbldv --ire 2104 2104> 104 136 71 49 2104> 121 129 65 60 Bad seq. 2104> fin sakae@pen:/tmp/nbldv$ tail -3 current.csv 21033104,119,67,48 21033121,115,62,58 21040121,129,65,60
ラスト・エンペラーならぬ、ラスト・エラーであって欲しいんだけど、新なデータを継続して入力すると、前のデータとsort順になっていないと言うエラーだ。
for { // data fill in array rs println('${rs} --- ${ds.bld[ds.len() - 2 ..]}}') if rs[ymdh] <= ds.bld[ds.len() - 1][ymdh] { println('Bad seq.') continue } ds.bld << rs println('${rs} xxx ${ds.bld[ds.len() - 2 ..]}}') }
forの中のコード。新にキー入力したデータがrs配列に用意される。dsの最後のデータと比べて新しくなっていないと、シーケンスエラー。debugの為、if文の比較対象を、表示させてみた。
sakae@pen:/tmp/nbldv$ ./nbldv --ire 2104 2104> 104 140 66 49 [21040104, 140, 66, 49] --- [[21033104, 119, 67, 48], [21033121, 115, 62, 58]]} [21040104, 140, 66, 49] xxx [[21033121, 115, 62, 58], [21040104, 140, 66, 49]]} 2104> 121 128 60 60 [21040121, 128, 60, 60] --- [[21033121, 115, 62, 58], [21040121, 128, 60, 60]]} Bad seq.
3月末までのデータに、4月分を追加。1日の朝の分は正しく登録された。1日の21時のデータを追加しようとしたら、手回しよく既に登録されてる。ってか、1日の朝のデータが消えている。
これは、量子力学で言う、トンネル効果だな。エネルギー障壁を乗り越えて、データが染み出してくると言うアレである。世紀の大発見、最初にこれを発見した人は、にわかに信じられなかっただろうね。オイラーも、こんな現象、信じたくないぞ。落ち着くんだ。少し頭を冷やせ。
新なBug2題
身に覚えの有るBugを2つ挙げておく。
ob$ ./nbldv --ire 2104 2104> ================ V panic ================ module: builtin function: get() message: array.get: index out of range (i == 0, a.len == 0) file: /home/sakae/src/v/vlib/builtin/array.v:224 =========================================
入力待ちになった時、何も入力しないでRETを叩いた。ぱにくってる。終了の印である "fin" は、入っていると、勝手な希望的コードになってるからね。
ob$ ./nbldv --ire 2104 2104> 104 130 0x40 50 ================ V panic ================ module: main function: main() message: strconv.atoi: parsing "0x40": invalid syntax file: ./nbldv.v:243 =========================================
もう一例は、ハッカーがデータを16進数で入力しようとした。まあ、オイラーのパソコンは10キーパッドが付いてるから、わざわざアルファベット混じりの数値を入れようとは、思わないけどね。
入力回り、防衛的コードを書くと、とんでもなく面倒になるぞ。
ob$ ./nbldv --ire r0304 0> ^C
これもflagの処理をサボった結果だな。
野生の勘でBug潰し
トンネル効果(Bug)を潰そうと、一晩寝かしたり、散歩したり、テディベアに話しかけると良い(我が家には、それが無いので、友人に頂いた、お馬様のオグリキャップの置物で代用)とか、民間療法を試みる。
最後は、神様仏様、しまいには、藁にもすがって、野生の勘を発揮
mut rs := [0, 0, 0, 0] for { : rs = [0, 0, 0, 0] // <--- 野生の勘で、追加してみた rs[ymdh] = strconv.atoi(ar[0]) ? : rs[ymdh] += (ym * 10000) println('${rs[ymdh]} --- ${ds.bld[ds.len() - 2..]} $ds.len()') if rs[ymdh] <= ds.bld[ds.len() - 1][ymdh] { :
なんの事は無い、rs配列にデータを充填してく前に、それをZEROクリアーするコードを入れた。
ob$ ./nbldv --ire 2104 2104> 104 130 65 65 21040104 --- [[21033104, 119, 67, 48], [21033121, 115, 62, 58]] 200 21040104 xxx [[21033121, 115, 62, 58], [21040104, 130, 65, 65]] 201 2104> 121 120 60 60 21040121 --- [[21033121, 115, 62, 58], [21040104, 130, 65, 65]] 201 21040121 xxx [[21040104, 130, 65, 65], [21040121, 120, 60, 60]] 202 2104> 205 140 70 50 21040205 --- [[21040104, 130, 65, 65], [21040121, 120, 60, 60]] 202 21040205 xxx [[21040121, 120, 60, 60], [21040205, 140, 70, 50]] 203 2104> fin ob$ tail -5 current.csv 21033104,119,67,48 21033121,115,62,58 21040104,130,65,65 21040121,120,60,60 21040205,140,70,50
そしたら、ちゃんと動いた。動いたのはいいんだけど、理屈が分からん。再発しないかしら? 取り合えずは、色々な環境で正常に動くか、観察だな。
prod
普段は、こんな感じでBug潰しに勤しんでいる。
sakae@pen:/tmp/nbldv$ v -g -show-timings . 33.544 ms SCAN 72.137 ms PARSE 28.894 ms CHECK 61.600 ms C GEN 43.009 ms C tcc.exe sakae@pen:/tmp/nbldv$ ls -l nbldv -rwxr-xr-x 1 sakae sakae 803576 Apr 19 14:52 nbldv
そして、OKとなったら、本番投入用の儀式を行う。
sakae@pen:/tmp/nbldv$ v -prod -show-timings . 33.399 ms SCAN 73.995 ms PARSE 29.214 ms CHECK 64.610 ms C GEN 4063.188 ms C cc sakae@pen:/tmp/nbldv$ ls -l nbldv -rwxr-xr-x 1 sakae sakae 111584 Apr 19 14:57 nbldv
コンパイラーがtccからccに切り替えられて、徹底的に最適化が実施される。その結果、出来上がるバイナリーも緻密化されるぞ。
-cg vs. -g
gdbにかける時、-gなり-cgオプションを与えた。gdbを起動して、それぞれのソースを見ればいいんだけど、それじゃゆっくり見れない。そこで下記のようにして、白昼の元に晒してみる。
ob$ v -g -o g.c nbldv.v ob$ v -cg -o cg.c nbldv.v ob$ wc *.c 13546 53385 484257 cg.c 25188 70847 889909 g.c
随分と差が付いてますなあ。-gでgdbした時、v語が観測出来た。そのしかけが内在されてるのかな? g.cを開いてみる。
#line 85 "../../../../../../tmp/nbldv/nbldv.v" array_push(&ds->bld, _MOV((Array_int[]){ rs })); }
これ、度々問題になった、部分だ。元のコードは、
85 ds.bld << rs
コメントで対応が残されていた。これを頼りに、逆表示してるのだろうね。それにしても、2倍近くの差は出ないだろうと思った。
何の事は無い、使っているvlibのソースもコメント化して残っているんだった。例えば、こんな具合ね。
#line 363 "../../../../../../home/sakae/src/v/vlib/flag/flag.v" Option_string flag__FlagParser_string_opt(flag__FlagParser* fs, string name, by te abbr, string usage) { #line 364 "../../../../../../home/sakae/src/v/vlib/flag/flag.v" string res = _SLIT(""); {
speed
上でやった、本番アプリとdebug用のアプリで、実行スピードにどの程度の差が有るか検証してみる。こういう時は、遅いシステムの出番。debian(32Bit)でのお試しさ。
debian:nbldv$ wc current.csv 53888 53888 1022984 current.csv debian:nbldv$ time ./nbldv real 0m0.293s user 0m0.247s sys 0m0.044s
データ数を有り得ない程水増しした(約70年分)。本番アプリの結果。
debian:nbldv$ v nbldv.v debian:nbldv$ time ./nbldv real 0m0.466s user 0m0.413s sys 0m0.050s
こちらはdebug用アプリだ。
ああ、闇雲にスピードチェックするだけじゃ、中坊だ。技術者(の端くれ)らしく、数字で示さんかい。
debian:nbldv$ v -profile zz.txt nbldv.v debian:nbldv$ ./nbldv
プロファイル用のアプリを作成。そしてそれを実行。計測しながらの実行なので、随分時間がかかるけど、我慢我慢。で、結果は?
debian:nbldv$ less zz.txt 112 0.165ms 1471ns strings__new_builder 84 0.610ms 7266ns strings__Builder_write_b : 107944 158.334ms 1467ns array_get 2 0.003ms 1676ns array_slice 1 0.008ms 7682ns array_clone 539107 3039.552ms 5638ns array_push : 1 19416.661ms 19416660819ns main__csv_read 4 0.006ms 1484ns main__AAy_len 2 12.115ms 6057377ns main__AAy_pp 2 0.227ms 113560ns main__AAy_tl 1 319.149ms 319148794ns main__AAy_am 1 323.765ms 323764741ns main__AAy_pm 1 0.047ms 46513ns main__mksf 1 141.502ms 141501755ns main__exec 1 20213.836ms 20213835697ns main__main
データの味方は下記。時間を消費してるのはデータの読み込み部分。この消費時間は、データ数nに比例する、カッコよく言うと O(n) って事だ。vlib内の関数も計測されるのは好印象だな。
The format is 4 fields, separated by a space, for each v function: a) how many times it was called b) how much *nanoseconds in total* it took c) an average for each function (i.e. (b) / (a) ) d) the function name
まとめ
今回はgoのコードをvのコードに変換すると言う無謀な事をやった。存分にエラーと戯れた感じがするぞ。エラー潰しは、探偵物語と言うかデカ(刑事)になった気分で、とっても楽しい。
そして、冒頭で見つけたemacs用のv-modeが、コードの色付けが華やかで、うきうきする。こうでなくっちゃ。
折角なので、Windows10用にクロスコンパイルしようと思ったが、unix用のosコールを一部で利用してるんで諦めた。
今まで、長々とやってきた成果を下記に置いておきます(サンプルデータ付き、メアドも載せておきますので、改良とか隠れBUGが有ったら、教えてください。)