しょんぼり技術メモ

まいにちがしょんぼり

MessagePack-RPCでバイナリデータを扱うときにUTF-8のままsliceすると死ぬほど遅いのでforce_encodingしよう(Ruby 1.9)

Ruby 1.9から、文字列にそのエンコーディングが含まれるようになった。また、MessagePack-RPCでは、Encoding::ASCII_8BITの文字列を送っても、受け手側ではEncoding::UTF_8になってしまうという仕様がある。
そのため、受け取ったデータをそのままsliceしたりすると非常に遅い。あるオフセットからのデータを取り出したいときなどに顕著となる。

以下実験。
(UTF-8の"あ")*1024 というデータ(3072バイト)を2バイトずつに切り出していく処理。

これをRuby 1.8.7で実行すると:

version: 1.8.7
  0.505368 sec          ←

となるが、1.9.2で実行すると:

version: 1.9.2
by UTF-8:
data_utf8.encoding: UTF-8
data_utf8.length: 1024
  1.894018 sec          ←
  converting by force-encoding
  0.000003 sec
data_ascii.encoding: ASCII-8BIT
data_ascii.length: 3072
by ASCII:
  0.207381 sec

と、3〜4倍遅くなる。コレを防ぐには、str.force-encoding(Encoding::ASCII_8BIT)してやればよいようだ。

実験コードは以下の通り:

#!/bin/env ruby                                                                  
# -*- encoding: UTF-8                                                            
                                                                                 
def bench_time                                                                   
  before = Time.now                                                              
  yield                                                                          
  printf("  %8.6f sec\n", Time.now - before)                                     
end                                                                              
                                                                                 
def do_bench(data)                                                               
  bench_time do                                                                  
    (1024*1024).times do |i|                                                     
      data.slice(i*2, 2)                                                         
    end                                                                          
  end                                                                            
end                                                                              
                                                                                 
# base data                                                                      
data_utf8 = "" * 1024                                                          
                                                                                 
puts "version: #{RUBY_VERSION}"                                                  
                                                                                 
if RUBY_VERSION < "1.9.0"                                                        
  do_bench(data_utf8)                                                            
  exit 0                                                                         
end                                                                              
                                                                                 
# ruby >= 1.9                                                                    
puts "by UTF-8:"                                                                 
puts "data_utf8.encoding: #{data_utf8.encoding}"                                 
puts "data_utf8.length: #{data_utf8.length}"                                     
do_bench(data_utf8)                                                              
                                                                                 
puts "  converting by force-encoding"                                            
data_ascii = nil                                                                 
bench_time do                                                                    
  data_ascii = data_utf8.force_encoding(Encoding::ASCII_8BIT)                    
end                                                                              
puts "data_ascii.encoding: #{data_ascii.encoding}"                               
puts "data_ascii.length: #{data_ascii.length}"                                   
                                                                                 
puts "by ASCII:"                                                                 
do_bench(data_ascii)