在 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