追求速度的极限 —— 在elixir里使用 :atomics 模块操作 mutable 数据

甲戌神展子江
• 阅读 1465

在 elixir 中常用的数据结构都是不可变(immutable)的,也就是每次修改实际上是在内存中新建一个数据。不可变数据的好处是可以避免副作用,方便测试,减少bug。缺点也很明显,就是速度慢。

例如 advent of code 2020 的第23天Part2 这道题,通过使用可变的数据结构,可以大幅提升速度。

题目大意是:给定一个数列,开头9个数是 “389125467”,后面是 10 到 1百万,最后一个数连接到开头形成环。以 3 为当前数,执行以下操作 100 百万次 —— “将当前数右边3个数拿起,然后在剩余的数字里寻找一个目标数,它比当前数小,且值最接近,如果没有,则返回最大的数。将拿起的3个数插入到目标数的右边。以当前数右边的数作为新的当前数。”

在 elixir 里是无法直接操作链表指针的,但我们可以用 map 来模拟一个单链表:

  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 模块,可以让我们新建并修改一个全局的数组。可以用它来实现一个可变的单链表。

  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

比较下两种实现的速度:

  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 位的整数。

点赞
收藏
评论区
推荐文章
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Jacquelyn38 Jacquelyn38
4年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Wesley13 Wesley13
3年前
java中final和static
 final的用法:final的意思是最终的,最后的额,不可变的,在java中也具有相似的含义。  final修饰基础数据表示把该数据修饰成常量,意味着不可修改,不可变。  final修饰对象的引用的时候,表示该引用不可变,但是引用的结果是可变的。这里和修饰数组相似,修饰数组的时候数组里边的内容是可变的。  final定义的
Wesley13 Wesley13
3年前
java SQL常用语句总结大全(超详细)
数据库数据库定义:\\数据库:\\存储数据的仓库.其本质是一个文件系统,数据库按照特定的格式将数据存储到文件中,使用者可以对数据库中的数据进行增加,修改,删除及查询操作。存储位置优点缺点内存例如:集合,实体类对象数据是放在内存中存取速度很快不能永久的保存,程序停止时,内存释放数据消失文件例如
Souleigh ✨ Souleigh ✨
4年前
JS 实现单链表
要存储多个元素,数组(或列表)可能是最常用的数据结构。但这种数据结构有一个缺点:(在大多数语言中)数据的大小是固定的,从数组的起点或中间插入或移除项的成本很高。  链表存储有序的集合,但不同于数组,链表中的元素在内存中并不是连续放置的。每个元素由一个存储元素本身的节点和一个指向下一个元素的引用(也称指针或链接)组成。  相对于传统的数组,链表的一个好处是
可莉 可莉
3年前
10. Scala数据结构(上)
10.1数据结构特点   10.1.1Scala集合基本介绍       uml统一建模语言      1)Scala同时支持不可变集合和可变集合,不可变集合可以安全的并发访问      两个主要的包      不可变集合:scala.collection.immutable      
Stella981 Stella981
3年前
Redis缓存如何保证一致性
为什么使用Redis做缓存MySQL缺点单机连接数目有限对数据进行写速度慢Redis优点内存操作数据速度快IO复用,速度快单线程模型,避免线程切换带来的开销,速度快一致性问题  读数据的时候首先去Redis里读,没有读到再去MySQL里读,读回来之后
Stella981 Stella981
3年前
IOS数据存储之NSUserDefaults
概述数据存储是开发中必不可少的一个功能,我们可以通过Sqlite数据库手动创建数据库,定义数据表;可以使用IOS的数据框架CoreData,更方便的操作数据库;也可以直接读写文件系统;这里将介绍另外一种常用的方法:使用NSUserDefaults类,以字典形式保存数据,IOS会自动把字典中的键值对转换成对应的XML文件(也就是plist文件),这
Wesley13 Wesley13
3年前
Java中的mutable和immutable对象实例讲解
1.mutable(可变)和immutable(不可变)类型的区别可变类型的对象:提供了可以改变其内部数据值的操作,其内部的值可以被重新更改。不可变数据类型:其内部的操作不会改变内部的值,一旦试图更改其内部值,将会构造一个新的对象而非对原来的值进行更改。2.mutable和immutable类型的优缺点 mutableimmutabl
Wesley13 Wesley13
3年前
Java设计模式之immutable(不可变)模式
immutable简介不可变对象永远不会发生改变,其字段的值只在构造函数运行时设置一次,其后就不会再改变。例如JDK中常见的两种基本数据类型String和Integer,它们都是不可变对象。为了理解immutable与mutable的区别,可以看看下面的一段代码:packagedate0804.demo2;
Wesley13 Wesley13
3年前
.Net转Java自学之路—基础巩固篇十三(集合)
集合:集合是用于存储对象的一个工具。  集合与数组的特点    相同点:都是一个容器    不同点:      集合:可以存储对象,只能存储对象,集合长度可变。      数组:可以存储对象,也可以存储基本数据类型,数组长度固定。  容器对象有很多种,通过内部的数据结构来区分,数据结构就是一种数据存储方式。  在不断