Concurrency performance comparison between Erlang and Go

Erlang has been known for its prowess in concurrent and distributed programming, by providing the three primitives spawn, send, and receive, which make concurrent programming easy. Although Erlang may not be the first to provide such a built-in language feature, its selling point was that you can create hundreds or thousands of cooperating Erlang processes without having to worry about its performance. And Erlang did deliver.  Real life applications do use this feature heavily.

Enter the Go language, which also provides similar primitives. The Go designers tried to avoid the process/thread war of words altogether, by calling it a goroutine (a co-routine). Simplistically, It’s basically a thread, which shares address space with other goroutines within a process, just like the Erlang processes within a virtual machine. A goroutine is implemented as a function, which can be invoked with the go keyword. It’s like spawn in Erlang. Related to goroutines are communication channels, a built-in reference type that provides mechanism to perform cross-goroutine communication. This is like send and receive in Erlang. And just like Erlang processes, goroutines must be able to communicate in order to send and receive information and coordinate their efforts.

Since Erlang and Go provide very similar concurrent programming features, I want to know how they measure up to each other.

In his Programming Erlang book, Joe Armstrong, the Erlang designer, had a small program to investigate how long it takes to spawn a large number of processes. Here’s the code of the program:

-module(processes).

-export([max/1]).

%% max(N)
%%   Create N processes then destroy them
%%   See how much time this takes

max(N) ->
    Max = erlang:system_info(process_limit),
    io:format("Maximum allowed processes: ~p~n", [Max]),
    statistics(runtime),
    statistics(wall_clock),
    L = for(1, N, fun()->
			  spawn(fun() ->
					wait()
				end)
		  end),
    lists:foreach(fun(Pid) ->
			  Pid ! die end, L),
    {_, Time1} = statistics(runtime),
    {_, Time2} = statistics(wall_clock),
    U1 = Time1 * 1000 / N,
    U2 = Time2 * 1000 / N,
    io:format("Process spawn time = ~p (~p) microseconds~n", [U1, U2]).

wait() ->
    receive
	die ->
	    void
    end.

for(N, N, F) ->
    [F()];
for(I, N, F) ->
    [F() | for(I+1, N, F)].

Note that I have modified the code a little to calculate the time to spawn the processes and to send a message to all processes to terminate.

Then I wrote a small Go program which performs similar work:

/*
** Copyright (C) 2012, xp@renzhi.ca
** All rights reserved.
**/

package main

import (
	"fmt"
	"time"
	"runtime"
	"os"
	"strconv"
)

const maxThreads int = 10
const usageMsg string = "test_concurrency num_threads\n"

func main() {
	if len(os.Args) < 2 {
		fmt.Printf("%s", usageMsg)
		return
	}
	var numThreads int
	numThreads, _ = strconv.Atoi(os.Args[1])
	fmt.Printf("Number of threads: %d\n", numThreads)

	num_cpu := runtime.NumCPU()
	fmt.Printf("Available cpu: %d\n", num_cpu)
	runtime.GOMAXPROCS(num_cpu)

	var ch chan int
	ch = make(chan int)
	t1 := time.Now()
	for i := 0; i < numThreads; i++ {
		go wait_4_cmd(i, ch)
	}
	for i := 0; i < numThreads; i++ {
		ch <- -1
	}
	d := time.Since(t1)
	fmt.Printf("Total time: %s\n", d.String())
	var a float64 = float64(d) / float64(numThreads) / 1000.0
	fmt.Printf("Average   : %f microseconds\n", a)
}

func wait_4_cmd(id int, ch chan int) {
    for {
		cmd := <- ch
		if cmd == -1 {
			return
		}
	}

}

Both programs perform the same work, i.e. create N number of processes, or goroutines, and then send a message to all of them to terminate.

On my laptop, here are some of the run results. The first part is the Erlang result:

xp@shanghai:~/erlang-projects/process$ erl +P 500000
Erlang R14A (erts-5.8) [source] [64-bit] [smp:2:2] [rq:2] [async-threads:0] [kernel-poll:false]

Eshell V5.8  (abort with ^G)
1> processes:max(20000).
Maximum allowed processes: 500000
Process spawn time = 13.0 (13.35) microseconds
ok
2> processes:max(100000).
Maximum allowed processes: 500000
Process spawn time = 14.0 (15.58) microseconds
ok
3> processes:max(200000).
Maximum allowed processes: 500000
Process spawn time = 13.2 (15.155) microseconds
ok
4>

The second part is the Go result:

xp@shanghai:~/workspace-xp/test-go$ bin/test_concurrency 20000
Number of threads: 20000
Available cpu: 2
Total time: 207.354ms
Average   : 10.367700 microseconds
xp@shanghai:~/workspace-xp/test-go$ bin/test_concurrency 100000
Number of threads: 100000
Available cpu: 2
Total time: 860.024ms
Average   : 8.600240 microseconds
xp@shanghai:~/workspace-xp/test-go$ bin/test_concurrency 200000
Number of threads: 200000
Available cpu: 2
Total time: 1.503317s
Average   : 7.516585 microseconds

In Go, there’s no easy way to compute the CPU time vs the elapse time. However, we can see that Go has better results. The average time to spawn a process in Erlang is about 13 to 14 microseconds, while the average time in Go is much lower. I have run many times, from creating 10000 processes/goroutines to 400000, in almost all cases, the results are quite consistent, Go won it hands down.

This measurement is, by no means, scientific. I just wanted to find out how Go stacks up against Erlang, in this specific feature. So far, the result looks pretty good for Go. However, Erlang still has many tricks in its bag to be overthrown by the new comer. For example, can you create a goroutine on another machine, or across Go processes? Can you easily write cross-node communication in Go? These are some of tricks that made Erlang a great platform. And there are other cool stuffs in Erlang too, like hot swappable code, supervisor, etc.

3 Comments

  1. Vince says:

    Interesting, I didn’t think Go could create goroutines that fast. That’s nice to know.

    Cheers

  2. Linicks says:

    Ran these with Go 1.0.2, and Erlang R16B with almost identicle performance on my development box. It seems as though Erlang is gaining in performance, but Go 1.1 is supposed to be quite a bit faster than Go 1.0.x. It will be interesting to test Go again when 1.1 is released.

  3. xp says:

    I haven’t compared the new versions, but that would be something interesting to look at.

Leave a Reply

*


Switch to our mobile site