Photo by Tron Le on Unsplash

Performance of Crystal Array, Set, and Hash

In reference to Performance of Array, Set, and Hash in Ruby, I was curious to see how the three different data structures perform when run in Crystal. The code is slightly modified to handle the types Crystal wants to see and the Time.now method in ruby is Time.local in Crystal.

testarray = Array(Int64).new
testset = Set(Int64).new
testhash = Hash(Symbol, Int64).new

i = 0_i64
10000000.times do |i_i64|
  testarray.push(i)
  testset.add(i)
  testhash[:i] = i 
end

array_start = Time.local
testarray.each do |num|
  num * num
end
array_finish = Time.local

set_start = Time.local
testset.each do |num|
  num * num
end
set_finish = Time.local

hash_start = Time.local
testhash.each do |k,v|
  v * v
end
hash_finish = Time.local

puts "Array timing: #{array_finish - array_start}
Set timing: #{set_finish - set_start}
Hash timing: #{hash_finish - hash_start}"

We run the code in two ways. First, just as a test with crystal run. And second, as crystal build --release. There are two levels of compiler optimizations here, where "run" is basically zero optimizations. 

The output from crystal run:

crystal run array_set_hash_timing.cr
Array timing: 00:00:00.046390000
Set timing: 00:00:00
Hash timing: 00:00:00.000001000

The output from crystal build --release and then running the subsequent binary:

crystal build array_set_hash_timing.cr --release
./array_set_hash_timing
Array timing: 00:00:00.005887000
Set timing: 00:00:00
Hash timing: 00:00:00

The first thing to notice is that Hashes and Sets are still faster than Arrays. The second thing to notice is that with compiler optimizations, hashes and sets are so fast they don't even register with Time.local differences. We will shortly tackle why.

The obvious difference here is the sheer increase in performance between Ruby and Crystal. For comparison, here are the results from the former performance test in Ruby:

Array timing: 0.273671
Set timing: 0.314005
Hash timing: 2.0e-06

The tests are run on the same hardware and operating system as with Ruby.

Let's get some real performance numbers for Crystal. Time.local is using elapsed system time, which is dependent on the cpu cycles and operating system keeping track of time. In Crystal, the more accurate measure of elapsed time between calls is the monotonic method. We just replace Time.local with Time.monotonic as so:

testarray = Array(Int64).new
testset = Set(Int64).new
testhash = Hash(Symbol, Int64).new

i = 0_i64
10000000.times do |i_i64|
  testarray.push(i)
  testset.add(i)
  testhash[:i] = i
end

array_start = Time.monotonic
testarray.each do |num|
  num * num
end
array_finish = Time.monotonic

set_start = Time.monotonic
testset.each do |num|
  num * num
end
set_finish = Time.monotonic

hash_start = Time.monotonic
testhash.each do |k, v|
  v * v
end
hash_finish = Time.monotonic

puts "Array timing: #{array_finish - array_start}
Set timing: #{set_finish - set_start}
Hash timing: #{hash_finish - hash_start}"

And here are the results we wanted to see, first as crystal run and then crystal build --release:

crystal run array_set_hash_timing.cr
Array timing: 00:00:00.039063026
Set timing: 00:00:00.000000319
Hash timing: 00:00:00.000000199
crystal build array_set_hash_timing.cr --release
./array_set_hash_timing
Array timing: 00:00:00.006179172
Set timing: 00:00:00.000000180
Hash timing: 00:00:00.000000111

Once again, array is slower than set or hash.  In comparison, the slowest data structure in Crystal is still 100x faster than in Ruby. 

I'm learning Crystal by writing a web server log parser. The ruby code parses a 30GB log file (of this website) around 32,000 lines per second. In Crystal, nearly the same code is parsing the same file around 815,000 lines per second.  The sheer performance difference between Crystal and Ruby is quite astounding. It's faster to parse the log file in Crystal on a Raspberry Pi 4 than it is to parse in Ruby on an AMD EPYC Threadripper 32. The best part is you can cross-compile the crystal code to target the ARMv7 cpu in the RPI4 from an AMD x86_64 build machine. I miss irb and testing code blocks, but crystal playground is great for realtime testing code.