You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
There is an idea to optimize the code for json.lua #34
When used .. in loops, the lua will create a lot of garbage and thus affect performance. I changed your script (json-fix.lua)
c:\test>busybox diff -U 0 json.lua json-fix.lua
--- json.lua
+++ json-fix.lua
@@ -41 +41 @@
-local json = { _version = "0.1.2" }
+local json = { _version = "0.1.2fix" }
@@ -235 +235 @@
- local res = ""
+ local res = {}
@@ -246 +246 @@
- res = res .. str:sub(k, j - 1)
+ table.insert(res, str:sub(k, j - 1))
@@ -253 +253 @@
- res = res .. parse_unicode_escape(hex)
+ table.insert(res, parse_unicode_escape(hex))
@@ -259 +259 @@
- res = res .. escape_char_map_inv[c]
+ table.insert(res, escape_char_map_inv[c])
@@ -264,2 +264,2 @@
- res = res .. str:sub(k, j - 1)
- return res, j + 1
+ table.insert(res, str:sub(k, j - 1))
+ return table.concat(res), j + 1
I created a .HAR file (size 313971312 bytes) using firefox and tested it with a simple Lua script.
local function read(file)
local f, err = io.open(file, "r");
if not f then
return;
else
local data = f:read("a");
f:close();
return (data);
end
end
local function test(obj)
print (obj._version)
local last = os.time();
local har = obj.decode(read(file));
print ('decode - ' .. os.time() - last .. " sec")
end
json = require "json";
jsonfix = require "json-fix";
file = 'Archive [21-05-25 11-43-43].har';
test(json)
test(jsonfix)
I got results like this on my computer (Intel(R) Core(TM) i3-9100 CPU @ 3.60GHz)
Reacted by Guy Sviry, αlpha 0x00, InternetUnexplorer, Olive Olivieria, Jimmy Elston, mosmetro, Semyon Filippov, Engineer Smith, Adelson, Rafael Latowicz and 6 more
(Translation)
Using "concat" is a good idea. I think it can be improved. "table.insert" can be modified to "[#tab +1] = value". The following is the test data
local tab = {}
for i=1, 10000000 do
-- table.insert(tab, i)
-- time : 8.61 7.33 7.21 s
-- tab[#tab +1] = i
-- time : 5.54 5.25 5.46 s
-- tab[table.size(tab) +1] = i
-- time : so long time
end
String concatenation using .. should never be done in loops as it incurs quadratic time complexity; each .. has to copy the entire string in linear time.
Using find + gsub could be more efficient than a rope (a rope takes 8 bytes per character, ugh), but seems to be hard to implement as the Unicode surrogate pair handling as it is currently implemented doesn't mesh well with it.
table.insert vs res[#res + 1] doesn't make much of a difference on LuaJIT, but on PUC Lua, the latter is indeed noticeably faster.
Sorry, but that claim in that form simply isn't true (or at best highly misleading). On which inputs? Since this is a complexity issue, any linear-time implementation can be arbitrarily faster.
Also, your parser doesn't seem spec-compliant to me. I see no provisions for unicode escape sequences.
It also seems to have the same complexity issue as this one: It doesn't properly use a rope for encoding.
Your isarray relies on undocumented behavior (pairs degrading to ipairs).
Formatting strings with %q does not always produce valid JSON.
TL;DR: Your JSON parser is broken and has a time complexity issue as well. Your performance claims in their current form can't be taken seriously.
Besides, my PR fixes the complexity issue.
Reacted by ROllerozxa, Lizzy Fleckenstein and Laurinchen
Sorry, but that claim in that form simply isn't true (or at best highly misleading). On which inputs? Since this is a complexity issue, any linear-time implementation can be arbitrarily faster.
Also, your parser doesn't seem spec-compliant to me. I see no provisions for unicode escape sequences. It also seems to have the same complexity issue as this one: It doesn't properly use a rope for encoding.
Your isarray relies on undocumented behavior (pairs degrading to ipairs).
Formatting strings with %q does not always produce valid JSON.
TL;DR: Your JSON parser is broken and has a time complexity issue as well. Your performance claims in their current form can't be taken seriously.
Besides, my PR fixes the complexity issue.
There are benchmarks. It is better.
Reacted by ROllerozxa, Lizzy Fleckenstein, Laurinchen and MasterDrake
I did. All of your points are invalidated by the benchmarks. Theory is theory but it comes down to the actual performance.
Oh yeah? So something being fast somehow magically makes it correct? Look, I have the fastest JSON decoder:
functionread_json(str) return1end
it's easy to optimize e.g. string decoding using gsub if you don't have to care about unicode escape sequences
"The" benchmarks of rxi/json.lua are completely insufficient; they can't claim to be representative. For the theoretical complexity issue I'm outlining, a benchmark can be trivially construed; in fact I've done it for an issue on this repo.
Just to be sure, I ran the benchmarks of rxi/json.lua against qjson.lua. Here are the results:
[decode]
Lua version : Lua 5.4
CPU name : AMD Ryzen 5 3500U with Radeon Vega Mobile Gfx
../json.lua : 0.0773s [x2.21] (min: 0.0753s, max 0.095s)
../qjson.lua : 0.035s [x1] (min: 0.0334s, max 0.0455s)
dkjson.lua : 0.0785s [x2.24] (min: 0.0725s, max 0.1s)
jfjson.lua : 0.104s [x2.98] (min: 0.0992s, max 0.134s)
[encode]
Lua version : Lua 5.4
CPU name : AMD Ryzen 5 3500U with Radeon Vega Mobile Gfx
../json.lua : 0.0287s [x1.63] (min: 0.0269s, max 0.0353s)
../qjson.lua : 0.0176s [x1] (min: 0.016s, max 0.0232s)
dkjson.lua : 0.0385s [x2.19] (min: 0.0357s, max 0.0485s)
jfjson.lua : 0.0507s [x2.88] (min: 0.0498s, max 0.054s)
json4lua.lua : 0.0476s [x2.7] (min: 0.0465s, max 0.0604s)
and here's LuaJIT
[decode]
Lua version : LuaJIT 2.1.0-beta3
CPU name : AMD Ryzen 5 3500U with Radeon Vega Mobile Gfx
../json.lua : 0.0134s [x1] (min: 0.0123s, max 0.0147s)
../qjson.lua : 0.0216s [x1.61] (min: 0.0211s, max 0.0227s)
dkjson.lua : 0.0286s [x2.13] (min: 0.0276s, max 0.0296s)
jfjson.lua : 0.0191s [x1.43] (min: 0.0185s, max 0.0209s)
[encode]
Lua version : LuaJIT 2.1.0-beta3
CPU name : AMD Ryzen 5 3500U with Radeon Vega Mobile Gfx
../json.lua : 0.0153s [x2.92] (min: 0.0148s, max 0.0161s)
../qjson.lua : 0.00524s [x1] (min: 0.0047s, max 0.0056s)
dkjson.lua : 0.0169s [x3.23] (min: 0.0163s, max 0.0182s)
jfjson.lua : 0.017s [x3.23] (min: 0.0166s, max 0.0181s)
json4lua.lua : 0.0229s [x4.37] (min: 0.0224s, max 0.0239s)
(oops, looks like qjson.lua isn't even always the fastest, despite omitting features and implementing JSON slightly incorrectly)
I don't know what you did wrong when benchmarking; qjson.lua is faster (about 2x), but definitely not a thousand or tens of thousands times for the given benchmarks.
Would probably help to have a shared benchmark script, so that people on both sides of the argument can verify that the tests are being conducted correctly (or at the very least consistently...)
Would probably help to have a shared benchmark script, so that people on both sides of the argument can verify that the tests are being conducted correctly (or at the very least consistently...)
The benchmark script is available in this repository. Just add qjson.lua to the table of JSON implementations to benchmark.
Which will affect LuaJIT (have not tested by how much)
How would that affect qjson.lua or json.lua? It's clearly in an if concerning only jfjson.lua (though it might explain why jfjson.lua is so slow in the LuaJIT benchmarks).
as it will not JIT compile variadic functions
[citation needed]
A much more likely explanation is that qjson.lua is hitting a LuaJIT NYI here since it relies heavily on patterns in string.find.
Addressing my incorrect claim that it is "thousands of times faster":
This was very wrong and a dumb mistake on my end. I've largely rewritten the code to be even further optimized and taken advantage of the rope optimization (pushed that a bit further too).
It's now faster in every case besides decoding on LuaJIT since it relies on patterns, which stitch.
I've fixed my comments to reflect this, thank you for bringing it to my attention.
This was very wrong and a dumb mistake on my end. I've largely rewritten the code to be even further optimized and taken advantage of the rope optimization (pushed that a bit further too).
Good. What remains to be addressed:
Your fix for isarray is insufficient. It deals with proper sequences (no holes) properly. It also deals with "pure" dicts (only string keys) or tables without integer keys in general properly (those will have length 0). However it doesn't deal with pathological cases such as arrays with holes properly (or perhaps you want to generate arrays for these? beware though: these can be arbitrarily sparse, which is a major performance concern). Currently it's pretty bad: Depending on what #t yields, arrays with holes may be either rejected or accepted - Schrödinger's Array! Arrays with holes could be fixed by just checking that len == 0 at the end. That would still not be sufficient though; arrays might have holes and out of range keys making up for them (consider e.g. {1, nil, 3, [42] = true}). What you really need to check is that all keys are integers (type(k) == "number" and math.floor(k) == k) within range k >= 1 and k <= #t; then you can compute the density as counted keys divided by maximum integer key, and decide based on that whether you really want to serialize to array.
You still use %q for encoding; this will fail e.g. for "\1", which will encode to just that: "\1", which is invalid JSON. This is unfortunately not trivial to fix since Lua allows arbitrary bytestrings whereas JSON expects strings of Unicode code points (e.g. there are unicode escapes, no byte escapes like Lua's escapes). You will also run into issues of UTF-8 vs. UTF-16.
Maybe I skimmed over it, but I still see no handling of Unicode escape sequences.
Took some time to find where the hell this was (the wiki has been deleted). Looks like only vararg results are unsupported based on that table, but I think in the context of a benchmark any potential for overhead should be removed though.
How would that affect qjson.lua or json.lua?
It doesn't, I'm just saying that generally the benchmarking script in this repo is not giving each project being benchmarked an equal footing (regardless of whether they're being directly compared here or not).
If detecting whether a table is an array or not is a severe bottleneck, isn't the above approach the best way to tackle this? (seems especially useful for exceptionally large arrays)
If detecting whether a table is an array or not is a severe bottleneck, isn't the above approach the best way to tackle this? (seems especially useful for exceptionally large arrays)
From a user's perspective, probably nobody would want to have to adjust all their input data, but from performance, would probably be much faster. Although I think getmetatable might be pretty slow on puc-lua
A faster way could be a lookup table which holds references to all of the array tables
From a user's perspective, probably nobody would want to have to adjust all their input data
I imagine that a user encoding the data is probably in control of how that data is generated (or can write a transformer easily). Ignoring that though, I'm not sure a user of a pure-lua JSON library really cares about splitting hairs over performance in the first place...
A faster way could be a lookup table which holds references to all of the array tables
Not sure how that's a more ergonomic API than wrapping the arrays at source, or with a transformer. That solution still requires knowing where every single array in the dataset is, and provides a duplicate source of truth for what is or isn't an array.
There's no real clear pick here. In the future maybe I'd like a configuration function to be exported which lets you tweak things like this to allow or disallow sparse arrays, but for now I prefer current behavior, or swapping to rapidjson/lunajson's method.
I think throwing an error is the best course of action. A table with holes simply can't be represented properly with accepting the possibility of sparse tables being exponentially wasteful.
Activity
IsFaser commentedon Mar 7, 2022
(Translation)
Using "concat" is a good idea. I think it can be improved. "table.insert" can be modified to "[#tab +1] = value". The following is the test data
DnsIs commentedon Mar 11, 2022
I completely agree with you.
Did the same test, same results.
Super.
".." - should be avoided for large amounts of data.
Optimize code
appgurueu commentedon Dec 17, 2022
String concatenation using
..should never be done in loops as it incurs quadratic time complexity; each..has to copy the entire string in linear time.Using
find+gsubcould be more efficient than a rope (a rope takes 8 bytes per character, ugh), but seems to be hard to implement as the Unicode surrogate pair handling as it is currently implemented doesn't mesh well with it.table.insertvsres[#res + 1]doesn't make much of a difference on LuaJIT, but on PUC Lua, the latter is indeed noticeably faster.vurvdev commentedon Jun 29, 2023
If you want a fast implementation: https://github.com/Vurv78/qjson.lua
Runs thousands of times faster than this repoappgurueu commentedon Jun 29, 2023
Sorry, but that claim in that form simply isn't true (or at best highly misleading). On which inputs? Since this is a complexity issue, any linear-time implementation can be arbitrarily faster.
Also, your parser doesn't seem spec-compliant to me. I see no provisions for unicode escape sequences.
It also seems to have the same complexity issue as this one: It doesn't properly use a rope for encoding.
Your
isarrayrelies on undocumented behavior (pairsdegrading toipairs).Formatting strings with
%qdoes not always produce valid JSON.TL;DR: Your JSON parser is broken and has a time complexity issue as well. Your performance claims in their current form can't be taken seriously.
Besides, my PR fixes the complexity issue.
yogwoggf commentedon Jun 29, 2023
There are benchmarks. It is better.
appgurueu commentedon Jun 29, 2023
I don't see how you are replying to any of my points.
yogwoggf commentedon Jun 29, 2023
I did. All of your points are invalidated by the benchmarks. Theory is theory but it comes down to the actual performance.
appgurueu commentedon Jun 29, 2023
Oh yeah? So something being fast somehow magically makes it correct? Look, I have the fastest JSON decoder:
it's easy to optimize e.g. string decoding using
gsubif you don't have to care about unicode escape sequences"The" benchmarks of
rxi/json.luaare completely insufficient; they can't claim to be representative. For the theoretical complexity issue I'm outlining, a benchmark can be trivially construed; in fact I've done it for an issue on this repo.appgurueu commentedon Jun 29, 2023
Just to be sure, I ran the benchmarks of
rxi/json.luaagainstqjson.lua. Here are the results:and here's LuaJIT
(oops, looks like
qjson.luaisn't even always the fastest, despite omitting features and implementing JSON slightly incorrectly)I don't know what you did wrong when benchmarking;
qjson.luais faster (about 2x), but definitely not a thousand or tens of thousands times for the given benchmarks.Perhaps do some benchmarks before citing them?
yogwoggf commentedon Jun 29, 2023
It's probably because you have a horribly slow CPU.
rollerozxa commentedon Jun 29, 2023
Really...?
yogwoggf commentedon Jun 29, 2023
Have you tried turning your computer on and off again? Sometimes that effects my benchmarks.
Derpius commentedon Jun 29, 2023
Would probably help to have a shared benchmark script, so that people on both sides of the argument can verify that the tests are being conducted correctly (or at the very least consistently...)
rollerozxa commentedon Jun 29, 2023
The benchmark script is available in this repository. Just add
qjson.luato the table of JSON implementations to benchmark.Derpius commentedon Jun 29, 2023
It has some odd contents such as this:
Which will affect LuaJIT (have not tested by how much) as it will not JIT compile variadic functions
appgurueu commentedon Jun 30, 2023
How would that affect
qjson.luaorjson.lua? It's clearly in anifconcerning onlyjfjson.lua(though it might explain whyjfjson.luais so slow in the LuaJIT benchmarks).[citation needed]
A much more likely explanation is that
qjson.luais hitting a LuaJIT NYI here since it relies heavily on patterns instring.find.vurvdev commentedon Jun 30, 2023
Addressing my incorrect claim that it is "thousands of times faster":
This was very wrong and a dumb mistake on my end. I've largely rewritten the code to be even further optimized and taken advantage of the rope optimization (pushed that a bit further too).
It's now faster in every case besides decoding on LuaJIT since it relies on patterns, which stitch.
I've fixed my comments to reflect this, thank you for bringing it to my attention.
See the link I posted above
appgurueu commentedon Jun 30, 2023
Good. What remains to be addressed:
isarrayis insufficient. It deals with proper sequences (no holes) properly. It also deals with "pure" dicts (only string keys) or tables without integer keys in general properly (those will have length 0). However it doesn't deal with pathological cases such as arrays with holes properly (or perhaps you want to generate arrays for these? beware though: these can be arbitrarily sparse, which is a major performance concern). Currently it's pretty bad: Depending on what#tyields, arrays with holes may be either rejected or accepted - Schrödinger's Array! Arrays with holes could be fixed by just checking thatlen == 0at the end. That would still not be sufficient though; arrays might have holes and out of range keys making up for them (consider e.g.{1, nil, 3, [42] = true}). What you really need to check is that all keys are integers (type(k) == "number" and math.floor(k) == k) within rangek >= 1 and k <= #t; then you can compute the density as counted keys divided by maximum integer key, and decide based on that whether you really want to serialize to array.%qfor encoding; this will fail e.g. for"\1", which will encode to just that:"\1", which is invalid JSON. This is unfortunately not trivial to fix since Lua allows arbitrary bytestrings whereas JSON expects strings of Unicode code points (e.g. there are unicode escapes, no byte escapes like Lua's escapes). You will also run into issues of UTF-8 vs. UTF-16.Derpius commentedon Jun 30, 2023
@appgurueu https://web.archive.org/web/20220708225331/http://wiki.luajit.org/NYI
Took some time to find where the hell this was (the wiki has been deleted). Looks like only vararg results are unsupported based on that table, but I think in the context of a benchmark any potential for overhead should be removed though.
It doesn't, I'm just saying that generally the benchmarking script in this repo is not giving each project being benchmarked an equal footing (regardless of whether they're being directly compared here or not).
Derpius commentedon Jun 30, 2023
On an unrelated note, regarding the
isarraydiscussion would the following approach not make more sense (from a performance perspective at least):If detecting whether a table is an array or not is a severe bottleneck, isn't the above approach the best way to tackle this? (seems especially useful for exceptionally large arrays)
vurvdev commentedon Jun 30, 2023
From a user's perspective, probably nobody would want to have to adjust all their input data, but from performance, would probably be much faster. Although I think
getmetatablemight be pretty slow on puc-luaA faster way could be a lookup table which holds references to all of the array tables
But still, this would require action on the user's part.
Derpius commentedon Jun 30, 2023
I imagine that a user encoding the data is probably in control of how that data is generated (or can write a transformer easily). Ignoring that though, I'm not sure a user of a pure-lua JSON library really cares about splitting hairs over performance in the first place...
Not sure how that's a more ergonomic API than wrapping the arrays at source, or with a transformer. That solution still requires knowing where every single array in the dataset is, and provides a duplicate source of truth for what is or isn't an array.
vurvdev commentedon Jun 30, 2023
This is intended. I don't think tables with gaps should be treated as arrays, or at least, they should cut off.
Checking how other JSON libraries handle encoding
{ 1, 2, [4] = 55 }util.TableToJSON{"1":1.0,"2":2.0,"4":55.0}[1, 2][1,2,null,55](only handles small gaps, throws error for excessively sparse array)[1,2,null,55][1,2,null,55][1,2]{"1":1,"2":2,"4":55}There's no real clear pick here. In the future maybe I'd like a configuration function to be exported which lets you tweak things like this to allow or disallow sparse arrays, but for now I prefer current behavior, or swapping to rapidjson/lunajson's method.
appgurueu commentedon Jul 1, 2023
I think throwing an error is the best course of action. A table with holes simply can't be represented properly with accepting the possibility of sparse tables being exponentially wasteful.