sort
goshが便利になった件
全くメインのタイトルと関係ない亊を載せているんで、某からは嫌われていると思うんだけど、許せ。犬も歩けば棒にあたるを期待してるからね。いわゆるびっくり箱。何が出て来るか、読んでみるまでわからないを、あえて期待してるんで。
で、今回はちょいと出遅れたけど、gaucheが新しくなった。
vbox$ gosh -V Gauche scheme shell, version 0.9.12 [utf-8,pthreads], i386-unknown-openbsd7.1 (version "0.9.12") : vbox$ gosh gosh$ open-TAB open-coding-aware-port open-output-buffered-port open-input-buffered-port open-output-fd-port open-input-fd-port open-output-file open-input-file open-output-string open-input-string
何が嬉しいって、REPLで補完が效くようになった亊。長年待ち望んでいたので、嬉しいぞ。 内部的には、GCが新しくなったという亊で、OpenBSDでは門前払いになるかと思ったら、何とか上記のように動いている。
但し、OpenBSDの持病により、テストでハングアップする。ここには触れないように注意だな。
gmake[2]: Entering directory '/tmp/Gauche-0.9.12/ext/threads' GAUCHE_TEST_RECORD_FILE="../../test.record" "../../src/gosh" -ftest -I"." -I. "./test.scm" > test.log Testing threads ...
debianでは、問題なし。FreeBSDでも問題はない。
make[1]: Leaving directory '/tmp/Gauche-0.9.12/src' Total: 35004 tests, 35004 passed, 0 failed, 0 aborted.
最新のやつを提供してると自慢のArch Linuxには、やっと来た。1月以上の遅れだ。
[sakae@arch tmp]$ pacman -Qi gauche : Build Date : Thu 04 Aug 2022 09:01:31 PM JST Install Date : Fri 05 Aug 2022 03:19:51 PM JST
sort -R
前回、ファイルのシャッフルに直交問題を解いた。もう少し深入りして、どう実現されてるかみておく。 それって、ソースを眺める亊だ。こういう用途にはOpenBSDが最適。ソースはいつでも見られるし、綺麗に整理されてるし、もう呼吸をするがごとく自然な亊だ。その点、あのOSは苦労するぞ。これから読むぞと、気合を入れて所在を確認、取寄せ。まるで、スキューバダイビングするみたいに、準備万端整えなきゃいけないからね。以後、特に断りがない限り、ソースの出所はOpenBSDって亊にしとく。なお、manもソースの一部だからね。
vbox$ cp -r /usr/src/usr.bin/sort /tmp vbox$ cd /tmp/sort/ vbox$ ls Makefile coll.c file.h radixsort.c sort.c vsort.h bwstring.c coll.h mem.c radixsort.h sort.h bwstring.h file.c mem.h sort.1 vsort.c
格納場所で見てもいいんだけど、手を加える可能性があるんで、/tmpにコピーしてる。必要なら、debug用に手軽にコンパイルも出来るしね。
お約束通り、sort.hを閲覧。
/* * MD5 context for random hash function */ extern MD5_CTX md5_ctx;
もう、ヒントが出てきた。ハッシュにはMD5を使ってるとな。sortは Version 1 AT&T UNIXで現れたそうなので、その時代には、sha256なんて、無かったんだな。現代になっても、昔のまま。まあ、厳密に考える亊はないよって亊なんだろうね。
後は md5_ctx
あたりをキーにして、使ってる所を探せばいいんだな。
sort.c
/* * Set random seed */ static void set_random_seed(void) : unsigned char rsd[1024]; arc4random_buf(rsd, sizeof(rsd)); MD5Update(&md5_ctx, rsd, sizeof(rsd));
ふむ、ここでイニシャライズしてるのね。種は、–random-sourceで指示がある場合はそれを使うし、指示がなければ、OpenBSDの自慢の種 arc4ramdomを使うとな。
次は、いよいよ本番。coll.c
/* * Implements random sort (-R). */ static int randomcoll(struct key_value *kv1, struct key_value *kv2, size_t offset __unused)
この関数の中で、核となると思われる関数が呼び出されている。自由に飛び廻りたいので、覚えたばかりの、cscopeとctagsを使って、飛び回る準備をする。
vbox$ cscope-indexer vbox$ cat cscope.files | xargs ctags Duplicate entry in file coll.c, line 613: LSCDEF Second entry ignored vbox$ vim :cs add cscope.out
vimでq: と言う便利なコマンドを知った。コマンドヒストリーが出て来て、自由に選んだコマンドを編集して、再度実行出来る。
で、cscopeとかを使って追っていくのは、木を見て森を見ずになっていないか? 樹海に埋もれて方向違いの所を見てしまって遭難って亊も考えられる。ここはきちんと、道筋を確認しておく必要があるな。そう、gdbしておけ。
Makefileに下記の一行を追加して、ガイドを雇えばよい。
PROG= sort CFLAGS= -g -O0 ## <== add this line SRCS= bwstring.c coll.c file.c mem.c radixsort.c sort.c vsort.c .include <bsd.prog.mk>
そしてコンパイル。後はBPを張って、実行するだけ。ああ、その前に試運転しておいた方がいいかな。
vbox$ cat z.txt AAA BBBB CCCCC DDDDDD EEEEEEE vbox$ sort -c z.txt vbox$ sort -R z.txt CCCCC AAA DDDDDD EEEEEEE BBBB
sort -c は、ちゃんとsortされたファイルか確認するコマンドだった。sort(1)して、初めて知ったと言うのは、内緒だ。ついでに -c の検証って亊で、BBBBの後ろに111を追加して実行。
vbox$ sort -c z.txt sort: z.txt:2: disorder: 111
なる程、2行目の次に番狂わせな111てデータが来ていますよ、て理解するのね。-cの同類で-Cってのもある。こちらは、番狂わせがあると、sortが失敗って亊で$?が1になった。
vbox$ gdb -q ./sort Reading symbols from ./sort... (gdb) b randomcoll Breakpoint 1 at 0x7cdf: file coll.c, line 955. (gdb) r -R z.txt Breakpoint 1, randomcoll (kv1=0x499c07f4, kv2=0x49993e34, offset=0) at coll.c:955 955 s1 = kv1->k; (gdb) bt #0 randomcoll (kv1=0x499c07f4, kv2=0x49993e34, offset=0) at coll.c:955 #1 0x15fc82db in key_coll (ps1=0x499c07f4, ps2=0x49993e34, offset=0) at coll.c:471 #2 0x15fc869b in list_coll_offset (ss1=0x4b0f300c, ss2=0x4b0f3010, offset=0) at coll.c:549 #3 0x15fc8829 in list_coll (ss1=0x4b0f300c, ss2=0x4b0f3010) at coll.c:577 #4 0x0ae60d6c in _libc_heapsort (vbase=0x4b0f3000, nmemb=5, size=4, compar=0x15fc87f0 <list_coll>) at /usr/src/lib/libc/stdlib/heapsort.c:159 #5 0x15fcc438 in sort_list_to_file (list=0xcf7d6e90, outfile=0x15fc1655 "-") at file.c:1124 #6 0x15fcf58e in main (argc=0, argv=0xcf7d6f90) at sort.c:1110
これで、地図が入手できた。この地図をファイルに落しておいて、後はvimで開いて、観光開始。好きな関数の所で、tagするなり、cscopeすればよい。cscopeで困った時は、:cs helpだ。
また、gdbを走らせる時、r -R z.txt > /dev/null とかやると結果が闇に葬られるので、余計な奴が出てこなくなる。sort特有な機能として出力先を指定出来るので、r -R -o z.txt z.txtとするのも有りだ。
整列のアルゴリズム って亊で、ヒープソートの説明が出てた。 その前に、manと言う手も有るな。
SYNOPSIS #include <stdlib.h> void qsort(void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *)); int heapsort(void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *)); int mergesort(void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *));
最後の引数は、2つのオブジェクトの大小関係を判定する関数。大小によって、正、ゼロ、負を返すという亊になってる。coll.cには、各種の判定関数が集められていた。
static int wstrcoll(struct key_value *kv1, struct key_value *kv2, size_t offset); static int gnumcoll(struct key_value*, struct key_value *, size_t offset); static int monthcoll(struct key_value*, struct key_value *, size_t offset); static int numcoll(struct key_value*, struct key_value *, size_t offset); static int hnumcoll(struct key_value*, struct key_value *, size_t offset); static int randomcoll(struct key_value*, struct key_value *, size_t offset); static int versioncoll(struct key_value*, struct key_value *, size_t offset);
純粋な数値としての判定、英語流の月による判定(MayはAugより前とか)、単位を含んだ数値での判定とか、バリエーションが豊富だ。
最初、ランダムにさせるのにハッシュって、どう実現するのって色々想像したけど、決定打を見出せず。どう実現してるか、調べた次第。
これらの関数に、比較したいオブジェクトを渡し、後は自由に比較して、大小関係を返すだけってのが、最大の味噌なんですな。月の比較なら、MayとAugは、どちらが大きいって判定するだけ。random化なら、"AAA","BBB"の比較を、md5("AAA")とmd5("BBB")の比較に置き換えてしまえばよい。
戦わずして勝つ
ソース解析の極意として、深入りするなってのがあるそうです。今回のsortには、–debugオプションなんてのが用意されてるので、ちょいと使ってみる。
vbox$ sort --debug -R z.txt Memory to be used for sorting: 1073451008 sort_method=heapsort ; k1=<DDDDDD>, k2=<EEEEEEE>; s1=<DDDDDD>, s2=<EEEEEEE>; cmp1=5 ; k1=<DDDDDD>, k2=<BBBB>; s1=<DDDDDD>, s2=<BBBB>; cmp1=1 ; k1=<DDDDDD>, k2=<CCCCC>; s1=<DDDDDD>, s2=<CCCCC>; cmp1=2 ; k1=<DDDDDD>, k2=<AAA>; s1=<DDDDDD>, s2=<AAA>; cmp1=-43 ; k1=<DDDDDD>, k2=<CCCCC>; s1=<DDDDDD>, s2=<CCCCC>; cmp1=2 ; k1=<EEEEEEE>, k2=<BBBB>; s1=<EEEEEEE>, s2=<BBBB>; cmp1=-4 ; k1=<BBBB>, k2=<CCCCC>; s1=<BBBB>, s2=<CCCCC>; cmp1=1 ; k1=<EEEEEEE>, k2=<BBBB>; s1=<EEEEEEE>, s2=<BBBB>; cmp1=-4 ; k1=<CCCCC>, k2=<EEEEEEE>; s1=<CCCCC>, s2=<EEEEEEE>; cmp1=3 EEEEEEE CCCCC :
比較に使われている部分と、その結果が出てきましたねぇ。
vbox$ sort --debug z.txt Memory to be used for sorting: 1073451008 sort_method=radixsort AAA :
今度は普通に実行。ソート方法が変った。そして、比較級の結果が出てこない。って、比較していないの? それでソートが出来るの? その前に、勝手にソートアルゴリズムを選んでくれるsortアプリって、AIみたいだなって言ったら、大袈裟過ぎるか。次から次に疑問が出てきて、興味は尽きないな。
ちょいと息抜きしてたら、ソースコード探険隊 なんていうかっこいいサイトに遭遇した。 オイラーの注目は、ナンバーズ予想で学ぶ統計学 という所です。
sort at linux
sort -R をやった時、比較関数内でmd5を使っている亊が分った。それって、ものすごい負荷にならないか? とんでもない回数md5されるはずだから。
だから、多分リナでは専用コマンドを用意したんだろうね。そんな仮説を検証してみるって亊で、debian(64Bit)に舞台を移して実験する。
sakae@pen:/tmp$ time sort TAGS > /dev/null real 0m6.492s user 0m19.573s sys 0m0.409s sakae@pen:/tmp$ time sort -R TAGS > /dev/null real 1m54.062s user 5m49.654s sys 0m0.260s
普通なソートとランダム化の比較。やっぱり、とんでもなく遅い。
sakae@pen:/tmp$ time shuf TAGS > /dev/null real 0m0.511s user 0m0.454s sys 0m0.057s
今度は、専用コマンド。専用っていうぐらいだから、普通のsortに比べても爆速だ。この秘密は? ソースお取寄せだな。
まて、その前にsort –debug してみるか。
sakae@pen:/tmp$ sort --debug -R z.txt sort: text ordering performed using ‘en_US.UTF-8’ sorting rules EEEEEEE _______ :
OpenBSDと違って、役立たずの情報は、某Windowsのごとしだなあ!
潜行準備
心して、用意をせいだな。
sakae@pen:/tmp$ mkdir t; cd t sakae@pen:/tmp/t$ apt-file search bin/shuf bedtools: /usr/bin/shuffleBed biosquid: /usr/bin/shuffle coreutils: /usr/bin/shuf emboss: /usr/bin/shuffleseq idba-extra: /usr/bin/shuffle_reads
shufがどのパッケージに入っているか調べる。そしてお取寄せ。ね、面倒でしょ。
sakae@pen:/tmp/t$ apt source coreutils Reading package lists... Done Need to get 5,584 kB of source archives. Get:1 http://ftp.riken.jp/Linux/debian/debian bullseye/main coreutils 8.32-4 (dsc) [2,096 B] : dpkg-source: info: applying 99_kfbsd_fstat_patch.patch dpkg-source: info: applying restore-ls-behavior-8.31.patch
付属品と言うか、デブリ(ごみ)が有るので、専用空間をあらかじめ用意して、その中で作業を実施したのさ。後始末が簡単だからね。で、めざすやつは、dirになってる。
sakae@pen:/tmp/t$ ls -F coreutils-8.32/ coreutils_8.32.orig.tar.xz coreutils_8.32-4.debian.tar.xz coreutils_8.32.orig.tar.xz.asc coreutils_8.32-4.dsc
とんでもなく、色々なコマンドが詰っているぞ。sortも一本のソースになってた。一本にまとめられてしまうと、解析が大変、ってか、見るものじゃないぞって暗黙のうちに告げられている。
後は普通に、configure; make すればよい。 CFLAGS= -g -O2 でコンパイルされるんで、普通にgdb出来る。ここは、後々の亊を考えて、-O0にしておくといいな。
shuf
srcの中では窮屈なので、出来たコマンドを/tmpの下にコピーした。そして実行。
sakae@pen:/tmp$ gdb ./shuf : (gdb) b main Breakpoint 1 at 0x24e0: file src/shuf.c, line 397. (gdb) r z.txt Starting program: /tmp/shuf z.txt : (gdb) 562 permutation = randperm_new (randint_source, ahead_lines, n_lines); (gdb) 564 if (outfile && ! freopen (outfile, "w", stdout)) (gdb) 586 i = write_permuted_output_reservoir (n_lines, reservoir, permuta tion); (gdb) 587 else if (input_range) (gdb) 591 i = write_permuted_lines (ahead_lines, line, permutation); (gdb) AAA DDDDDD :
このあたりなんですかね。それはそうと、同じ亊をDebian(32Bit)で実行しようとすると、コンパイルが失敗する。もう、32Bit版は、野晒しの刑になってるのか。いえね、ずっと熱いものだから、32Bit機をクーラーのある所で使ってたんで、こんな亊が発覚したのさ。
ctagsはsortするか?
ctagsって、成果物がソートされてる。それって、どんな仕組みになってるの? sort繋がりで、調べてみる。 まずは、文中にsortなんていう語句が表れるかの予備調査。研究を始めるには、背景をしっかり把握しておくに越した亊はありませんから。
ob$ grep sort *.[ch] ctags.c:NODE *head; /* head of the sorted binary tree */ ctags.h:typedef struct nd_st { /* sorting structure */ ctags.h:extern NODE *head; /* head of the sorted binary tree */ tree.c: warnx("too many entries to sort");
はあ、出てきましたねぇ。次は軽くgdbで核心ぽい所を炙り出し。(nextして出て来るトレース情報を編集した。)
(gdb) 131 for (exit_val = step = 0; step < argc; ++step) 132 if (!(inf = fopen(argv[step], "r"))) { 137 curfile = argv[step]; 138 find_entries(argv[step]); 139 (void)fclose(inf); 132 if (!(inf = fopen(argv[step], "r"))) { 131 for (exit_val = step = 0; step < argc; ++step) 142 if (head) { 143 if (xflag) 146 if (!(outf = fopen(outfile, aflag ? "a" : "w"))) 148 put_entries(head); 149 (void)fclose(outf); 151 } (gdb) 152 exit(exit_val);
ヘッダーにある、この情報から、二分(探索)木って分る。子供をsonsと称しているけど、それでいいのか?男女別け隔てなく使える、siblingが宜しい表現と思われるぞ。
typedef struct nd_st { /* sorting structure */ struct nd_st *left, *right; /* left and right sons */ char *entry, /* function or type name */ *file, /* file name */ *pat; /* search pattern */ int lno; /* for -x option */ bool been_warned; /* set if noticed dup */ bool dynfile; /* set if file will need freed */ } NODE;
NODEへの登録をやってる、 add_node
にBPをおいて調べる。
#0 add_node (node=0x451ea640, cur_node=0x451eba60) at tree.c:117 #1 0x18e5eb05 in pfnote (name=0xcf7c6c5e "begtoken", ln=48) at tree.c:88 #2 0x18e5bb28 in hash_entry () at C.c:418 #3 0x18e5ac21 in c_entries () at C.c:130 #4 0x18e5d5b1 in find_entries (file=0xcf7c7048 "ctags.h") at ctags.c:239 #5 0x18e5cc81 in main (argc=1, argv=0xcf7c6f78) at ctags.c:138
核心部分は、
static void add_node(NODE *node, NODE *cur_node) { int dif; B dif = strcmp(node->entry, cur_node->entry); : else if (dif < 0) if (cur_node->left) add_node(node, cur_node->left); else cur_node->left = node; => else if (cur_node->right) add_node(node, cur_node->right);
登録したいやつと、現在のやつを比べる。その正負によって、左側にするか右側にぶら下げるか決定。これで、知らず知らずのうちに、大小関係を満したツリーが出来上がる。
(gdb) p cur_node $6 = (NODE *) 0x451eba60 (gdb) p *$ $7 = { left = 0x0, right = 0x451d2720, entry = 0x451cd220 "GETC", file = 0xcf7c7048 "ctags.h", pat = 0x451dcf40 "#define\tGETC(op,exp)\t((c = getc(inf)) op (int)exp)", lno = 45, been_warned = 0 '\000', dynfile = 0 '\000' }
今度は、そのツリーの出力、 put_entries
put_entries(NODE *node) { B => if (node->left) put_entries(node->left); : fprintf(outf, "%s\t%s\t%c^%s%c\n", node->entry, node->file, searchar, node->pat, searchar); if (node->right) put_entries(node->right);
このように、通りがけ順にツリーをなぞれば、昇順に、すなわちソートされた結果が得られる。 このなぞりの再帰が美しいな。lisper好みだ。
vi vs emacs for tag
強引に作ったOpenBSDのカーネル用TAGSを開く。
Visit tags table (default TAGS): /usr/src/sys/ File TAGS is large (139.9 MiB), really open? (y)es or (n)o or (l)iterally Starting a new list of tags tables
こんな風にmini-bufferに出てきた。警告だ。この時、emacsのメモリー消費状況はどうなってる? topで監視。
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 1757 sakae 20 0 89272 41824 14060 S 0.0 1.7 0:01.27 emacs 1757 sakae 20 0 233620 186084 14120 S 0.0 7.5 0:03.13 emacs
上は、開く前、下は開いた後。単位は4kかな。RESは実メモリー、VIRTは仮想メモリーだな。いずれも大幅に増加してる。
この確認は、debian(64Bit)環境でやってる。今度はviでやりたいんだけど、今の環境はviと言いつつ実体は、vim-tinyだ。この際だから、vim-tinyを消してnviにしよう。nviを入れたら、ちゃんとviと認識して、リンクが貼られた。
こういうの勝手にやってくれるのは便利なようで不便。注意していないと見過してしまうぞ。BSD系みたいに、手動で設定を促してくれる方が良いと思われる。
sakae@pen:/usr/src/sys$ wc TAGS tags 1485342 4577037 146663406 TAGS 1469952 7048230 153532442 tags
tagsは、ctags -R して作ったもの。TAGSは、元から入っていたもの。2017年に苦労して作ったものだ。tag jump サイズが違うから、新たに作ったのかな。
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 2040 sakae 20 0 8408 4648 4196 S 0.0 0.2 0:00.00 vi 2040 sakae 20 0 8496 5004 4324 S 0.0 0.2 0:00.00 vi
上はtagする前、下はtagした後。メモリーの利用度はほとんど変らない。まあ、viでのtagの利用方法は、mmapしてtagを貼り付け、検索、終ったら廃棄してるんで、幾ら巨大なtagファイルを使っても、メモリー負担は無いんだな。
その点、emacsの方は、しっかりとメモリー展開してるのかな? 詳しくは、xref.elにある (xref-find-definitions IDENTIFIER) を見ておけって亊か。まて、その前にTAGSをロードする作業が有るだろう。
TAGSを普通のファイルとして開いてみたら(C-X C-F)ファイルが大きすぎるけど開く?って聞いてきた。という亊は、emacsが持って生れた特製って亊になるな。
ああ、特製と言えば、nviの場合、 nvi -t sys_open
みたいに、いきなりtag-nameを指定してファイルを開けるぞ。これ、なかなか便利。
そろそろvim指とemacs指が喧嘩を始めたみたいなので、今後のために整理された資料を用意しておく。 vimとemacsのコマンド比較