small medium large xlarge

Tom201501_pragsmall
13 Feb 2015, 14:16
Tom Rawdanowicz (1 post)

I am relatively new to programming, and currently trying to get a taste for a range of languages. I find the concepts of functional programming appealing, and given that I had the most experience with Ruby previously, I decided to try out Elixir and bought the book.

The promise of easy multi-threaded execution made me salivate, so I quickly fired up iex to run spawn/pmap1.exs on my machine. I found that Parallel.pmap to square 1..10_000_000 gave me a noticeable ~90sec processing time. The CPU’s all lit up, and the fans on my MacBook Pro went crazy - very satisfying.

I then decided to compare it to Enum.map, expecting it to be somewhat slower. To my surprise the task finished before I had a chance to look away from the screen - approx 10 times faster!!

The code is straight from the book:

MBP:spawn USER$ cat pmap1.exs
#---
# Excerpted from "Programming Elixir",
# published by The Pragmatic Bookshelf.
# Copyrights apply to this code. It may not be used to create training material,
# courses, books, articles, and the like. Contact us if you are in doubt.
# We make no guarantees that this code is fit for any purpose.
# Visit http://www.pragmaticprogrammer.com/titles/elixir for more book information.
#---
defmodule Parallel do
def pmap(collection, func) do
collection
|> Enum.map(&(Task.async(fn -> func.(&1) end)))
|> Enum.map(&Task.await/1)
end
end

Here are the formal timings:

iex(5)> {elapsed,result} = :timer.tc(&(Parallel.pmap/2), [1..10_000_000, &(&1 * &1)])
{99200598,
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121, 144, 169, 196, 225, 256, 289, 324, 361, 400, 441, 484, 529, 576, 625, 676, 729, 784, 841, 900, 961, 1024, 1089, 1156, 1225, 1296, 1369, 1444, 1521, 1600, 1681, 1764, 1849, 1936, 2025, 2116, 2209, 2304, 2401, ...]}

iex(6)> {elapsed,result} = :timer.tc(&(Enum.map/2), [1..10_000_000, &(&1 * &1)])
{11663665,
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121, 144, 169, 196, 225, 256, 289, 324, 361, 400, 441, 484, 529, 576, 625, 676, 729, 784, 841, 900, 961, 1024, 1089, 1156, 1225, 1296, 1369, 1444, 1521, 1600, 1681, 1764, 1849, 1936, 2025, 2116, 2209, 2304, 2401, ...]}

Given that Ruby is supposedly “slow”, I was astonished that the actual performance on this task was even better! These are the Ruby 2.1 results in irb:

2.1.0 :001 > require 'benchmark'

2.1.0 :005 > puts Benchmark.realtime{Array(1..10000000).map!{|nr| nr * nr}}
1.060514
=> nil

Compiling pmap1.exs did not seem to make any difference.

I understand that the task being done asynchronously is not very complex, so the overhead of spinning up multiple threads might be the killer here, but does a 10 fold difference sound reasonable?

Am I missing anything?

Could you provide another example that might demonstrate the advantage of parallel processing better than this one?

Tom R

You must be logged in to comment