エンコーディング変換の高速化

当然の話だけど、対象の文字列が長くなると、Encode::encode も、$e->encode も大差ない。
ちょっといじって試してみる。

use strict;
use warnings;
use Benchmark qw/cmpthese timethese/;
use utf8;
use Encode;
use Jcode;
use Unicode::Japanese;

use Smart::Comments;

my $str = join '', ( 'a' .. 'z', ( map { chr } ord('ぁ') .. ord('ん') ) );
$str = $str x shift if @ARGV;
my $bytes = encode_utf8($str);
my $uj    = Unicode::Japanese->new;
my $j     = Jcode->new;
my $e     = find_encoding('sjis');

cmpthese(
    timethese(
        0,
        {
            'U::J n'  => sub { Unicode::Japanese->new( $str, 'utf8' )->sjis },
            'U::J s'  => sub { $uj->set( $str,               'utf8' )->sjis },
            'Jcode n' => sub { Jcode->new( $str,             'utf8' )->sjis },
            'Jcode s' => sub { $j->set( $str,                'utf8' )->sjis },
            'E::ft'   => sub {
                my $b2 = $bytes;
                Encode::from_to( $b2, 'UTF-8', 'Shift_JIS' );
            },
            'E::e' => sub { encode( 'sjis', $str ) },
            'E oo' => sub { $e->encode($str) },
        }
    )
);

文字列をそのまま使う場合と、100 倍長くした場合をDebian etch(Athlon 64 X2 4400+) で。

            Rate   E::ft  U::J n Jcode n  U::J s Jcode s    E::e    E oo
E::ft    39473/s      --    -35%    -39%    -58%    -59%    -59%    -85%
U::J n   60922/s     54%      --     -6%    -35%    -36%    -36%    -77%
Jcode n  64749/s     64%      6%      --    -31%    -32%    -32%    -75%
U::J s   93982/s    138%     54%     45%      --     -2%     -2%    -64%
Jcode s  95475/s    142%     57%     47%      2%      --     -0%    -64%
E::e     95475/s    142%     57%     47%      2%      0%      --    -64%
E oo    262993/s    566%    332%    306%    180%    175%    175%      --
          Rate   E::ft  U::J n  U::J s Jcode n Jcode s    E::e    E oo
E::ft   1037/s      --    -62%    -63%    -65%    -65%    -65%    -67%
U::J n  2727/s    163%      --     -2%     -7%     -8%     -9%    -12%
U::J s  2773/s    168%      2%      --     -5%     -7%     -7%    -11%
Jcode n 2925/s    182%      7%      5%      --     -2%     -2%     -6%
Jcode s 2973/s    187%      9%      7%      2%      --     -1%     -4%
E::e    2997/s    189%     10%      8%      2%      1%      --     -4%
E oo    3110/s    200%     14%     12%      6%      5%      4%      --

ついでに、Mac OS X(Core Duo 1.60 GHz) でも。

            Rate   E::ft Jcode n  U::J n    E::e Jcode s  U::J s    E oo
E::ft    23144/s      --    -42%    -45%    -55%    -59%    -66%    -78%
Jcode n  39659/s     71%      --     -6%    -23%    -30%    -42%    -62%
U::J n   42306/s     83%      7%      --    -17%    -25%    -38%    -59%
E::e     51210/s    121%     29%     21%      --     -9%    -25%    -51%
Jcode s  56520/s    144%     43%     34%     10%      --    -17%    -45%
U::J s   68495/s    196%     73%     62%     34%     21%      --    -34%
E oo    103603/s    348%    161%    145%    102%     83%     51%      --
          Rate   E::ft Jcode n Jcode s    E::e    E oo  U::J s  U::J n
E::ft    470/s      --    -52%    -53%    -56%    -57%    -71%    -72%
Jcode n  984/s    110%      --     -2%     -7%     -9%    -39%    -41%
Jcode s 1002/s    113%      2%      --     -5%     -8%    -38%    -40%
E::e    1057/s    125%      7%      5%      --     -3%    -35%    -37%
E oo    1085/s    131%     10%      8%      3%      --    -33%    -35%
U::J s  1617/s    244%     64%     61%     53%     49%      --     -3%
U::J n  1675/s    257%     70%     67%     58%     54%      4%      --

む、こちらは長くなった場合に Unicode::Japanese がやたらと早い。なんでだろ。 

ちなみに、似たような話は Python にもある*1。例えば、str から unicode を作る方法はだいたい次の方法がある。

t = unicode(s, encoding)
t = s.decode(encoding)
import codecs
decoder = codecs.getdecoder(encoding)
t = decoder(s)[0]

文字列の長さを変えながら速度を測定。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import codecs
from timeit import Timer

iteration = 1000
encoding = 'shift_jis'

for n in (2 ** n for n in xrange(10)):
    s = u' あ'.encode(encoding) * n
    timers = []
    timers.append(Timer('unicode(%s, %s)' % (repr(s), repr(encoding))))
    timers.append(Timer('%s.decode(%s)' % (repr(s), repr(encoding))))
    timers.append(Timer('decoder(%s)[0]' % repr(s), 'import codecs; decoder = codecs.getdecoder(%s)' % repr(encoding)))

    print '%5d' % n, ' '.join('%.6f' % timer.timeit(iteration) for timer in timers)
    1 0.139643 0.110270 0.057828
    2 0.151211 0.117217 0.064562
    4 0.152519 0.129006 0.069813
    8 0.168138 0.140995 0.094859
   16 0.191728 0.167559 0.118740
   32 0.247785 0.225073 0.171301
   64 0.330757 0.302506 0.255668
  128 0.500264 0.476449 0.428612
  256 0.843505 0.823368 0.776082
  512 1.538498 1.505656 1.454657

これは、str から unicode を作る方法だけど、入出力を行う場合は

import codecs
fp = codecs.open(filename, 'r', encoding)
try:
  for line in fp:
    pass
finally:
  fp.close()

とか、

import codecs
fp = file(filename, 'r')
try:
  fp = codes.getreader(encoding)(fp)
  for line in fp:
    pass
finally:
  fp.close()

とかのほうがいいかも。

*1:まぁ、http://tabesugi.net/memo/2006/83.html#010048 の受け売りだ