Wednesday, October 14, 2009

Loops in Ruby

Recently I decided to learn yet another programming language for some scripting purposes. And I tried to choose what language it would be: Python or Ruby. Both are being under heavy development and promising now. Both have some advantages and some lacks. Finally Ruby seems to be more beautiful, more expressive and more diverse while Python is stricter (in the meaning of C-style programming), simpler and much faster(!).
I could't make a choise for too long. So I decided to learn both of them and to make a choise after learning them better.
The first one to learn was Ruby. Of course I started to read a literature about it (downloaded some free books) and watch for RSS-feeds devoted to it. A while ago I stumbled on an article describing a lot of ways to implement loops in Ruby. Well, the article comprises about 20 techniques to write a loop. Not a few you know.
The first question I had was: "Are there some difference between time complexities of these implementations?" This question is very important and not only for me. Why? Because if you provide users with similar facilities you should guarantee that they will get just about the same result. As a mathematician I can say that in the sense of operations and functions a result of an operation is not only some number or a string etc. - a result of any operation also includes time spent for the operation. Eventually what a loop will you choose? I would choose that one which is the fastest.
So, I tested few variants of ruby-loops and measured the time spent for them. I use Ruby versions from the official ubuntu-repository. Here it is a source code of my simple test:
#!/usr/bin/ruby -w

count = ARGV[0].to_i

timer = Time.now
1.upto(count) do |i|
  j = i
end
timer = Time.now - timer
puts "time of '1.upto(#{count})': #{timer.to_f};"

timer = Time.now
count.times do |i|
  j = i
end
timer = Time.now - timer
puts "time of '#{count}.times': #{timer.to_f};"

timer = Time.now
i = 0
while i < count
  j = i
  i += 1
end
timer = Time.now - timer
puts "time of 'while i < #{count}': #{timer.to_f};"

timer = Time.now
for i in 1..count
  j = i
end
timer = Time.now - timer
puts "time of 'for i in 1..#{count}': #{timer.to_f};"

timer = Time.now
i = 0
loop do
  j = i
  i += 1
  break if i == count
end
timer = Time.now - timer
puts "time of 'loop do': #{timer.to_f};"

timer = Time.now
(1..count).each do |k|
  j = k
end
timer = Time.now - timer
puts "time of '(1..#{count}).each': #{timer.to_f};"
Using Ruby1.9 I get the result shown below:
twosev@mars:~$ ./test.rb 100000000
time of '1.upto(100000000)': 7.486318544;
time of '100000000.times': 7.308785904;
time of 'while i < 100000000': 3.017657131;
time of 'for i in 1..100000000': 24.533301017;
time of 'loop do': 9.221907458;
time of '(1..100000000).each': 8.044952292;
Using Ruby1.8 I get the next result:
twosev@mars:~$ ruby1.8 ./test.rb 100000000
time of '1.upto(100000000)': 55.227473;
time of '100000000.times': 55.592968;
time of 'while i < 100000000': 34.947448;
time of 'for i in 1..100000000': 51.095535;
time of 'loop do': 67.403318;
time of '(1..100000000).each': 53.441183;
As one can see from the result 'while'-loop in Ruby1.9 is about 8 times faster than 'for'-loop. This is really bad "feature" of Ruby1.9. And this lack can be really crucial in the question which language to prefer: Ruby or Python. The difference of loop implementations in Ruby1.8 is less critical ('while' is about 2 times faster than 'loop do') but it is too slow at all. By the way, what can we say about Python in the same manner? Let's see. Opposite to Ruby, Python has only two general ways to represent a loop: 'for' and 'while'. Here it is a source of the same test written in Python:
#!/usr/bin/env python

import sys
import time

count = int(sys.argv[1])

timer = time.clock()
for i in range(count):
  j = i
print "time of 'for': " + str(time.clock() - timer)

timer = time.clock()
i = 0
while i < count:
  j = i
  i += 1
print "time of 'while': " + str(time.clock() - timer)
After running it I get:
twosev@mars:~$ ./test.py 100000000
time of 'for': 16.18
time of 'while': 23.99
Well.. This is not so good as I wanted it to be, but it is much better than Ruby. As one can see 'while'-loop in Python is slower than 'for' less than 1.5 times. In conclusion I want to compare these results with the same test written in C:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main(int argc, char *argv[])
{
  int count = atoi(argv[1]);
  double timer = (double)clock() / CLOCKS_PER_SEC;
  
  int i = 0, j = 0;
  for (; i < count; ++i) { j = i; }
  printf("time of 'for': %f\n", (double)clock() / CLOCKS_PER_SEC - timer);
  
  timer = (double)clock() / CLOCKS_PER_SEC;
  i = 0;
  while (i < count) { j = i; ++i; }
  printf("time of 'while': %f\n", (double)clock() / CLOCKS_PER_SEC - timer);
  
  return 0;
}
Of course, C as a compiled language is much faster than Ruby and Python both. So I ran the test with parameter 10000000000 (not 100000000).
twosev@mars:~$ ./test 10000000000
time of 'for': 3.720000
time of 'while': 3.720000
Wow! This language has really equal loop implementations. Of course, not Ruby nor Python can't get the same result.But I suppose they should work for it. As for our present situation, Python looks like more preferable than Ruby in the meaning of the issue described above.