Skip to content

Commit 1576566

Browse files
committed
Benchmarks, overhauls
* true and false decode as their respective lua values now. * null decodes as a special table instance stored as the field 'NULL' on the qjson library. * largely rewrote entire library to make decoder faster. * added benchmarks * removed false information about this being thousands of times faster than rxi/json * deleted qjson_simple, not worth maintaining. Could make a smaller one not focused on performance at all if I wanted to.
1 parent 5be296f commit 1576566

File tree

6 files changed

+264
-168
lines changed

6 files changed

+264
-168
lines changed

README.md

Lines changed: 45 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,13 @@
1-
<img src="qjson.svg" height=150px />
1+
<img src="qjson.svg" height=150px/>
22

33
Tiny, quick JSON encoding/decoding in pure Lua.
44

55
# [![Release Shield](https://img.shields.io/github/v/release/Vurv78/qjson.lua?include_prereleases)](https://github.com/Vurv78/qjson.lua/releases/latest) [![License](https://img.shields.io/github/license/Vurv78/qjson.lua?color=red)](https://opensource.org/licenses/MIT) [![Linter Badge](https://github.com/Vurv78/qjson.lua/workflows/Run%20Lest/badge.svg)](https://github.com/Vurv78/qjson.lua/actions) [![github/Vurv78](https://img.shields.io/discord/824727565948157963?label=Discord&logo=discord&logoColor=ffffff&labelColor=7289DA&color=2c2f33)](https://discord.gg/yXKMt2XUXm)
66

