Cайт веб-разработчика, программиста Ruby on Rails ESV Corp. Екатеринбург, Москва, Санкт-Петербург, Новосибирск, Первоуральск

Стоит ли безоговорочно доверять тестам сравнения производительности языков программирования?

Стоит ли безоговорочно доверять тестам сравнения производительности языков программирования, скорости выполнения программ (benchmark, benchmarks) на популярных сайтах?

Более внимательное рассмотрение одного из тестов с сайта (которому многие привыкли доверять) сравнения производительности benchmarksgame.alioth.debian.org. Тест взят просто случайным образом (тот случай, где Ruby оказался медленее PHP) - тест mandelbrot.

Я написал простейшие тесты сравнения производительности для Ruby, написал точь-в-точь идентичные на PHP. На этих тестах PHP "одержал" сокрушительное поражение. И, о чудо, на сайте benchmarksgame.alioth.debian.org, которому многие привыкли безоговорочно доверять, обнаруживается, что PHP все еще на коне, да еще и очень опережает Ruby. Меня это очень заинтересовало. Как же так? Все оказалось совершенно просто - суть в реализации алгоритмов - если в тесте на PHP используются простейшие циклы, то в тесте Ruby используются итераторы - более медленный, хотя и более мощный механизм. В общем, все встало на свои места, по крайней мере для меня - Ruby быстрее PHP. Как сказал классик: "Мало ли чего можно рассказать! Не всему же надо верить."

Дабы не быть голословным, привожу исходники этого теста. Знатоки Ruby и PHP сразу заметят в чем тут соль, и почему Ruby вдруг оказался медленнее.

 

PHP:

/* 
   The Computer Language Benchmarks Game
   http://benchmarksgame.alioth.debian.org/

   contributed by Thomas GODART (based on Greg Buchholz's C program)
   multicore by anon
 */

function getProcs() {
   $procs = 1;
   if (file_exists('/proc/cpuinfo')) {
      $procs = preg_match_all('/^processor\s/m', file_get_contents('/proc/cpuinfo'), $discard);
   }
   $procs <<= 1;
   return $procs;
}

$h = (int) (($argc == 2) ? $argv[1] : 600);
$w = $h;

if ($w % 8) {
   fprintf(STDERR, "width %d not multiple of 8\n", $w);
   exit(1);
}

printf ("P4\n%d %d\n", $w, $h);

$shsize = $w * ($w -->> 3);
$shmop = shmop_open(ftok(__FILE__, chr(time() & 255)), 'c', 0644, $shsize);

if (!$shmop) {
   echo "faild to shmop_open()\n";
   exit(1);
}

$bit_num = 128;
$byte_acc = 0;

$yfac = 2.0 / $h;
$xfac = 2.0 / $w;

$shifted_w = $w >> 3;
$step = 1;

$procs = getProcs();
$child = $procs - 1;
while ($child > 0) {
   $pid = pcntl_fork();
   if ($pid === -1) {
      die('could not fork');
   } else if ($pid) {
      --$child;
      continue;
   }
   break;
}

$step = $procs;
$y = $child;

for ( ; $y < $h ; $y+=$step)
{
   $result = array('c*');

   $Ci = $y * $yfac - 1.0;

   for ($x = 0 ; $x < $w ; ++$x)
   {
      $Zr = 0; $Zi = 0; $Tr = 0; $Ti = 0.0;

      $Cr = $x * $xfac - 1.5;

      do {
         for ($i = 0 ; $i < 50 ; ++$i)
         {
            $Zi = 2.0 * $Zr * $Zi + $Ci;
            $Zr = $Tr - $Ti + $Cr;
            $Tr = $Zr * $Zr;
            if (($Tr+($Ti = $Zi * $Zi)) > 4.0) break 2;
         }
         $byte_acc += $bit_num;
      } while (FALSE);

      if ($bit_num === 1) {
         $result[] = $byte_acc;
         $bit_num = 128;
         $byte_acc = 0;
      } else {
         $bit_num >>= 1;
      }
   }
   if ($bit_num !== 128) {
      $result[] = $byte_acc;
      $bit_num = 128;
      $byte_acc = 0;
   }
   shmop_write($shmop, call_user_func_array('pack', $result), $y * $shifted_w);
}

