1, Elixir compile data into beam

对于beam 来说,一般有几种存储数据的方式:

  • process dictionary
  • ETS table

如果把gen server 的state 也算是一种方式的话,那么还有:

  • gen server state

在Elixir 的Macro 中,提供了很方便的方式,可以在编译期将数据编译成beam,最简单的方式就是使用 @expr,关于@ 的详细文档,可以参见 here.

defmodule CompileDataBeam_1 do
  @beam_data %{a: "a", b: "b"}
  def test_beam_data do
    @beam_data
  end
end

在这个例子中,将一个map 在编译期compile 到了beam,当调用 test_beam_data/0 这个函数时,就可以得到 %{a: "a", b: "b"} 这个map.

@expr

编译一个.ex 是比较基本的操作,

$ elixirc compile_data_beam_1.ex

编译之后,会得到.beam 文件,大概是这个样子:

$ ls Elixir.CompileDataBeam_1.beam
Elixir.CompileDataBeam_1.beam

接着,可以尝试去反编译它,将它反编译成 .erl,接着再有 .erl 文件生成 .S 文件。需要反编译的话,这里有个 library.

$ iex -pa erlware_commons/_build/default/lib/erlware_commons/ebin/
iex(1)> :ec_compile.beam_to_erl_source(Elixir.CompileDataBeam_1, "compile_data_beam_1.erl")
:ok

至此,.erl 文件,很简单的得到了,其中,最主要的部分应该是:

test_beam_data() -> #{a => <<"a">>, b => <<"b">>}.

接着,再将 erl 文件编译成 S 文件,

$ erlc -S compile_data_beam_1.erl

将会得到 compile_data_beam_1.S,其中,最重要的部分是:

