Skip to content

Commit 96cc75f

Browse files
authored
Merge pull request #17 from aminya/despacer
2 parents 2b912a1 + 69a0660 commit 96cc75f

File tree

9 files changed

+127
-15
lines changed

9 files changed

+127
-15
lines changed

.github/workflows/CI.yml

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,21 @@ jobs:
2222
- 6
2323
clang:
2424
- 12
25+
cmake:
26+
- 3.20.2
27+
ninja:
28+
- 1.10.2
29+
CC:
30+
- clang
31+
CXX:
32+
- clang++
33+
env:
34+
CC: ${{ matrix.CC }}
35+
CXX: ${{ matrix.CXX }}
2536
steps:
2637
- uses: actions/checkout@v2
38+
with:
39+
submodules: 'true'
2740

2841
# Cache
2942
- name: Cache
@@ -38,17 +51,24 @@ jobs:
3851
./.dub
3952
./llvm
4053
C:/Program Files/LLVM
41-
key: "cache-D:${{ matrix.d }}-OS:${{ matrix.os }}-Clang:${{ matrix.clang }}"
54+
key: "cache-OS:${{ matrix.os }}-D:${{ matrix.d }}-Clang:${{ matrix.clang }}-dub:${{ hashFiles('./dub.selections.json')}}-pnpm:${{ hashFiles('./pnpm-lock.yaml') }}"
55+
restore-keys: |
56+
"cache-OS:${{ matrix.os }}-D:${{ matrix.d }}-Clang:${{ matrix.clang }}"
4257
4358
# Setup compilers and tools
4459

4560
- name: Setup LLVM
46-
if: contains(matrix.os, 'ubuntu') && matrix.clang
4761
uses: KyleMayes/install-llvm-action@v1
4862
with:
4963
version: ${{ matrix.clang }}
5064
cached: ${{ steps.cache.outputs.cache-hit }}
5165

66+
- name: Setup Cmake and Ninja
67+
uses: aminya/install-cmake@new-versions-and-arch
68+
with:
69+
cmake: ${{ matrix.cmake }}
70+
ninja: ${{ matrix.ninja }}
71+
5272
- name: Setup Node
5373
uses: actions/setup-node@v2
5474
with:

.gitmodules

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[submodule "src/native/despacer"]
2+
path = src/native/despacer
3+
url = https://github.com/aminya/despacer
4+
branch = minijson

Readme.md

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
Minify JSON files **blazing fast**! Supports Comments. Written in D.
44

5-
385 times faster than jsonminify!
5+
4180 times faster than jsonminify!
66

