追求速度的極限 —— 在elixir裏使用 :atomics 模塊操做 mutable 數據

在 elixir 中經常使用的數據結構都是不可變(immutable)的,也就是每次修改其實是在內存中新建一個數據。不可變數據的好處是能夠避免反作用,方便測試,減小bug。缺點也很明顯,就是速度慢。數組

例如 advent of code 2020 的第23天Part2 這道題,經過使用可變的數據結構,能夠大幅提高速度。數據結構

題目大意是:給定一個數列,開頭9個數是 「389125467」,後面是 10 到 1百萬,最後一個數鏈接到開頭造成環。以 3 爲當前數,執行如下操做 100 百萬次 —— 「將當前數右邊3個數拿起,而後在剩餘的數字裏尋找一個目標數,它比當前數小,且值最接近,若是沒有,則返回最大的數。將拿起的3個數插入到目標數的右邊。以當前數右邊的數做爲新的當前數。」測試

在 elixir 裏是沒法直接操做鏈表指針的,但咱們能夠用 map 來模擬一個單鏈表:atom

defmodule MapList do
    def new do
      %{}
    end

    def put(s, a, next) do
      Map.put(s, a, next)
    end

    def find(s, a) do
      Map.get(s, a)
    end

    def exchange(s, a, x) do
      Map.get_and_update!(s, a, fn c -> {c, x} end)
    end
  end

下面是實際的解題邏輯:指針

def start(label, amount, moves, module) do
    label = String.split(label, "", trim: true) |> Enum.map(&String.to_integer/1)
    list = module.new()

    list =
      for {x, n} <- Enum.zip(label, tl(label) ++ [length(label) + 1]), reduce: list do
        acc ->
          module.put(acc, x, n)
      end

    list =
      for x <- (length(label) + 1)..(amount - 1), reduce: list do
        acc ->
          module.put(acc, x, x + 1)
      end

    list = module.put(list, amount, hd(label))

    list = move(list, hd(label), amount, moves, module)

    a = module.find(list, 1)
    b = module.find(list, a)
    149_245_887_792 = a * b
  end

  def move(list, _, _, 0, _), do: list

  def move(list, i, max, moves, module) do
    a = module.find(list, i)
    b = module.find(list, a)
    c = module.find(list, b)
    t = target(i - 1, [a, b, c], max)
    {tr, list} = module.exchange(list, t, a)
    {cr, list} = module.exchange(list, c, tr)
    list = module.put(list, i, cr)
    move(list, cr, max, moves - 1, module)
  end

  def target(0, picked, max) do
    target(max, picked, max)
  end

  def target(i, picked, max) do
    if i in picked do
      target(i - 1, picked, max)
    else
      i
    end
  end

因爲 map 是不可變數據結構,可想而知將其複製數億次耗時是比較長的。在 erlang 21 版本後,加入了 :atomics 模塊,可讓咱們新建並修改一個全局的數組。能夠用它來實現一個可變的單鏈表。code

defmodule Atomics do
    def new do
      :atomics.new(1_000_000, [])
    end

    def put(s, a, next) do
      :ok = :atomics.put(s, a, next)
      s
    end

    def find(s, a) do
      :atomics.get(s, a)
    end

    def exchange(s, a, x) do
      old = :atomics.exchange(s, a, x)
      {old, s}
    end
  end

比較下兩種實現的速度:ip

def run do
    Benchee.run(%{
      "map" => fn -> start("389125467", 1_000_000, 10_000_000, MapList) end,
      "atomics" => fn -> start("389125467", 1_000_000, 10_000_000, Atomics) end
    })
  end
Name              ips        average  deviation         median         99th %
atomics          0.22         4.56 s     ±6.09%         4.56 s         4.76 s
map            0.0239        41.76 s     ±0.00%        41.76 s        41.76 s

Comparison: 
atomics          0.22
map            0.0239 - 9.16x slower +37.20 s

能夠看到使用 :atomics 模塊後,耗時只有 map 的九分之一左右。內存

:atomics 也有它的侷限性,例如數組的值只能是 64 位的整數。get

相關文章
相關標籤/搜索