if ($child > 0) {
   exit(0);
}

$child = $procs - 1;
$status = 0;
while ($child-- > 0) {
   pcntl_wait($status);
}

$step = $shsize >> 3;
for($i = 0; $i < $shsize; $i+=$step) {
   echo shmop_read($shmop, $i, $step);
}
shmop_delete($shmop);

 

Ruby:

# The Computer Language Benchmarks Game
# http://benchmarksgame.alioth.debian.org
#
#  contributed by Karl von Laudermann
#  modified by Jeremy Echols
#  modified by Detlef Reichl
#  modified by Joseph LaFata

PAD = "\\\\__MARSHAL_RECORD_SEPARATOR__//" # silly, but works

class Worker
  
  attr_reader :reader
  
  def initialize(enum, index, total, &block)
    @enum             = enum
    @index            = index
    @total            = total
    @reader, @writer  = IO.pipe
    
    if RUBY_PLATFORM == "java"
      @t = Thread.new do
        self.execute(&block)
      end
    else
      @p = Process.fork do
        @reader.close
        self.execute(&block)
        @writer.close
      end
      
      @writer.close
    end
  end
  
  def execute(&block)
    (0 ... @enum.size).step(@total) do |bi|
      idx = bi + @index
      if item = @enum[idx]
        res = yield(item)
        @writer.write(Marshal.dump([idx, res]) + PAD)
      end
    end
    
    @writer.write(Marshal.dump(:end) + PAD)
  end
end

def parallel_map(enum, worker_count = 8, &block)
  count = [enum.size, worker_count].min
  
  Array.new(enum.size).tap do |res|  
    workers = (0 ... count).map do |idx|
      Worker.new(enum, idx, count, &block)
    end
  
    ios = workers.map { |w| w.reader }

    while ios.size > 0 do
      sr, sw, se = IO.select(ios, nil, nil, 0.01)

      if sr
        sr.each do |io|
          buf = ""
          
          while sbuf = io.readpartial(4096)
            buf += sbuf
            break if sbuf.size < 4096
          end
          
          msgs = buf.split(PAD)
          
          msgs.each do |msg|
            m = Marshal.load(msg)
            if m == :end
              ios.delete(io)
            else
              idx, content = m
              res[idx] = content
            end
          end
        end
      end      
    end
    
    Process.waitall
  end
end

$size = (ARGV[0] || 100).to_i
csize = $size - 1

puts "P4"
puts "#{$size} #{$size}"

set = (0 ... $size).to_a

results = parallel_map(set, 8) do |y|
  res = ""
  
  byte_acc = 0
  bit_num  = 0
  
  ci = (2.0 * y / $size) - 1.0

  $size.times do |x|
    zrzr = zr = 0.0
    zizi = zi = 0.0
    cr = (2.0 * x / $size) - 1.5
    escape = 0b1
  
    50.times do
      tr = zrzr - zizi + cr
      ti = 2.0 * zr * zi + ci
      zr = tr
      zi = ti
      # preserve recalculation
      zrzr = zr * zr
      zizi = zi * zi
      if zrzr + zizi > 4.0
        escape = 0b0
        break
      end
    end
  
    byte_acc = (byte_acc << 1) | escape
    bit_num  += 1
    
    if (bit_num == 8)
      res += byte_acc.chr
      byte_acc = 0
      bit_num = 0
    elsif (x == csize)
      byte_acc <<= (8 - bit_num)
      res += byte_acc.chr
      byte_acc = 0
      bit_num = 0
    end
  end

  res
end

print results.join

Вот тот же самый тест, но уже написанный полностью идентично на Ruby и PHP, в котором все встало на свои места.