77
[![CI](https://github.com/aminya/minijson/actions/workflows/CI.yml/badge.svg)](https://github.com/aminya/minijson/actions/workflows/CI.yml)
88

@@ -23,6 +23,7 @@ https://github.com/aminya/minijson/releases/tag/v0.5.1
2323
- Dub
2424

2525
```
26+
git submodule update --init --recursive
2627
dub build --config=library --build=release-nobounds --compiler=ldc2
2728
# or
2829
dub build --config=executable --build=release-nobounds --compiler=ldc2
@@ -90,12 +91,21 @@ minifyFiles(["file1.json", "file2.json"], true);
9091

9192
On AMD Ryzen 7 4800H:
9293

94+
- minifyString: minijson is 4178 times faster than jsonMinify
95+
- minifyFiles: minijson is 1198 times faster than jsonMinify.
96+
9397
```
94-
❯ node .\benchmark\native-benchmark.mjs
95-
0.152 seconds
98+
❯ .\dist\minijson-benchmark.exe --benchmark-minifyString --benchmark-minifyFiles
99+
Benchmark minifyFiles
100+
49 ms
101+
Benchmark minifyString
102+
14 ms
96103
97104
❯ node .\benchmark\js-benchmark.mjs
98-
58.818 seconds
105+
Benchmark minifyString
106+
58.502 seconds
107+
Benchmark minifyFiles
108+
58.703 seconds
99109
```
100110

101111
### Contributing

benchmark/benchmark.d

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ void main(string[] args)
99
{
1010
bool benchmarkMinifyFiles = false;
1111
bool benchmarkMinifyString = true;
12+
bool benchmarkParallelMinifyString = false;
1213

13-
getopt(args, "benchmark-minifyFiles", &benchmarkMinifyFiles, "benchmark-minifyString", &benchmarkMinifyString);
14+
getopt(args, "benchmark-minifyFiles", &benchmarkMinifyFiles, "benchmark-minifyString",
15+
&benchmarkMinifyString, "benchmark-parallel-minifyString", &benchmarkParallelMinifyString);
1416

1517
const string[] files = dirEntries("./test/fixtures/standard", SpanMode.shallow).map!(entry => entry.name).array();
1618

@@ -36,7 +38,7 @@ void main(string[] args)
3638

3739
if (benchmarkMinifyString)
3840
{
39-
writeln("Benchmark minifyString");
41+
writeln("Benchmark minifyString single-threaded");
4042
const repeat = 120;
4143
auto repeater = iota(repeat);
4244
string tmp;
@@ -52,5 +54,23 @@ void main(string[] args)
5254
result = sw.peek();
5355

5456
writeln(result / repeat);
57+
58+
if (benchmarkParallelMinifyString)
59+
{
60+
writeln("Benchmark minifyString multi-threaded");
61+
auto repeater2 = iota(repeat);
62+
63+
sw.reset();
64+
foreach (_; repeater2)
65+
{
66+
foreach (fileContent; filesContent.parallel())
67+
{
68+
tmp = minifyString(fileContent);
69+
}
70+
}
71+
result = sw.peek();
72+
73+
writeln(result / repeat);
74+
}
5575
}
5676
}

benchmark/js-benchmark.mjs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,25 @@ import { standardFiles } from "../test/fixtures.mjs"
77
// warmup
88
const tmp = await jsonMinify("{}")
99

10+
console.log("Benchmark minifyString")
11+
12+
const filesContents = await Promise.all(
13+
standardFiles.map(async (jsonFile) => {
14+
return readFile(jsonFile, "utf8")
15+
})
16+
)
17+
18+
const t11 = performance.now()
19+
20+
for (const fileContent of filesContents) {
21+
const data = jsonMinify(fileContent)
22+
}
23+
24+
const t22 = performance.now()
25+
console.log(((t22 - t11) / 1000).toFixed(3), "seconds")
26+
27+
console.log("Benchmark minifyFiles")
28+
1029
const t1 = performance.now()
1130

1231
await Promise.all(

dub.sdl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ sourcePaths "./src/native"
1010
importPaths "./src/native"
1111

1212
dependency "automem" version="~>0.6.6"
13+
preGenerateCommands "git submodule update --init" # despacer download
14+
dependency "despacer" path="./src/native/despacer/bindings/d"
1315

1416
configuration "executable" {
1517
targetType "executable"

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
"build.release": "pnpm build -- --build release-nobounds --compiler=ldc2",
2424
"build.profile": "pnpm build -- --build profile --compiler=ldc2 && node ./src/node/build.js && npm run build.node.js",
2525
"build.benchmark": "dub build --config=benchmark --build release-nobounds --compiler=ldc2",
26-
"start.profile": "shx rm -rf ./trace.* && npm run start.benchmark && profdump.exe --dot trace.log trace.dot && dot -Tsvg trace.dot -o trace.svg && ./trace.svg",
26+
"start.profile": "shx rm -rf ./trace.* && npm run start.benchmark.node && profdump.exe --dot trace.log trace.dot && dot -Tsvg trace.dot -o trace.svg && ./trace.svg",
2727
"build.node": "npm run build.release && node ./src/node/build.js && npm run build.node.js",
2828
"build.node.js": "tsc -p ./src/node/tsconfig.json",
2929
"build.wasm": "ldc2 ./src/wasm/wasm.d ./src/native/lib.d --od ./dist --O3 --mtriple=wasm32-unknown-unknown-wasm",

src/native/despacer

Submodule despacer added at f1acbdb

src/native/lib.d

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
module minijson.lib;
22

3-
import std : ctRegex, replaceAll, join, array, matchAll, matchFirst, RegexMatch;
3+
import std : ctRegex, matchAll, matchFirst, toStringz;
4+
5+
import despacer.simd_check : supports_sse4_1, supports_avx2;
46

57
const tokenizerWithComment = ctRegex!(`"|(/\*)|(\*/)|(//)|\n|\r|\[|]`, "g");
68
const tokenizerNoComment = ctRegex!(`[\n\r"[]]`, "g");
79

8-
const spaceOrBreakRegex = ctRegex!(`\s`);
9-
1010
/**
1111
Minify the given JSON string
1212
@@ -49,7 +49,7 @@ string minifyString(in string jsonString, in bool hasComment = false) @trusted
4949
const noLeftContext = leftContextSubstr.length == 0;
5050
if (!in_string && !noLeftContext)
5151
{
52-
leftContextSubstr = leftContextSubstr.replaceAll(spaceOrBreakRegex, "");
52+
leftContextSubstr = remove_spaces(leftContextSubstr);
5353
}
5454
if (!noLeftContext)
5555
{
@@ -122,9 +122,45 @@ private bool hasNoSlashOrEvenNumberOfSlashes(in string leftContextSubstr) @safe
122122
return slashCount % 2 == 0;
123123
}
124124

125-
private bool notSlashAndNoSpaceOrBreak(in string matchFrontHit) @safe
125+
private bool notSlashAndNoSpaceOrBreak(const ref string matchFrontHit) @safe
126+
{
127+
return matchFrontHit != "\"" && hasNoSpace(matchFrontHit);
128+
}
129+
130+
/** Removes spaces from the original string */
131+
private string remove_spaces(string str) @trusted nothrow
132+
{
133+
static if (supports_sse4_1())
134+
{
135+
import despacer.despacer : sse4_despace_branchless_u4;
136+
137+
// this wrapper reduces the overall time by 15 compared to d_sse4_despace_branchless_u4 because of no dup and toStringz
138+
auto cstr = cast(char*) str;
139+
const length = str.length;
140+
return str[0 .. sse4_despace_branchless_u4(cstr, length)];
141+
}
142+
else
143+
{
144+
const spaceOrBreakRegex = ctRegex!(`\s`);
145+
leftContextSubstr.replaceAll(spaceOrBreakRegex, "");
146+
}
147+
}
148+
149+
/** Check if the given string has space */
150+
private bool hasNoSpace(const ref string matchFrontHit) @trusted
126151
{
127-
return matchFrontHit != "\"" && matchFrontHit.matchFirst(spaceOrBreakRegex).empty();
152+
static if (supports_avx2())
153+
{
154+
import despacer.despacer : avx2_hasspace;
155+
156+
// the algorithm never checks for zero termination so toStringz is not needed
157+
return !avx2_hasspace(cast(const char*) matchFrontHit, matchFrontHit.length);
158+
}
159+
else
160+
{
161+
const spaceOrBreakRegex = ctRegex!(`\s`);
162+
return matchFrontHit.matchFirst(spaceOrBreakRegex).empty();
163+
}
128164
}
129165

130166
/**

0 commit comments

Comments
 (0)