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が有ったら、教えてください。)

nbldv.tar.gz

etc