Skip to content

Commit

Permalink
Improve Builder performance
Browse files Browse the repository at this point in the history
  • Loading branch information
j8r committed Dec 2, 2018
1 parent eb0bfc5 commit 9b39f2d
Show file tree
Hide file tree
Showing 6 changed files with 85 additions and 66 deletions.
17 changes: 8 additions & 9 deletions README.md
Expand Up @@ -69,18 +69,17 @@ There are benchmarks comparing `CON` and the stdlib's `JSON` implementation
Some results:

```
CON.parse minified 636.54k ( 1.57µs) (± 3.02%) 1936 B/op fastest
CON.parse pretty 572.87k ( 1.75µs) (± 1.32%) 1936 B/op 1.11× slower
JSON.parse minified 562.57k ( 1.78µs) (± 1.80%) 2128 B/op 1.13× slower
JSON.parse pretty 477.41k ( 2.09µs) (± 3.85%) 2128 B/op 1.33× slower
CON.parse minified 403.83k ( 2.48µs) (± 4.67%) 1937 B/op fastest
CON.parse pretty 383.93k ( 2.6µs) (± 3.35%) 1936 B/op 1.05× slower
JSON.parse minified 322.33k ( 3.1µs) (± 3.63%) 2129 B/op 1.25× slower
JSON.parse pretty 277.7k ( 3.6µs) (± 3.82%) 2129 B/op 1.45× slower
```

```
CON::Builder minified 1.24M (807.52ns) (± 2.04%) 320 B/op fastest
CON::Builder pretty 674.25k ( 1.48µs) (±19.03%) 1057 B/op 1.84× slower
JSON::Builder minified 853.06k ( 1.17µs) (± 7.70%) 576 B/op 1.45× slower
JSON::Builder pretty 703.44k ( 1.42µs) (± 6.03%) 848 B/op 1.76× slower
#to_con 896.16k ( 1.12µs) (± 5.23%) 321 B/op fastest
#to_pretty_con 865.9k ( 1.15µs) (± 1.93%) 321 B/op 1.03× slower
#to_json 644.32k ( 1.55µs) (± 2.49%) 578 B/op 1.39× slower
#to_pretty_json 515.68k ( 1.94µs) (± 2.28%) 849 B/op 1.74× slower
```

## License
Expand Down
8 changes: 4 additions & 4 deletions benchmark/builder.cr
Expand Up @@ -3,19 +3,19 @@ require "json"
require "../src/any"

Benchmark.ips do |x|
x.report("CON::Builder minified") do
x.report("#to_con") do
DATA.to_con
end

x.report("CON::Builder pretty") do
x.report("#to_pretty_con") do
DATA.to_pretty_con
end

x.report("JSON::Builder minified") do
x.report("#to_json") do
DATA.to_json
end

x.report("JSON::Builder pretty") do
x.report("#to_pretty_json") do
DATA.to_pretty_json
end
end
29 changes: 17 additions & 12 deletions spec/builder_spec.cr
Expand Up @@ -104,11 +104,16 @@ describe CON::Builder do
end
end

it "writes hash with indent" do
assert_built(%< foo 1\n bar 2\n>, " ") do |con|
it "writes nested hash with indent" do
assert_built(%<foo 1\nbar {\n foobar 2\n sub {\n key nil\n }\n}\n>, " ") do |con|
hash do
field "foo", 1
field "bar", 2
hash "bar" do
field "foobar", 2
hash "sub" do
field "key", nil
end
end
end
end
end
Expand All @@ -127,18 +132,18 @@ describe CON::Builder do
end
end

it "writes nested array" do
assert_built(%<[[\n]\n]>, " ") do |con|
it "writes nested array with indent" do
assert_built(%<[[\n ]\n]>, " ") do |con|
array do
array do
end
end
end
end

it "writes hash with array and indent" do
assert_built(%<foo {\n[\n 1\n]\n}>, " ") do |con|
hash "foo" do
it "writes document with array and indent" do
assert_built(%<[\n 1\n]>, " ") do |con|
hash do
array do
value 1
end
Expand Down Expand Up @@ -199,15 +204,15 @@ describe CON::Builder do
it "errors on max nesting (object)" do
builder = CON::Builder.new IO::Memory.new
builder.max_nesting = 3
builder.hash do
builder.hash do
builder.hash do
builder.hash "a" do
builder.hash "a" do
builder.hash "a" do
end
end
end

expect_raises(CON::Builder::Error, "Nesting of 4 is too deep") do
builder.hash do
builder.hash "a" do
end
end
end
Expand Down
6 changes: 3 additions & 3 deletions spec/to_con_spec.cr
Expand Up @@ -137,11 +137,11 @@ describe "to_pretty_con" do
end

it "does for Hash" do
{"foo" => 1, "bar" => 2}.to_pretty_con.should eq " foo 1\n bar 2\n"
{"foo" => 1, "bar" => 2}.to_pretty_con.should eq "foo 1\nbar 2\n"
end

it "does for nested Hash" do
{"foo" => {"bar" => 1}}.to_pretty_con.should eq " foo {\n bar 1\n\n }\n"
{"foo" => {"bar" => 1}}.to_pretty_con.should eq "foo {\n bar 1\n}\n\n"
end

