hash
hash
前回は圧縮がらみでgaucheのコードを見てしまった。そこにはハッシュも見ておくようにって案内が出てた。
(define t (make-hash-table `string=?)) (hash-table-put! t "kk" 123) (hash-table-get t "kk")
普通に使うと上記のようになる。内部はどうなってる?
hash of gauche
ソースを展開してhash.cをみる。
/* For strings, we employ siphash. We use public domain implementation by Sam Trenholme http://samiam.org/blog/20131006.html. It has a version suitable for 32bit architecture, too. See dws_adapter.h for the details. */
パブリックドメインのソフトにお願いしてるとな。
/* Public domain 2013 Sam Trenholme */ /* Calculate the hash for a given string */ DwSH_WORD DwSip_hash(uint8_t *str, uint32_t len, DwSH_WORD k1, DwSH_WORD k2) { uint32_t offset = 0; int a = 0; DwSH_WORD v0 = 0, v1 = 0, v2 = 0, v3 = 0, fx = 0, m = 0; DwSip_ksetup(&k1, &k2, &v0, &v1, &v2, &v3, &fx); while(offset <= len) { m = DwSip_getword(&offset, str, len); v3 ^= m; for(a = 0; a < DwSH_CROUNDS; a++) { DwSip_round(&v0,&v1,&v2,&v3); } v0 ^= m; } :
なんと、世間一般で使われている、md5とかsha256みたいな奴っぽい。まあ、そうだわな。任意長の文字列を一定の長さの数値に変換。かつ、その数値がなるべく衝突しない実装って言うと、暗号系のハッシュしかない。
これってshiroさんの目の付けどころって事かな?
hash of ruby
と言う事で、我がrubyも調べてみる。
Hash from ri
The older syntax for Hash data uses the "hash rocket," =>: h = {:foo => 0, :bar => 1, :baz => 2} h # => {:foo=>0, :bar=>1, :baz=>2} Alternatively, but only for a Hash key that's a Symbol, you can use a newer JSON-style syntax, where each bareword becomes a Symbol: h = {foo: 0, bar: 1, baz: 2} h # => {:foo=>0, :bar=>1, :baz=>2} You can also use a String in place of a bareword: h = {'foo': 0, 'bar': 1, 'baz': 2} h # => {:foo=>0, :bar=>1, :baz=>2}
まずはハッシュの復習。昔はロケット記号ってのにお世話になったな、と遠くを見る目だ。
sip_hash13(const uint8_t key[16], const uint8_t *data, size_t len) { : for (; data != end; data += sizeof(uint64_t)) { m = U8TO64_LE(data); SIP_ROUND(m, v0, v1, v2, v3); }
string hashぐらいでgrepすると、何とまあ、gaucheと同じパブリックドメインなソフトにお世話になってました。この分ではpythonも同じかな。
hash of scm
2013年より古い、老舗のやつはどうよ? 再びscheme系に戻って、昔からのやつを調べてみる。
unsigned long strhash(str, len, n) unsigned char *str; sizet len; unsigned long n; { if (len>5) { sizet i = 5; unsigned long h = 264 % n; while (i--) h = ((h<<8) + ((unsigned)(downcase[str[h % len]]))) % n; return h; } else { sizet i = len; unsigned long h = 0; while (i) h = ((h<<8) + ((unsigned)(downcase[str[--i]]))) % n; return h; } }
nは、ハッシュのバケットと呼ばれていて、scmでは137に設定されてた。値を137にちらばるようにしてるんだな。
slib
(require 'hash) して使うハッシュ値を求める関数。それを使って実践する (require 'hash-table)が有る。R5RSの時代にはハッシュなんて機能がschemeには有りませんでしたから。ってか、いつでも作れるので仕様には入っていなかったんです。
そんな裸なschemeは嫌われた。で、srfiってのが、標準への呼び掛け。便利なポータブルで動く機能を追加して、rubyやpythonに対抗しようって事だ。
(define (hash:hash-string-ci str n) (let ((len (string-length str))) (if (> len 5) (let loop ((h (modulo 264 n)) (i 5)) (if (positive? i) (loop (modulo (+ (* h 256) (char->integer (char-downcase (string-ref str (modulo h len))))) n) (- i 1)) h)) (let loop ((h 0) (i (- len 1))) (if (>= i 0) (loop (modulo (+ (* h 256) (char->integer (char-downcase (string-ref str i)))) n) (- i 1)) h)))))
こちらはslibで定義されてた奴。
use slib
ちょっとポータブルなハッシュ機能を使ってみる。
> (require 'hash-table) ;loading /usr/local/share/slib/hashtab ; loading /usr/local/share/slib/alist ; done loading /usr/local/share/slib/alist.scm ;done loading /usr/local/share/slib/hashtab.scm #<unspecified> > (define zz (make-hash-table 101)) #<unspecified> > (define tt (hash-associator string=?)) > (tt zz "Foo" 123) #(() () () () () () () (("Foo" . 123)) () () ... ) > tt #<CLOSURE <anon> "/usr/local/share/slib/hashtab.scm": (hashtab key val) (#@let* ((num (#@hashfun #@key (#@vector-length #@hashtab)))) (#@vector-set! #@hashtab #@num (#@asso (#@vector-ref #@hashtab #@num) #@key #@val))) #@hashtab>
モジュールを読み込む。次にサイズが101のテーブルを作る。それから、データの登録用関数を登録する。この時の引数は、KEYの比較方式を指定する。これでやっと登録の準備が整った。最後は、作成された関数を使ってテーブルに登録。
> (hash "Foo" 101) 7
登録時に使ったKEYのハッシュ値を計算させてみた。登録位置はzzの7番目って事で、一致してたね。
> (define cc (hash-inquirer string=?)) #<unspecified> > (cc zz "Foo") 123 > cc #<CLOSURE <anon> "/usr/local/share/slib/hashtab.scm": (hashtab key) (#@ainq (#@v ector-ref #@hashtab (#@hashfun #@key (#@vector-length #@hashtab))) #@key)>
こちらは、アクセス例。やはりアクセス用の関数を登録してから、アクセスする。
手探りにやったので、登録関数名にttとかアクセス関数にccとかの、適当名にしちゃったけど、それらしく、put/get ぐらいが良かろう。
K&R "The C Programming language"
古典と言ったら、この本でしょう。ここにもハッシュの基本が解説されてた。さりげなくね。
struct hashlist { S_CHAR *name; S_CHAR *def; struct hashlist *next; /* next in chain */ }; #define HASHMAX 100 /* size of hashtable */ struct hashlist *hashtab[HASHMAX]; hash(s) S_CHAR * s; { int hashval; for (hashval = 0; *s != '\0';) hashval += *s++; return (hashval % HASHMAX); } lookup(s) S_CHAR * s; { struct hashlist *np; for (np = hashtab[hash(s)]; np != NULL; np = np->next) if (strcmp(s, np->name) == 0) return (np); /* found */ return (NULL); /* not found */ }
勿論ハッシュテーブルへの登録関数も掲載されてるんだけど、長くなるので省略(てか、著作権があるからね)。
単語帳
これから、hashの挙動を調べてみようと思う。特にバケット数は、素数が良いと言われているけど、本当か? なんてのを確かめてみたい。hashに喰わせる文字列は、適当に英文から拾ってくるのもあれなんで、あれを使う事にする。
世の中で一番長い単語は call-with-current-continuation だと思っている。 名前と言うなら、寿限無でしょう。実話としては、長い名前こういうのもあるそうです。そのうちにNHKのあの番組で取あげられるかな。
それはさておき、 システムに備付のやつを確認してみる。
ob$ wc /usr/share/dict/words 235971 235971 2493843 /usr/share/dict/words ob$ echo '2493843 / 235971' | bc 10
23万語を収録する単語帳で、平均は10文字と出た。でも、短かいのやら長いのやらが有るはず。ちょいとawkで確認してみる。リナだとGNUに汚染されてるので、BSD系のやつを使います。
ob$ cat cnt.swk { cnt[ length($1) ] += 1 } END { for( x in cnt ) print x, "\t", cnt[x] }
思わず連想配列が出て来たけど、これもハッシュだよな。
ob$ cat /usr/share/dict/words | awk -f cnt.awk | sort -n 1 52 2 160 3 1427 4 5286 5 10237 6 17712 7 23881 8 29999 9 32411 10 30882 11 26018 12 20468 13 14941 14 9767 15 5926 16 3376 17 1814 18 842 19 428 20 198 21 82 22 41 23 17 24 5 28 1
この表を眺めていると、どうもベル型カーブっぽい。
gnuplot> plot "cnt.txt" using 1:2 with line
グラフにしてみたら、正しくそうだった。人間の所業にもこういうカーブが現れるって面白いね。
on debian
OpenBSDばかり見ていると嫌われるのでリナでも確認。ってか、OpenBSDではやけに単語数が多いので、気になって調べてみた訳。
sakae@pen:/usr/share/dict$ wc cracklib-small american-english 54763 54763 492822 cracklib-small 102774 102774 976241 american-english 157537 157537 1469063 total
基軸通貨がポンドからドルに変ったごとく、単語もアメリカン・イングリッシュになってる。OpenBSDのそれは、英国仕様ではなかろうか?
それより気になるのは、クラッカー御用達の単語帳が付属してる事。両者に違いはあるのか、プチ確認してみる。
sakae@pen:/usr/share/dict$ grep scm american-english sakae@pen:/usr/share/dict$ grep scm cracklib-small scm
scheme業界で知らない人が居たらそれはもぐりの業者ですって名前を検索。普通の人は知らない単語ですって出て来た。対して、クラッカーは、クラッカーらしく、ちゃんと知ってた。まあ、アイドル辞書みたいなもので、ちゃーんとおみ通しなのさ。変な単語と思って、採用しないように。
なお、改めてOpenBSDののwordsの正体を見たら
words -- common words, and important technical terms from all fields, that are spelled the same in British and American usage. web2 -- Webster's Second International Dictionary, all 234,936 words worth. The 1934 copyright has lapsed.
wordsもweb2も、同一な内容なので、補足説明になるのかな。出典が明らかになってるのはリナとは違うんです。
hash of awk
perlの元となったawkにもhashが使われているはず。探してみたら、tran.cに定義されてた。
int hash(const char *s, int n) /* form hash value for string s */ { unsigned hashval; for (hashval = 0; *s != '\0'; s++) hashval = (*s + 31 * hashval); return hashval % n; }
これだとKR本の欠点を解消してるね。あちらは単に文字コードを加算してるだけだ。だから、文字列 "ab" も "ba" も、同一のハッシュ値を生成してしまう。対して、こちらは、重み付けをしてるから、同一になる事は無い。
とほほのAWK入門 が、良いまとめになっているので、見ておくと吉。debianにはgawkが入っていると思っていたら、軽量版のmawkってのが入っていた。んで、面白いオプションを発見。
sakae@deb:/tmp$ awk -W dump -f cnt.awk END 000 . pusha x 002 . a_pusha cnt 004 . set_al 018 006 . pushi x 008 . pushs "\011" 010 . pushi x 012 . ae_pushi cnt 014 . pushint 3 016 . print 018 . aloop 006 020 . pop_al 021 . exit0 MAIN 000 . omain 001 . f_pushi $1 004 . pushint 1 006 . length 008 . ae_pusha cnt 010 . pushd 1 012 . add_asg 013 . pop 014 . ol_gl
assembler-like listing of program だそうです。面白いので追求していってもいいんだけど、脱線しちゃうんで、又の機会に。
check hash
実際に単語帳のそれぞれのwordのハッシュを取り、その個数を積算してみる。 手始めにKR本のオリジナルな関数をkrとして組み込んでみる。
#include <stdio.h> #include <string.h> #define SEED "/usr/share/dict/words" #define HASHMAX 100 int kr(const char *s, int n){ unsigned hashval; for (hashval = 0; *s != '\0'; s++) hashval += *s; return hashval % n; } int main(){ char buf[64], *p; FILE *fp; int ht[HASHMAX], i; for (i=0; i<HASHMAX; i++) ht[i] = 0; fp = fopen(SEED, "r"); while (fgets(buf, sizeof(buf), fp) != NULL) { p = strchr(buf, '\n'); *p = '\0'; ht[ kr(buf, HASHMAX) ] += 1; } fclose(fp); for (i=0; i<HASHMAX; i++) printf("%d\t%d\n", i, ht[i]); }
結果の表の2列目でソートしてみた。
ob$ ./a.out | sort -n -k 2 19 1741 23 1776 21 1793 9 1797 : 65 2874 70 2886 69 2920 68 2921
大変、隔った結果となった。バケットサイズを100から素数の101に変更してみる。
6 1527 13 1599 : 60 3092 62 3115
素数にすると良い結果って嘘かな。scmのそれに習って137にしてみる。
100 1621 65 1624 : 61 1821 46 1833
長くなりそうなので、続く。