hash (2)

自前でgnuplotをコンパイル

FreeBSDでの出来事さ。portsから簡単に入るんだけど、texとか余計な物が大過ぎて辟易。ならば野良ビルドと思って始めたんだ。そしたら、見事にエラーですよ。

gmake[4]: Entering directory '/tmp/gnuplot-5.4.1/src'
c++  -g -O2  -L/usr/local/lib  -o gnuplot alloc.o axis.o breaders.o boundary.o color.o
command.o contour.o datablock.o datafile.o dynarray.o encoding.o eval.o external.o fit.
o gadgets.o getcolor.o graph3d.o graphics.o help.o hidden3d.o history.o internal.o inte
rpol.o jitter.o libcerf.o matrix.o misc.o mouse.o multiplot.o parse.o plot.o plot2d.o p
lot3d.o pm3d.o readline.o save.o scanner.o set.o show.o specfun.o standard.o stats.o st
dfn.o tables.o tabulate.o term.o time.o unset.o util.o util3d.o variable.o version.o vo
xelgrid.o vplot.o     -lreadline  -lncurses  -lz -lgd     -lm
ld: error: undefined symbol: libiconv_open
>>> referenced by encoding.c:234
>>>               encoding.o:(init_special_chars)
 :

ports兄貴を参考に、iconvは必要だぞって忠告を受けて、LDFLAGS=-L/usr/local/libは指定しておいたんだけどね。肝心の LIBS=-liconv を忘れていたみたい。

上記のc++の行を適当なファイルにコピーして、最後の部分に -liconv を追加。そのファイルをshellから実行したら、無事にgnuplotが出来上がった。

世の中のMakefileには、肝心な所を隠してしまう奴がいる(そう、最近ではrubyがそうだ)。裏でこそこそやられてしまうと、こういう原始的な事が出来無い。少しは反省して貰いたいな。

hash of mawk

前回見付けてしまったGNUじゃ無いmawkをお取り寄せ。ハッシュがどうなっているか確認。

/*
 * FNV-1 hash function
 * http://www.isthe.com/chongo/tech/comp/fnv/index.html
 */
unsigned
hash(const char *s)
{
    /* FNV-1 */
    register unsigned h = 2166136261U;

    while (*s) {
        h ^= (UChar) (*s++);
        h *= 16777619U;
    }
    return h;
}

こんな案内と共にコードが使われていた。出典が載ってるのは有り難い(上で示したコードは32Bit用なんて事も解るしね)。作者様への礼儀ってもんだろう。色々なソースを見ていると、決して一人ではどうにもなる訳じゃないって事がヒシヒシと感じられるよ。

URLを辿ってみると、色々な所で使われている事が解る。そして、お遊びっぽいお題として、ハッシュ値がZEROになる、入力を求めよなんてのも出てた。これを進化させていくと、ビットコインへ行き着くんですなあ。

sakae@deb:/tmp/mawk-1.3.4-20200120/examples$ mawk -f primes.awk 1950 2000
1951
1973
1979
1987
1993
1997
1999

折角mawkのソースが有るんで、中身の観光をしていたら、例に面白いやつを発見。指定した範囲内の素数を求めるですって。awkでもここまで出来ますって素敵だな。

octave:1> a=primes(1000);
octave:2> a(100:110)
ans =

   541   547   557   563   569   571   577   587   593   599   601

octaveだと、指定した所まで求めておいて、後はスライスしなさい風。

(%i1) primes(8000,8100);
(%o1) [8009, 8011, 8017, 8039, 8053, 8059, 8069, 8081, 8087, 8089, 8093]

maximaだと、範囲を指定して求めるのか。

ああ、昔の事を思い出したぞ。LLって言う集会が行われていた。Lightweight Language の自慢大会ね。そこでのお題に、指定されたURLからコンテンツをGETして、内容を検索しろってのが有った。