7-
(Running `decode` benchmark by [`rxi/json.lua`](https://github.com/rxi/json.lua), encode benchmarks soon (currently ~3x faster))
8-
```
9-
Lua version : LuaJIT 2.1.0-beta3
10-
CPU name : 11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz
11-
../json.lua : 0.00494s [x7.06e+03] (min: 0.00452s, max 0.00526s)
12-
dkjson.lua : 0.0121s [x1.73e+04] (min: 0.0118s, max 0.0123s)
13-
jfjson.lua : 0.00993s [x1.42e+04] (min: 0.00954s, max 0.0103s)
14-
qjson_simple.lua : 4.3e-06s [x6.14] (min: 0s, max 1.6e-05s)
15-
qjson.lua : 7e-07s [x1] (min: 0s, max 1e-06s)
16-
```
17-
187
## Features
198
* Pure lua, should work on every version (5.1-5.4, JIT)
20-
* Incredibly fast: ~10000x faster than rxi/json at decoding.
21-
* Actually tiny, < 150 loc.
9+
* Quick, focused on performance. (See benchmarks below)
10+
* Actually tiny, ~150 sloc.
2211
* Decent error handling: `Expected : to follow key for object at char 39`
2312

2413
## Usage
@@ -30,7 +19,7 @@ print(json.encode {
3019
})
3120

3221
--[[
33-
{"qjson": ["fast","simple","tiny"],"hello": "world!"}
22+
{"qjson":["fast","simple","tiny"],"hello":"world!"}
3423
]]
3524

3625
print(json.decode([[
@@ -39,12 +28,48 @@ print(json.decode([[
3928
```
4029

4130
## Notes
42-
* `true`, `false` and `null` are represented as strings.
31+
* `null` is output as a special table instance, retrieved from `qjson.NULL`
32+
* This does not guarantee 100% compatibility with the more niche parts of the JSON spec (like unicode escapes)
33+
34+
## Benchmarks
35+
Using benchmarks/bench.lua:
36+
37+
```
38+
LuaJIT 2.1.0-beta3
39+
11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz
4340
44-
## qjson_simple
41+
Running benchmarks...
42+
| Name (Decode) | Min | Max | Avg | Avg / Best |
43+
| vurv78/qjson | 0.0091 | 0.012012 | 0.00968749 | x1.75539 |
44+
| rxi/json | 0.004745 | 0.006889 | 0.0055187 | x1 |
45+
| actboy168/json | 0.010586 | 0.012723 | 0.0113411 | x2.05503 |
46+
| luadist/dkjson | 0.011112 | 0.016785 | 0.0134858 | x2.44366 |
4547
46-
Smaller initial version of `qjson` without all of the optimizations that make it less readable.
48+
| Name (Encode) | Min | Max | Avg | Avg / Best |
49+
| vurv78/qjson | 0.003788 | 0.004491 | 0.004021 | x1 |
50+
| rxi/json | 0.0104 | 0.014977 | 0.0108013 | x2.68623 |
51+
| actboy168/json | 0.010361 | 0.012899 | 0.0110817 | x2.75596 |
52+
| luadist/dkjson | 0.014438 | 0.017056 | 0.0153153 | x3.80883 |
53+
```
54+
55+
```
56+
Lua 5.3
57+
11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz
58+
59+
Running benchmarks...
60+
| Name (Decode) | Min | Max | Avg | Avg / Best |
61+
| vurv78/qjson | 0.015195 | 0.018472 | 0.0159068 | x1 |
62+
| rxi/json | 0.045206 | 0.052624 | 0.048079 | x3.02255 |
63+
| actboy168/json | 0.019059 | 0.022751 | 0.0200545 | x1.26075 |
64+
| luadist/dkjson | 0.028623 | 0.034445 | 0.0307014 | x1.93009 |
65+
66+
| Name (Encode) | Min | Max | Avg | Avg / Best |
67+
| vurv78/qjson | 0.006012 | 0.017234 | 0.00760202 | x1 |
68+
| rxi/json | 0.015458 | 0.019752 | 0.0170518 | x2.24306 |
69+
| actboy168/json | 0.015981 | 0.021264 | 0.0169299 | x2.22703 |
70+
| luadist/dkjson | 0.02172 | 0.025274 | 0.0229826 | x3.02323 |
71+
```
4772

48-
Often runs worse than `qjson`, but can run at around the same speed.
73+
From here, you can see this library is significantly faster on regular lua, and a bit slower than rxi/json on LuaJIT.
4974

50-
Doesn't support escaping strings, exponential numbers, or `null`.
75+
Currently working on making it faster for LuaJIT, but this is pretty hard to fix considering making it faster would require not using as many [lua patterns](https://www.lua.org/pil/20.2.html), which would slow down PUC-Lua.

benchmarks/bench.lua

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
2+
---@alias BenchResult { min: number, max: number, avg: number }
3+
local function bench(times, fn) ---@return BenchResult
4+
collectgarbage() -- Clear gc before running
5+
6+
for _ = 1, 5 do -- Warm up
7+
fn()
8+
end
9+
10+
local results --[=[@type number[]]=] = {}
11+
for i = 1, times do
12+
local start = os.clock()
13+
fn()
14+
results[i] = os.clock() - start
15+
end
16+
17+
local min, max, avg = math.huge, -math.huge, 0
18+
for _, time in ipairs(results) do
19+
if time < min then
20+
min = time
21+
elseif time > max then
22+
max = time
23+
end
24+
25+
avg = avg + time
26+
end
27+
28+
return {
29+
min = min, max = max, avg = avg / times
30+
}
31+
end
32+
33+
local function get(url)
34+
local handle = assert(io.popen("curl -s " .. url))
35+
local content = handle:read("*a")
36+
handle:close()
37+
return content
38+
end
39+
40+
local loadstring = loadstring or load
41+
42+
print("Downloading required files...")
43+
44+
local LIBS --[[@type table<string, table>]] = {
45+
["vurv78/qjson"] = require "qjson",
46+
["rxi/json"] = loadstring(get("https://raw.githubusercontent.com/rxi/json.lua/master/json.lua"))(),
47+
["actboy168/json"] = loadstring(get("https://raw.githubusercontent.com/actboy168/json.lua/master/json.lua"))(),
48+
["luadist/dkjson"] = loadstring(get("https://raw.githubusercontent.com/LuaDist/dkjson/master/dkjson.lua"))(),
49+
}
50+
51+
local DECODE_TARGET = get("https://raw.githubusercontent.com/simdjson/simdjson/master/jsonexamples/twitter.json")
52+
local ENCODE_TARGET = require("qjson").decode(DECODE_TARGET)
53+
54+
local util = require "benchmarks.util"
55+
56+
print( util.version )
57+
print( util.cpu )
58+
59+
print("Running benchmarks... ")
60+
61+
do
62+
local total --[=[@type table<string, BenchResult>]=] = {}
63+
for name, lib in pairs(LIBS) do
64+
local decode = lib.decode
65+
total[name] = bench(100, function()
66+
decode(DECODE_TARGET)
67+
end)
68+
end
69+
70+
local best = math.huge
71+
for _, result in pairs(total) do
72+
if result.avg < best then
73+
best = result.avg
74+
end
75+
end
76+
77+
print( ("| %-15s | %-10s | %-10s | %-10s | %-11s |"):format("Name (Decode)", "Min", "Max", "Avg", "Avg / Best") )
78+
for name, result in pairs(total) do
79+
print( ("| %-15s | %-10g | %-10g | %-10g | x%-10g |"):format(name, result.min, result.max, result.avg, result.avg / best) )
80+
end
81+
end
82+
83+
print()
84+
85+
do
86+
local total --[=[@type table<string, BenchResult>]=] = {}
87+
for name, lib in pairs(LIBS) do
88+
local encode = lib.encode
89+
total[name] = bench(100, function()
90+
encode(ENCODE_TARGET)
91+
end)
92+
end
93+
94+
local best = math.huge
95+
for _, result in pairs(total) do
96+
if result.avg < best then
97+
best = result.avg
98+
end
99+
end
100+
101+
print( ("| %-15s | %-10s | %-10s | %-10s | %-11s |"):format("Name (Encode)", "Min", "Max", "Avg", "Avg / Best") )
102+
for name, result in pairs(total) do
103+
print( ("| %-15s | %-10g | %-10g | %-10g | x%-10g |"):format(name, result.min, result.max, result.avg, result.avg / best) )
104+
end
105+
end

benchmarks/util.lua

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
local os = package.config:sub(1, 1) == "\\" and "Windows" or "Unix"
2+
local cpu
3+
4+
if os == "Windows" then
5+
local handle, err = io.popen("wmic cpu get name /VALUE")
6+
assert(handle, err)
7+
cpu = handle:read("*a"):match("Name=([^\n]+)")
8+
handle:close()
9+
else
10+
local handle, err = io.popen([[lscpu | sed -nr '/Model name/ s/.*:\s*(.* @ .*)/\1/p']])
11+
assert(handle, err)
12+
cpu = handle:read("*a")
13+
handle:close()
14+
end
15+
16+
return {
17+
os = os,
18+
version = jit and jit.version or _VERSION,
19+
cpu = cpu
20+
}

0 commit comments

Comments
 (0)