6

I am finding that the time it takes to deliver a message in Elixir is proportional to the size of the message, when I would expect it to be relatively constant. Since data structures are immutable, the runtime should be able to pass large structures between processes by reference (in constant time). Consider the following test.

use Bitwise

defmodule PerfTask do
  def pack(s) do
    {millis, packed} = :timer.tc(fn -> Enum.to_list(s) end)
    IO.puts("packed in #{millis} millis")
    Task.async(fn -> packed end)    
  end

  def unpack(t) do
    {millis, unpacked} = :timer.tc(fn -> Task.await(t) end)
    IO.puts("unpacked in #{millis} millis")
    unpacked
  end

  def go(n) do
    IO.puts "n = #{n}"
    1..n |> pack |> unpack
  end
end

PerfTask.go(1 <<< 20)

With 2^20 elements in the list, this prints

n = 1048576
packed in 106481 millis
unpacked in 9916 millis

It takes roughly 10 times as long to build the list as it does to get it out of the Task. (Note that the list is built before the task starts. All the task has to do is return the already-built list.)

With 2^22 elements in the list, it prints

n = 4194304
packed in 397428 millis
unpacked in 38748 millis

The ratio is still approximately 10:1. The 4x longer list takes 4x as long to send between processes. What am I missing?

$ iex
Erlang/OTP 18 [erts-7.2] [source] [64-bit] [smp:8:8] [async-threads:10] [kernel-poll:false]

Interactive Elixir (1.2.0) - press Ctrl+C to exit (type h() ENTER for help)

(I have confirmed the problem is not specific to the Task module by replacing it with plain processes with similar results.)

Peer Stritzinger
  • 8,232
  • 2
  • 30
  • 43
Peter Winton
  • 574
  • 3
  • 8
  • 2
    I may be wrong but I think you're making a bad assumption that data is passed by reference. Since there's no guarantee that a process will be running on the same machine, I'm pretty sure all data is passed by value. – Onorio Catenacci Feb 01 '16 at 17:26
  • 3
    See this Q & A for more on this question: http://stackoverflow.com/questions/3406425/does-erlang-always-copy-messages-between-processes-on-the-same-node – Onorio Catenacci Feb 01 '16 at 17:28

1 Answers1

6

Per this answer from @rvirding your basic assumption is flawed. To quote Mr. Virding:

. . . current versions of Erlang basically copy everything except for larger binaries. In older pre-SMP times it was feasible to not copy but pass references. While this resulted in very fast message passing it created other problems in the implementation, primarily it made garbage collection more difficult and complicated implementation. I think that today passing references and having shared data could result in excessive locking and synchronisation which is, of course, not a Good Thing.

In the context of Elixir, "larger binaries" means very long strings--bigger than 64K.

Community
  • 1
  • 1
Onorio Catenacci
  • 14,928
  • 14
  • 81
  • 132
  • 1
    I would have assumed the other way given my programming background as well. It's a natural assumption. But Erlang deals with scaling in a much different fashion than most of us are used to. – Onorio Catenacci Feb 01 '16 at 18:13
  • Just to make some corrections, the size at which heap binaries become refcounted binaries is 64B. There are 2 other kinds of binaries as well, sub binaries (that reference other binaries) and match contexts (optimized for pattern-matching). source: http://erlang.org/doc/efficiency_guide/binaryhandling.html – asonge Feb 08 '16 at 02:09