gawkがそれを実現しててびっくりした。そして強い嫌悪感を抱いた。機能肥大症に罹患してるな。良きUNIXの伝統を忘れているよ。BASHも同じだぞ。って事で、今度はbashしてみる。

hash of bash

sakae@deb:/tmp/bash-5.1.8$ hash
hits    command
   1    /usr/bin/grep
   3    /usr/bin/tar
   1    /usr/bin/cp
   3    /usr/bin/find
   2    /usr/bin/gnuplot
   1    /usr/bin/man
   1    /usr/bin/sync
  12    /usr/bin/ls
   4    /usr/bin/curl
   2    /usr/bin/make
   1    /usr/bin/cat
   1    /usr/bin/mkdir
   5    /usr/bin/emacs
   1    /usr/bin/rm

bashは、ユーザーの挙動を監視してるんです。 .bash_history だけかと思ったら、他にもhashなんていう内部コマンドを持ってて、ユーザーの嗜好を把握してましたよ。遣り過ぎ、監視社会は、ここまで来たって、いつかクロ現に取あげられるに違いない。

hashlib.c にコードが置かれていた。その冒頭部分。

/* This is the best 32-bit string hash function I found. It's one of the
   Fowler-Noll-Vo family (FNV-1).

   The magic is in the interesting relationship between the special prime
   16777619 (2^24 + 403) and 2^32 and 2^8. */

#define FNV_OFFSET 2166136261
#define FNV_PRIME 16777619

/* If you want to use 64 bits, use
FNV_OFFSET      14695981039346656037
FNV_PRIME       1099511628211
*/

素晴しいハッシュ関数を見付けだぞ。俺って凄いなって、餓鬼だな。御礼の一言ぐらい述べておけよ。あるいは出典のURLを載せておくとかしろよ。

そうそう、bashはawkの真似をしたくて、bashで連想配列を搭載してます。てんこ盛り過ぎると思うのは、オイラーだけかな。

other hash

他にハッシュ関数はないかと手元の本を漁ってみたよ。そしたら今は亡きユニマガの連載からアルゴリズムを取り出してムックにしたものが出て来た。

int as(const char *s, int n){
  unsigned h =0, g;
  char *p;
  for (p = s; *p ; p++) {
    h = (h << 4) + (*p);
    if (g = h & 0xf0000000) {
      h = h ^ (g >> 24);
      h = h ^ g;
    }
  }
  return h % n;
}

解説によると、1990年に出版されたコンパイラ II 技法 原理 ツールって本からの抜粋。色々なデータを与えてテストした所、良い結果が得られたとの事。そう言われると自分でも確かめてみたくなる。何せ古い時代のものですからね。現代に通用するか?

test hash

前回は、世界の単語を餌にしてハッシュを取り、衝突数の最小値、最大値あたりを調べた。それって、統計的にどうよ。ばらつきとかをさっぱり検証してない、小学生の研究だな。少し大人になれよ。統計言語Rの登場か。まて、そんな大上段なツールを取り出さなくても済むぞ。そう、かの昔、血圧グラフをやった時、gnuplotの統計機能を使ったんだ。

vbox$ ./a.out | cut -f 2 | gnuplot -e 'stats "-"'

a.outは前回の最後にちょろっと作ったやつ。cutを使って値だけ取出、それをgnuplotに渡してあげる。"-" は、標準入力からのデータ取込になる。オイラーこういうバラック方式のプログラミング大好きよ。結果は、

* FILE:
 Records:           100
 Out of range:        0
 Invalid:             0
 Header records:      0
 Blank:               0
 Data Blocks:         1

* COLUMN:
 Mean:            1027.7400
 Std Dev:          817.7511
 Sample StdDev:    821.8708
 Skewness:           1.1329
 Kurtosis:           2.5087
 Avg Dev:          695.1900
 Sum:           102774.0000
 Sum Sq.:       1.72497e+08

 Mean Err.:         81.7751
 Std Dev Err.:      57.8237
 Skewness Err.:      0.2449
 Kurtosis Err.:      0.4899

 Minimum:          291.0000 [ 14]   ;   0%
 Maximum:         3056.0000 [ 71]   ; 100%
 Quartile:         519.0000         ;  25%
 Median:           649.0000         ;  50%
 Quartile:        1498.5000         ;  75%

