简单测试 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.exs
和 mix 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 开发者/爱好者,这里有一些我创建的群组:
-
Telegram 交流群:
@elixircn_dev
-
QQ 交流群:
912763380
添加 QQ 群时请填写来源为“博客”。注意请不要灌水,谢谢。
相关文章
让 Void Linux 成为 Elixir 应用的基础镜像
前言 Void Linux 是一个忍不住想关注的发行版,它既可以较为精小,又可以相对膨胀。它同时维护 glibc 和 musl 两个不同 C 库的版本,又发布有内置 BusyBox 和 GNU Coreutils 两个不同工具集
Elixir 与 Rust 协作开发
前言 本文介绍的是我所使用的两门重要编程语言 Elixir 和 Rust,尤其是 Elixir 作为我个人的主力开发语言已有好几年时间。在前期我始终将它们独立使用,各自解决
在 NixOS 中使用 asdf 管理 Erlang 的多个版本
介绍 NixOS 是一个特殊的 Linux 发行版,它不遵循 FHS 以至于你通常不能运行来自第三方的存在动态链接的二进制文件。对于这类预编译的软件,你可以手动修补所有的二