読者です 読者をやめる 読者になる 読者になる

時間の比較は文字列と数値のどちらが早い?

tech

最近会社でapacheのアクセスログ解析というのをperlを使って行っています。例えばこんな感じのログがテキストファイルに保存されています。

...
2010-09-20 21:30:12 A.B.C.D http://example.com/hoge.html ...
2010-09-20 21:30:13 W.X.Y.Z http://example.com/foo/bar.html ...
...

このログファイルのサイズがだいたい1GBくらいあります。レコード数は500万行くらいだったかな(ちょっとあいまい)。このログ全部を解析すると時間もかかるし、必要な部分が見づらくなります。なので、特定の時間帯だけを解析するようにしています。
僕は時間帯を判定するのに、ログから取り出した時間を文字列のまま比較するという実装をしていました。

  • 文字列のまま時間を比較(PHPの例)
<?php
#解析する始めと終わりの時間
$start_time = "21:15:00"
$end_time = "22:30:00"

#$timeにはhh:mm:ss形式の時間文字列が入っている
if($time > $start_time && $time < $end_time)
{
  #do something
}
?>


で、このコードを見た会社の人から「文字列比較でやってるの!?文字列比較はコストがかかるから、自分なら数値(秒)に直して比較するな」みたいなことを言われました。そう言われてみれば確かに文字列比較ってコストかかるよなー(と思う)。でも文字列を数値に直すのにもコストかかるからそんなに変わらないかもとも思ったりしたわけです。
大量のログなので少しでも早い実装をしたいと思い、どれくらいの速度差がでるのか「文字列での比較」と「数値での比較」のサンプルコードを書いて比較してみました。

  • 時刻を数値に直して比較
<?php
#解析する始めと終わりの時間
$start_time = 21 * 3600 + 15 * 60 + 0 #21:15:00を秒で表現
$end_time = 22 * 3600 + 30 * 60 + 0 #22:30:00を秒で表現

#$timeにはhh:mm:ss形式の時間文字列が入っている
$data = explode(":" , $time);

#時、分、秒を秒に変換
$s = $data[0] * 3600 + $data[1] * 60 + $data[0];

#数値で時間の比較
if($s > $start_time && $s < end_time)
{
  #do something
}
?>

実際は速度差がわかりやすいようにループをまわして00:00:00から23:59:59までの比較を170万回行いました。


結果はというと、なんと文字列のまま比較したほうが早いという結果になりました。
(数値に直して比較バージョン2では時刻中の:を削除して数値に変換する方法をとっています)

プログラム 文字列のまま比較 数値に直して比較バージョン1 数値に直して比較バージョン2
perl 0m3.997s 0m7.094s 0m6.712s
php 0m4.653s 0m8.718s 0m6.795s
ruby 0m10.363s 0m21.304s 0m16.303s


「文字列を分割して、数値に変換するコスト」が「文字列の比較を行うコスト」よりも多いということかな。それとも僕の文字列から数値への変換方法がまずいのかな。
言語のソースとか読めたらわかるんだろうけど、ちょっと僕には敷居が高いですね(´・ω・`)
まあとりあえずは文字列のまま比較してログ解析を行います。


PHPのコードはこん感じです。

文字列のまま比較
<?php
#秒をhh:mm:ssに変換する(seqはsequenceの略)
function toTime($seq)
{
  $h = $seq / 3600;
  $m = ($seq / 60) % 60;
  $s = $seq % 60;

  return sprintf("%02d:%02d:%02d" , $h , $m , $s);
}

#始まりと終わりの時間
$start_time = "21:15:00";
$end_time = "22:30:00";

$count = 0;

#24:00:00未満まで計測
$max = 24 * 60 * 60;

#たくさん比較するためのループ
for($i = 0; $i < 20; $i++)
{
  #00:00:00から23:59:59までループ
  for($j = 0; $j < $max; $j++)
  {
    $time = toTime($j);

    if($time > $start_time && $time < $end_time)
    {
      $count++;
    } 
  } 
} 

echo "$count\n";
?>
数値に変換して比較
<?php
function toTime($seq)
{
  $h = $seq / 3600;
  $m = ($seq / 60) % 60;
  $s = $seq % 60;

  return sprintf("%02d:%02d:%02d" , $h , $m , $s);
}

$start_time = 21 * 3600 + 15 * 60 + 0;
$end_time = 22 * 3600 + 30 * 60 + 0;

$count = 0;

$max = 24 * 60 * 60;

for($i = 0; $i < 20; $i++)
{
  for($j = 0; $j < $max; $j++)
  {
    $time = toTime($j);

    #:で分割して時、分、秒に分ける
    $data = explode(":" , $time);

    #時、分、秒を秒に変換
    $s = $data[0] * 3600 + $data[1] * 60 + $data[2];

    if($s > $start_time && $s < $end_time)
    {
      $count++;
    }
  }
}

echo "$count\n";

数値に変換して比較バージョン2

<?php
function toTime($seq)
{
  $h = $seq / 3600;
  $m = ($seq / 60) % 60;
  $s = $seq % 60;

  return sprintf("%02d:%02d:%02d" , $h , $m , $s);
}

$start_time = 211500;
$end_time = 223000;
$count = 0;
$max = 24 * 60 * 60;
for($i = 0; $i < 20; $i++)
{
  for($j = 0; $j < $max; $j++)
  {
    $time = toTime($j);

    #時間文字列中の:を削除して擬似的な数値に変換
    $s = str_replace(":" , "" , $time);

    if($s > $start_time && $s < $end_time)
    {
      $count++;
    }
  }
}

echo "$count\n";