简单测试 Erlang/OTP 27 新增的 json 模块

发表于 更新于

前言

前不久 Erlang 出现了一个新的提案(EEP-68),该提案旨在将 JSON 的编码/解码功能引入到 Erlang/OTP 中。随着最近 27.0-rc2 版本的发布,新增的名为 json 的模块已经可用了。

本文不是严格全面的基准测试,只是十分简单的性能比较。只起到基本的传播作用。

测试环境

硬件/系统

在 32 核 7950x,6000 频率 DDR5 内存的机器上,运行的系统是 NixOS。

工具链版本

  • Erlang: 27.0-rc2
  • Elixir: 1.16.2-otp-26

库版本

defp deps do
  [
    {:benchee, "~> 1.3"},
    {:jason, "~> 1.4"},
    {:poison, "~> 5.0"},
    {:simdjsone, "~> 0.2.2"},
    {:jiffy, "~> 1.1"},
    {:jsone, "~> 1.8"}
  ]
end

构造测试样例

我有以下区块链交易信息的 JSON 数据:

{
  "txs": [
    {
      "lock_time": 0,
      "ver": 1,
      "size": 224,
      "inputs": [
        {
          "sequence": 4294967295,
          "prev_out": {
            "spent": true,
            "tx_index": 251046142,
            "type": 0,
            "addr": "1Cz2o3kcCzWUME4yaTakC78ZmAuGmnkARX",
            "value": 2327000,
            "n": 0,
            "script": "76a914837297476f9f22b1b63aef82560b3ae0610c980388ac"
          },
          "script": "483045022100b0f9f874293d3ea9d214411a20ce094b819ac564d3dfeeeaef651bf9b9bfb9410220665470d6c58ad5ab77ce81179efdc45a1629b592f98e93f7b45dd7d7a3591c06012103da87165824fb8bd4face2ea4cf97e3ffe8359110141096f750db876289d66223"
        }
      ],
      "double_spend": false,
      "time": 1494882215,
      "tx_index": 251067906,
      "vin_sz": 1,
      "hash": "a7fd5cf3901ffd4410dd730942366658648b64119cfc8240feb3c07806703540",
      "vout_sz": 2,
      "relayed_by": "127.0.0.1",
      "out": [
        {
          "spent": false,
          "tx_index": 251067906,
          "type": 0,
          "addr": "1Bf226wi388ax87yCFxcijYP8iz1KCxasU",
          "value": 29880,
          "n": 0,
          "script": "76a91474e1ea3e27554771e289045c7a082045246ae86a88ac"
        },
        {
          "spent": false,
          "tx_index": 251067906,
          "type": 0,
          "addr": "37McuYxfwo97fiFUwxUpj8FG3McWrWvfk2",
          "value": 2270000,
          "n": 1,
          "script": "a9143e25a34198abcbcae06cf4e235ea8de65fafba6387"
        }
      ]
    }
    // ...
  ]
}

这个 JSON 内容相当长,有 500 多行。为了避免干扰阅读,此处我仅显示一条交易数据,其余的全部截断(完整 JSON 内容)。

创建函数用于产生测试用例:

defmodule Otp27Json do
  @doc """
  返回区块链交易的 map 数据。
  """
  @spec blockchain() :: map
  def blockchain do
    json = File.read!("blockchain.json")

    :json.decode(json)
  end

  @doc """
  返回区块链交易的 JSON 字符串。
  """
  @spec blockchain_json() :: String.t()
  def blockchain_json do
    File.read!("blockchain.json")
  end
end

这两个函数在性能测试时只会被调用一次,其返回的数据将被所有编码/解码函数使用。所以这些函数不会涉及性能测试。

测试代码

创建 benches/encode_sample.exs 用于编码测试,内容如下:

blockchain = Otp27Json.blockchain()

Benchee.run(%{
  ":json.encode/1" => fn -> blockchain |> :json.encode() |> :erlang.iolist_to_binary() end,
  "Poison.encode!/1" => fn -> Poison.encode!(blockchain) end,
  "Jason.encode!/1" => fn -> Jason.encode!(blockchain) end,
  ":simdjson.encode/1" => fn -> :simdjson.encode(blockchain) end,
  ":jiffy.encode/1" => fn -> :jiffy.encode(blockchain) end,
  ":jsone.encode/1" => fn -> :jsone.encode(blockchain) end
})

创建 benches/decode_sample.exs 用于解码测试,内容如下:

blockchain_json = Otp27Json.blockchain_json()