100個のデータを受取ました(バケット数)。その平均値は、1027.74(衝突の平均)で標準偏差は817.7511でした。データの合計は102774でした。リナの単語悵に登録されてる個数に相当する。

Minimum以下は、株屋さんが大好きなローソクチャート(箱ひげ図)の元データになる。データを昇順に並べた時、それぞれの個数の%における値を表している。Minimum/Maximumの欄にある鍵括弧内の数値は、インデックス番号だ。

データ数の半分は519から1498の範囲に収まっていると読めれば、株に手を出してもOK。保証はしないけど。

これで大分大人っぽくなったけど、株屋さんなら、まだ半人前。Aの銘柄だけに入れ込んでいたら負けるぞ。少なくともB銘柄とも同時比較したい。するべき!

ならば、cutしたデータ一旦ファイルに落とす。もうひとつも別なファイルの落とす。両者を、paste a.txt b.txt >ab.txt とかやって、それを gnuplot -e 'stats "ab.txt"' すれば良い。面倒この上ない。面倒は罪って思うのは(自称)ハッカーの芽生えだ。何とかしたい。

そして、何とかついでに、世界の単語だけじゃなくてもっと現実的なデータも扱いたい。例えば、ソースコードに現れる単語(変数名)とか、マニュアル類とか、格調高くシェークスピアの小説とかね。

これらは、残念ながら一単語/行にはなっていない。まあ、あたりまえだ。1行に複数の単語がスペース区切で並んでいる。この普通の書式の文書を扱かうようにしておきたい。

そんな願いの多く(オーク)は、awkにお任せあれ。これ、寒いオヤジギャクだな。いえ違います。awkをエー・ダブリュー・ケーと読まないでと、作者の方々は言ってます。オークと読んで欲しいと。監督と呼ばないで、ビッグボスって呼んでねって、あの新庄ちゃんが、同じフレーズを使ってたね。

vbox$ cat main.c | awk '{for(i=1; i<=NF; i++) print $i}'
vbox$ man pwd | col -b | awk '{for(i=1; i<=NF; i++) print $i}'

出たな、一行野郎。たったこれだけで、C言語のソース(何でもいいけど)を、全行に渡り、スペース区切で単語に分解してくれます。2番目は、巷に溢れているマニュアルを単語に分解する例です。

何? 分解した中に記号が含まれててうざい。C言語が許す変数(関数)名だけを取り出したい。それをawkでやりたい。重複を除きたいんで連想配列で何とかなりませんか? 何もかもawkに押し付けてはいけません。身動きが出来無くなりますから。そんなのはunixの専門家に任せましょう。

egrep '^[A-Za-z]' で名前とおぼしき奴を抜き出す(ちょっと緩い設定なんで注意の事)。重複除きは、sort | uniq の定番組み合わせとかね。

次は、前回のアプリの改変。問題はパイプから入力文書を受け取る方法。セキュリティー的理由により、getsがOpenBSDではサポートされていないんだ。まことに以て潔い。しょうがなしに、fgetsを流用する。巻末にコードを載せたので参考に。

