uniq のための sort -n と LC_MONETARY

ファイルに int な文字列がたくさん書いてあって、 uniq するだけのために sort するときって、 "-n" つけますか??

きっと sort するためには numeric で扱ってくれたほうが楽なんだろうけど、 numeric に変換するのもまたコストだよなー、とおもうところです。

ということで、比較してみました。

環境

ichii386% uname -a
SunOS aho 5.11 snv_121 i86pc i386 i86pc Solaris
ichii386% cat /etc/release
                       OpenSolaris Development snv_121 X86
           Copyright 2009 Sun Microsystems, Inc.  All Rights Reserved.
                        Use is subject to license terms.
                            Assembled 13 August 2009
ichii386% locale
LANG=C
LC_CTYPE="C"
LC_NUMERIC="C"
LC_TIME="C"
LC_COLLATE="C"
LC_MONETARY="C"
LC_MESSAGES="C"
LC_ALL=C
  • sortのバイナリ
    • /usr/gnu/bin/sort (gnu, GNU Coreutils付属)
    • /usr/bin/sort (sort, いちばんふつうの。)
    • /usr/bin/i86/sort (i86, ナゾ)
    • /usr/bin/amd64/sort (amd64, 64bit ELFらしい)
    • /usr/xpg4/bin/sort (xpg4, xpg4がどういう仕様なのかは知らない)

gnu 以外の sort がどう違うのかはよくわかんないです。誰かおしえてください。

結果

"yyyymmdd [tab] id" が 1 億レコード並んで 1.7GB のファイルを以下のようなかんじで sort しました。 /hoge/tmp は十分な容量とそれなりのI/O性能がある場所です。

cat aho.txt > /dev/null
time /usr/bin/sort -T/hoge/tmp aho.txt > /dev/null
time /usr/bin/sort -T/hoge/tmp -n aho.txt > /dev/null
time /usr/xpg4/bin/sort -T/hoge/tmp aho.txt > /dev/null
time /usr/xpg4/bin/sort -T/hoge/tmp -n aho.txt > /dev/null
time /usr/gnu/bin/sort -T/hoge/tmp aho.txt > /dev/null
time /usr/gnu/bin/sort -T/hoge/tmp -n aho.txt > /dev/null
time /usr/bin/i86/sort -T/hoge/tmp aho.txt > /dev/null
time /usr/bin/i86/sort -T/hoge/tmp -n aho.txt > /dev/null
time /usr/bin/amd64/sort -T/hoge/tmp aho.txt > /dev/null
time /usr/bin/amd64/sort -T/hoge/tmp -n aho.txt > /dev/null

結果はつぎのグラフのようになりました。

なんと、gnu版だけ -n のほうが速く、しかもトータルで一番速かった!!

なにが違う?

数値で比較してそうなとこはたぶん以下。

gnu
/* Compare strings A and B as numbers without explicitly converting them to
   machine numbers.  Comparatively slow for short strings, but asymptotically
   hideously fast. */

static int
numcompare (const char *a, const char *b)
{
  while (blanks[to_uchar (*a)])
    a++; 
  while (blanks[to_uchar (*b)])
    b++; 

  return strnumcmp (a, b, decimal_point, thousands_sep);
}

strnumcmp は coreutils 付属ですが、中身は至ってふつう。

onnv
/*
 * Scan fractional part.
 */
for (++i; i < length; i++) {
    if (IS_SEPARATOR(number[i]))
        continue;       

    if (!isdigit((uchar_t)number[i]))
        break;

    if (number[i] != '0')
        state |= IN_NUMBER;

    if (sign == '0')
        digits[j++] = '0' + '9' - number[i];
    else
        digits[j++] = number[i];
}

ここに貼ったのだけじゃなんも分かんなくて、ポインタのつもり。

で、なにがちがうかというと、 onnv 版では IS_SEPARATOR のところで LC_NUMERIC の thousands_sep だけでなく、 LC_MONETARY の mon_thousands_sep も見てるみたいです。

結果が違う例

ichii386% cat test.txt   
1
1,200
1.100
 500
1300

1,200 が 500 と 1300 の間に入ってほしいところです。

  • C, gnu
    • "," 以降は無視されているようです。
ichii386% LC_MONETARY=C /usr/gnu/bin/sort -n test.txt 
1
1,200
1.100
 500
1300
  • C, onnv
    • gnu 同様 "," 以降は無視っぽい
ichii386% LC_MONETARY=C /usr/bin/sort -n test.txt   
1
1,200
1.100
 500
1300
  • ja_JP.UTF-8, gnu
    • C のときと同じ結果でした
ichii386% LC_MONETARY=ja_JP.UTF-8 /usr/gnu/bin/sort -n test.txt 
1
1,200
1.100
 500
1300
  • ja_JP.UTF-8, onnv
    • ちゃんと 1,200 が 500 の次に来ている!
ichii386% LC_MONETARY=ja_JP.UTF-8 /usr/bin/sort -n test.txt   
1
1.100
 500
1,200
1300

まとめ

LC_MONETARY なんて意識してる人ほとんどいないような気がするので、 GNU Coreutils で余計な気遣いせずにすっきりやってくれるほうが速かった、ということなのかな。しかし、それだけの違いにしちゃあ結果が変わりすぎな気がします。ほかにも似たようなところで処理が違うのでしょう。

なんにせよ、ふだんから LC_ALL=C で暮らしているのが最強ってことですね!