Benchee.run(%{
  ":json.decode/1" => fn -> :json.decode(blockchain_json) end,
  "Poison.decode!/1" => fn -> Poison.decode!(blockchain_json) end,
  "Jason.decode!/1" => fn -> Jason.decode!(blockchain_json) end,
  ":simdjson.decode/1" => fn -> :simdjson.decode(blockchain_json) end,
  ":jiffy.decode/2" => fn -> :jiffy.decode(blockchain_json, [:return_maps]) end,
  ":jsone.decode/1" => fn -> :jsone.decode(blockchain_json) end
})

这里我们调用了 6 个库的编码/解码函数,它们返回的最终结果都是 json 字符串和 map 数据。

启动测试

先后执行 mix run benches/encode_sample.exsmix run benches/decode_sample.exs 命令,并等待结果。

测试结果

编码性能比较

Name                         ips        average  deviation         median         99th %
:jiffy.encode/1          35.42 K       28.23 μs    ±17.94%       27.54 μs       36.77 μs
:simdjson.encode/1       32.10 K       31.16 μs    ±25.41%       30.44 μs       38.13 μs
:json.encode/1           22.88 K       43.71 μs    ±16.54%       42.34 μs       57.77 μs
Jason.encode!/1          16.76 K       59.67 μs    ±25.47%       57.16 μs       71.09 μs
Poison.encode!/1         13.51 K       74.02 μs    ±13.44%       73.41 μs      115.66 μs
:jsone.encode/1          13.19 K       75.83 μs    ±14.45%       72.11 μs      120.80 μs

Comparison:
:jiffy.encode/1          35.42 K
:simdjson.encode/1       32.10 K - 1.10x slower +2.92 μs
:json.encode/1           22.88 K - 1.55x slower +15.48 μs
Jason.encode!/1          16.76 K - 2.11x slower +31.44 μs
Poison.encode!/1         13.51 K - 2.62x slower +45.79 μs
:jsone.encode/1          13.19 K - 2.69x slower +47.60 μs

解码性能比较

Name                         ips        average  deviation         median         99th %
:simdjson.decode/1       32.28 K       30.98 μs    ±42.04%       27.56 μs      113.37 μs
:json.decode/1           17.57 K       56.92 μs    ±10.80%       55.58 μs       94.70 μs
Poison.decode!/1         16.58 K       60.30 μs     ±7.07%       59.05 μs       75.67 μs
Jason.decode!/1          15.88 K       62.98 μs     ±6.76%       62.19 μs       79.82 μs
:jsone.decode/1          13.59 K       73.58 μs    ±11.68%       71.66 μs       97.37 μs
:jiffy.decode/2          12.02 K       83.23 μs    ±17.19%       75.18 μs      109.93 μs

Comparison:
:simdjson.decode/1       32.28 K
:json.decode/1           17.57 K - 1.84x slower +25.93 μs
Poison.decode!/1         16.58 K - 1.95x slower +29.32 μs
Jason.decode!/1          15.88 K - 2.03x slower +31.99 μs
:jsone.decode/1          13.59 K - 2.37x slower +42.60 μs
:jiffy.decode/2          12.02 K - 2.69x slower +52.24 μs

总结

在编码性能上,jiffy 和 simdjson 这两个 NIF 实现都很出色。json 模块明显慢于它们,但也明显快于当前 Elixir 生态的流行实现如 Poison 和 Jason。垫底的 jsone 是纯 Erlang 实现的 JSON 库,它自称很快,但实际结果比想象中要糟。

在解码性能上,simdjson 一骑绝尘。json 模块和两个流行的 Elixir 实现差别不大。jsone 依然没有亮点,出乎意料的是 jiffy 居然垫底了。

所以综合来讲,注重性能的仍然可以选择 simdjson。新增的 json 模块中规中矩,还有上升空间。

结束语

我个人非常赞同引入内置的 JSON 支持。作为业务类型的语言,有什么理由不这么做?相反,如果是一门底层语言,有什么理由内置?

新增的 json 模块必然能整体提高 Elixir/Erlang 应用生态的 JSON 编解码性能,因为人们不总是会为了性能去选择 NIF 的实现。如果你不需要,也可以在编译或打包时将其排除。

加入我们

如果你也是 Elixir 开发者/爱好者,这里有一些我创建的群组:

添加 QQ 群时请填写来源为“博客”。注意请不要灌水,谢谢。

本文由作者按照 CC BY 4.0 进行授权
分享:

相关文章