vbox$ cat /usr/share/dict/words | ./a.out | gnuplot -e 'stat "-"'

 * FILE:
  Records:           211
  Out of range:        0
  Invalid:             0
  Column headers:      0
  Blank:               0
  Data Blocks:         1

 * COLUMNS:
  Mean:            1118.3460           1118.3460
  Std Dev:           34.3403             34.1761
  Sample StdDev:     34.4220             34.2574
  Skewness:          -0.0452             -0.0427
  Kurtosis:           3.1081              2.8379
  Avg Dev:           26.5722             27.2762
  Sum:           235971.0000         235971.0000
  Sum Sq.:       2.64146e+08         2.64144e+08

  Mean Err.:          2.3641              2.3528
  Std Dev Err.:       1.6717              1.6637
  Skewness Err.:      0.1686              0.1686
  Kurtosis Err.:      0.3373              0.3373

  Minimum:         1032.0000 [113]     1030.0000 [ 73]
  Maximum:         1204.0000 [ 17]     1222.0000 [160]
  Quartile:        1099.0000           1095.0000
  Median:          1118.0000           1119.0000
  Quartile:        1139.0000           1141.0000

  Linear Model:       y = -0.04976 x + 1174
  Slope:              -0.04976 +- 0.06875
  Intercept:          1174 +- 76.93
  Correlation:        r = -0.05
  Sum xy:             2.639e+08

gnuplotに2列のデータを渡すと、その列をX、Yとみなして、散布図と解釈してくれる。だから、ちょっと解析データが追加されてたりする。Linier Modelで、回歸直線を示してくれたり、Correlationで、相関が出てきたりしてる。AIとかにかぶれていなければ、普段使いの統計解析としては、まあ十分だろう。

で肝心のハッシュの性能だけど、左側は1990年代の技法の結果。右側がmawkで採用してた自慢のやつだ。バケットサイズが211と比較的緩めだ。両者共、優劣をつけがたい。

今度は、バケットサイズを素人っぽく、100に変更(息をするがごとく、再コンパイルね)

Mean:            2359.7100           2359.7100
Std Dev:          888.8830             50.2443

Minimum:         1421.0000 [ 62]     2243.0000 [ 46]
Maximum:         3932.0000 [ 73]     2513.0000 [ 53]
Quartile:        1703.0000           2326.0000
Median:          2061.0000           2356.5000
Quartile:        3008.0000           2393.5000

1990年式は、バケットサイズに制約がありそう。ならば素数の101にしてみる。

Mean:            2336.3465           2336.3465
Std Dev:           50.5852             42.1930

Minimum:         2220.0000 [ 85]     2244.0000 [ 15]
Maximum:         2486.0000 [ 66]     2457.0000 [ 61]
Quartile:        2301.0000           2302.0000
Median:          2336.0000           2343.0000
Quartile:        2366.0000           2368.0000

以上は、32Bit環境のOpenBSDだ。64Bit環境では、どうなる? 1990年式は通用するのか? 興味ある所だ。結果は一致したよ。

気分転換で、debian(64Bit)で、gccのman を題材に、バケット数は100 って条件。総単語数2万弱の結果。

Mean:             196.9700            196.9700
Std Dev:           57.3635             12.9348

Minimum:          125.0000 [ 19]      167.0000 [ 96]
Maximum:          323.0000 [ 50]      227.0000 [ 70]
Quartile:         157.5000            189.0000
Median:           173.0000            198.0000
Quartile:         235.0000            206.0000

今度はソースが供給されてるって事で、golangで試してみる。むちゃくちゃ単語が長いので、それなりにバッファーサイズを大きくしておく事。小さいとセグフォするぞ。

Mean:            3556.0300           3556.0300
Std Dev:         1565.5974             61.4606
Sum:           355603.0000         355603.0000

Minimum:         1819.0000 [ 31]     3356.0000 [ 58]
Maximum:         6107.0000 [  8]     3697.0000 [ 67]
Quartile:        2213.0000           3515.0000
Median:          3168.5000           3564.5000
Quartile:        4912.0000           3592.5000

測定条件は、下記。って、あの人が好きなパイプ・プログラミングだな。

sakae@pen:/tmp/z$ find /usr/local/go/src/cmd -name '*.go' | xargs cat |\
awk '{for(i=1; i<=NF; i++) print $i}' | sort | uniq |\
 ./a.out | gnuplot -e 'stat "-"'