{function, test_beam_data, 0, 10}.
  {label,9}.
    {line,[{location,"compile_data_beam_1.erl",26}]}.
    {func_info,{atom,'Elixir.CompileDataBeam_1'},{atom,test_beam_data},0}.
  {label,10}.
    {move,{literal,#{a => <<"a">>,b => <<"b">>}},{x,0}}.
    return.

到这里,基本上可以看得出来,%{a: "a", b: "b"} 是作为一个字面量(literal variable)编译到beam 的。

define functions

%{a: "a", b: "b"} 作为一个整块编译到beam 足够简单粗暴,如果想得到特定key 对应的value, 可以使用 Map.get/2,3 函数。

但是,如果尝试将一个很大的变量compile 到beam,那么将会编译期触发memory 方面的问题:

defmodule CompileDataBeamBigVar do
  @big_big_map Map.new(for i <- 1..100_000, do: {i, Enum.take_random(?a..?z, 10)})
  def get(k) do
    Map.get(@big_big_map, k)
  end
end

在编译期间,系统内存将会出现很大的spike (这个和Elixir 代码编译成beam 的过程有关,也和Erlang AST 相关,这一部分,将在后续的writing 详细分析)。

多说几句背景,在 tubitv 有几个地方重度使用到了『编译期将database 数据compile 到beam』这个特性,其中有两个地方,非常典型:

  • content policy
  • content discovery

content policy 主要是将content 的policy 进行compile,然后利用简单表达式计算,加速某个content policy 的check,这就要求将所有的policy 进行compile,以及content 到policy 映射关系进行编译,随着content 数量的增多,如果还仅仅作为一个变量来编译的话,service 时不时就会memory crash.

content discovery 是将content 分类编排,比如说,动作片放哪些content id, 喜剧片放哪些,在tubi 对这里的预期是,content ops 修改了分类编排之后,可以尽快的set live,同样考虑到policy 的影响,对content discovery 这部分的内容也做了编译期处理,将其compile 到beam,提高interface performance.

implementation

在这个例子中,map 的key 和 value 都固定,所以可以试用函数:

def get(1), do: 'value_1'
def get(2), do: 'value_2'
...

完整的module:

defmodule CompileDataBeamDefineFunctions do
  @big_big_map Map.new(for i <- 1..100_000, do: {i, Enum.take_random(?a..?z, 10)})
  @big_big_map
  |> Enum.each(fn {k, v} ->
    def get(unquote(k)), do: unquote(v)
  end)
end

define modules

不出意外的话,编译完成的beam 文件,将会达到3.7M 的大小,这对于 EVM 来说,是非常大的文件了,非常容易导致各种问题,compile 以及load binary. 既然可以将一个大的变量编译成functions, 为何不能将其编译成多个module呢?

implementation through hash keys

将一个大的map 的key 通过hash 拆分为多个块,可以分为三步:

  • split data

    在这个例子中,data 是一个大的map,所以,可以根据key 的hash 值将其分为若干小的map

  • define modules

    对于每个小的map,需要将其compile 成不同的module

  • define external function

    由于将大的map 编译成了多个module,就需要维护key 到module name 的映射关系

split data

拆分最简单的办法就是hash:

  def split_map(map, n) do
    hash_map = map
    |> Enum.map(fn {k, _} = item ->
      {:erlang.phash2(k, n), item}
    end)
    for i <- 0..(n - 1) do
      {i, :proplists.get_all_values(i, hash_map)}
    end
  end

可以使用 Erlang 自带的函数对key 进行hash, :erlang.phash2/2.

define modules

  big_big_map
  |> CompileDataBeamDefineModules.Help.split_map(@slice_size)
  |> Enum.map(fn {i, map_list} ->
    defmodule Module.safe_concat([CompileDataBeamDefineModules, "#{i}"]) do
      map_list
      |> Enum.map(fn {k, v} ->
        def get(unquote(k)), do: unquote(v)
      end)
    end
  end)

可以像define function 那样define module,非常简单。

define get function

最简单定义function 的方式:

def get(k) do
   apply(Module.safe_concat([CompileDataBeamDefineModules, "#{:erang.phash2(k, n)}"]), :get, [k])
end

n 是拆分成块的数量。

但是这样有一个问题,在runtime concat module 是一个非常耗时的操作,可以将这一步放在编译期:

  for i <- 0..(n - 1) do
    defp module_name(unquote(i)) do
      unquote([CompileDataBeamDefineModules, "#{i}"] |> Module.safe_concat())
    end
  end

  def get(k) do
    apply(module_name(:erlang.phash2(k, n)), :get, [k])
  end

define modules using multi-files

define 多个 module ,可以在编译期间将内存spike 的问题尽可能修复,系统内存尽可能平稳,但是并没有降低编译时间。虽然是多个module, 但是由于放在同一个文件中,在Elixir 编译期间,是将这多个module 看做一个文件,只能够利用一个process 来编译这多个module.

大概是这段 code

    result =
      spawn_workers(files, [], [], [], [], %{
        dest: Keyword.get(options, :dest),
        each_cycle: Keyword.get(options, :each_cycle, fn -> [] end),
        each_file: Keyword.get(options, :each_file, fn _file -> :ok end),
        each_long_compilation: Keyword.get(options, :each_long_compilation, fn _file -> :ok end),
        each_module: Keyword.get(options, :each_module, fn _file, _module, _binary -> :ok end),
        output: output,
        long_compilation_threshold: Keyword.get(options, :long_compilation_threshold, 15),
        schedulers: schedulers
      })

一个文件,spawn 一个worker 去compile,所以,如果将多个module 放置在多个文件中,是否就可以利用多核去编译,加快编译速度?

Elixir 提供了 EEx.

define eex file

defmodule CompileDataBeamSplitFiles.N<%= number %> do
  <%= total%>
  |> CompileDataBeamSplitFiles.Help.split_map()
  |> Map.get(<%= number %>)
  |> Enum.map(fn {k, v} -> def get(unquote(k)), do: unquote(v) end)
end

numbertotal 是输入参数。

the generator

defmodule CompileDataBeamSplitFiles.Generator do

  def generate_multi_files(n) do
    for i <- 0..(n - 1) do
      content = EEx.eval_file("def_modules_split_files.eex", number: i, total: n)
      filename = "def_modules_split_files_#{i}.ex"
      File.write!(filename, content)
    end
  end
end

如果不使用mix 来组织代码的话,就需要这个一个 generator 来生成文件,如果使用mix 的话,应该在project 的 compilers scope 中定义 before compile .

总结下

在tubitv,这种将一个大的变量(来自database)编译成多个文件多个module,极大的稳定了系统内存,避免出现因内存spike 而node crash 的情况。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,258评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,335评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,225评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,126评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,140评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,098评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,018评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,857评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,298评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,518评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,400评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,993评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,638评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,661评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352

推荐阅读更多精彩内容