it "does for empty Hash" do
Expand All @@ -153,7 +153,7 @@ describe "to_pretty_con" do
end

it "does for nested Hash with indent" do
{"foo" => {"bar" => 1}}.to_pretty_con(indent: " ").should eq " foo {\n bar 1\n\n }\n"
{"foo" => {"bar" => 1}}.to_pretty_con(indent: " ").should eq "foo {\n bar 1\n}\n\n"
end

describe "Time" do
Expand Down
65 changes: 40 additions & 25 deletions src/builder.cr
Expand Up @@ -7,52 +7,43 @@ module CON

getter io : IO
property max_nesting : Int32 = 99
@nest = 0
@total_indent : String
@previous_total_indent : String?
@nest : Int32 = 0
@indent : String?
@root_document = false
@begin_hash = false
@begin_array = false

def initialize(@io : IO, @indent : String? = nil)
@total_indent = @indent || ""
@previous_total_indent = nil
# Not needed at the start/end of a document
@root_document = true
@begin_hash = true
end

protected def initialize(@io : IO, indent : String?, @total_indent : String, @nest : Int32)
# Increment the indentation, if any
if @indent = indent
@previous_total_indent = @total_indent
@total_indent += indent
end
protected def initialize(@io : IO, @indent : String?, @nest)
end

def field(key, value)
if @begin_array
raise CON::Builder::Error.new("Can't use field inside an array")
elsif @indent
@io << @total_indent
add_indent
elsif @begin_hash
@begin_hash = false
else
@io << ' '
end
key.to_s.to_con_key self
@io << ' '
value.to_con Builder.new(@io, @indent, @total_indent, @nest)
value.to_con Builder.new(@io, @indent, @nest)
@io << '\n' if @indent
end

def value(value)
if @begin_hash && !@root_document
raise CON::Builder::Error.new("Can't use value inside a hash")
elsif indent = @indent
@total_indent = indent if @total_indent.empty?
io << '\n' << @total_indent
elsif @indent
io << '\n'
add_indent
elsif @root_document
@begin_hash = true
@root_document = false
Expand All @@ -61,13 +52,13 @@ module CON
else
@io << ' '
end
value.to_con Builder.new(@io, @indent, @total_indent, @nest)
value.to_con Builder.new(@io, @indent, @nest)
end

private def key(value)
if @previous_total_indent
@io << '\n' << @previous_total_indent
elsif !@indent && !@begin_hash && !@begin_array
if @indent
add_indent
elsif !@begin_hash && !@begin_array
@io << ' '
end
value.to_con_key self
Expand All @@ -80,16 +71,22 @@ module CON
end

def array(&block)
increment_nest
array_nest = @nest
io << '['
increment_nest
@begin_array = true
@begin_hash = false
previous_root_document = @root_document
@root_document = false
yield
@root_document = previous_root_document
io << '\n' if @indent
io << @previous_total_indent << ']'
if @indent
@io << '\n'
array_nest.times do
@io << @indent
end
end
@io << ']'
end

def hash(key : String, &block)
Expand All @@ -102,16 +99,22 @@ module CON
end

def hash(&block)
increment_nest
@begin_hash = true
if @root_document
yield
else
hash_nest = @nest
increment_nest
@io << '{'
@io << '\n' if @indent
yield
if @indent
hash_nest.times do
@io << @indent
end
end
@io << '}'
@io << '\n' if @indent
@io << @previous_total_indent << '}'
end
@begin_hash = false
end
Expand All @@ -121,6 +124,18 @@ module CON
raise CON::Builder::Error.new("Nesting of #{@nest} is too deep")
end
end

private def add_indent
@nest.times do
@io << @indent
end
end

private def add_previous_indent
(@nest - 1).times do
@io << @indent
end
end
end

# Returns the resulting `String` of writing CON to the yielded `CON::Builder`.
Expand Down
26 changes: 13 additions & 13 deletions src/lexer.cr
Expand Up @@ -64,19 +64,19 @@ module CON::Lexer::Main
skip_whitespaces_and_comments
@buffer.clear
value = case @current_char
when '"' then next_char; consume_string
when '[' then next_char; Token::BeginArray
when ']' then next_char; Token::EndArray
when '{' then next_char; Token::BeginHash
when '}' then next_char; Token::EndHash
when 't' then consume_true
when 'f' then consume_false
when 'n' then consume_nil
when '-' then consume_int(negative: true)
when '0'..'9' then consume_int
when '\0' then Token::EOF
else raise "Unknown char: '#{@current_char}'"
end
when '"' then next_char; consume_string
when '[' then next_char; Token::BeginArray
when ']' then next_char; Token::EndArray
when '{' then next_char; Token::BeginHash
when '}' then next_char; Token::EndHash
when 't' then consume_true
when 'f' then consume_false
when 'n' then consume_nil
when '-' then consume_int(negative: true)
when '0'..'9' then consume_int
when '\0' then Token::EOF
else raise "Unknown char: '#{@current_char}'"
end
@column_number = 1
value
end
Expand Down

0 comments on commit 9b39f2d

Please sign in to comment.