ああ、今回は思い出したように、awkを使ってしまったけど、普通は tr ' ' '\n' を使うかな。

Segmentation fault

goのコードの分解実験の時、セグフォが発生したんで注意って、サラリと書いた。良い機会なので、検証してみる。

容易に実験出来るように、main関数内にあるbuf[256]てなってる所を、buf[6]にした。それから、HASHMAXも小くして(5)、観測が容易になるようにした。

sakae@deb:/tmp/z$ echo 123 | ./a.out
1       0
0       1
0       0
0       0
0       0
sakae@deb:/tmp/z$ echo 1234 | ./a.out
0       0
0       1
1       0
0       0
0       0
sakae@deb:/tmp/z$ echo 12345 | ./a.out
Segmentation fault

おめでとう、見事に現象再現。リナはまずい事が発生しても、証拠を残さない設定になってるんで、当局の要請で、ゲロるように設定(そう言えば、久し振りの忘年会はゲロの季節でもあります)。

sakae@deb:/tmp/z$ ulimit -c unlimited
sakae@deb:/tmp/z$ cc -g ab.c
sakae@deb:/tmp/z$ echo 12345 | ./a.out
Segmentation fault (core dumped)
sakae@deb:/tmp/z$ gdb -q a.out core
Reading symbols from a.out...
[New LWP 1529]
Core was generated by `./a.out'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0  0x0048536c in main () at ab.c:50
50          *p = '\0';
(gdb) p/x p
$1 = 0x0
(gdb) p/x buf
$2 = {0x31, 0x32, 0x33, 0x34, 0x35, 0x0}
(gdb) p/x &buf
$3 = 0xbff7e432
(gdb) p/x &p
$4 = 0xbff7e438

解決策はどうよ。

#define _GNU_SOURCE         /* See feature_test_macros(7) */
#include <string.h>

char *strchrnul(const char *s, int c);

The strchrnul() function is like strchr() except that if c is not found
in s, then it returns a pointer to the null  byte  at  the  end  of  s,
rather than NULL.

GNU流の解決方法が出てた。

sakae@deb:/tmp/z$ echo 12345 | ./a.out
2       0
0       1
0       0
0       0
0       1

これで枕を高くして眠れる。もう幾つ寝るとお正月? ちと、気が早いか。何故結果が2になる? GNUがお化けを送り込んできたか。それを解決出来無ければ、正月は来ないさ。

code

#include <stdio.h>
#include <string.h>

#define HASHMAX 211

int kr(const char *s, int n){
        unsigned hashval;
        for (hashval = 0; *s != '\0'; s++)
                hashval += *s;
        return hashval % n;
}

int nkr(const char *s, int n){
        unsigned hashval;
        for (hashval = 0; *s != '\0'; s++)
                hashval += *s + 31 * hashval;
        return hashval % n;
}

unsigned fnv(const char *s, int n) {
    register unsigned h = 2166136261U;
    while (*s) {
        h ^= (unsigned char) (*s++);
        h *= 16777619U;
    }
    return h % n;
}

int as(const char *s, int n){
  unsigned h =0, g;
  const char *p;
  for (p = s; *p ; p++) {
    h = (h << 4) + (*p);
    if ((g = h & 0xf0000000)) {
      h = h ^ (g >> 24);
      h = h ^ g;
    }
  }
  return h % n;
}

int main(){
  char buf[256], *p;
  FILE *fp;
  int  a[HASHMAX], b[HASHMAX], i;

  for (i=0; i<HASHMAX; i++) a[i] = b[i] = 0;
  while (fgets(buf, sizeof(buf), stdin) != NULL) {
    p = strchr(buf, '\n');
    *p = '\0';
    a[ as(buf, HASHMAX) ] += 1;
    b[ fnv(buf, HASHMAX) ] += 1;
  }
  for (i=0; i<HASHMAX; i++) printf("%d\t%d\n", a[i], b[i]);
}

This year's Index

Home