14
14
end
15
15
16
16
def implementations ( ruby_obj )
17
+ state = JSON ::State . new ( JSON . dump_default_options )
18
+
17
19
{
18
20
json : [ "json" , proc { JSON . dump ( ruby_obj ) } ] ,
21
+ json_state : [ "json (reuse)" , proc { state . generate ( ruby_obj ) } ] ,
19
22
oj : [ "oj" , proc { Oj . dump ( ruby_obj ) } ] ,
20
23
rapidjson : [ "rapidjson" , proc { RapidJSON . dump ( ruby_obj ) } ] ,
21
24
}
22
25
end
23
26
24
- def benchmark_encoding ( benchmark_name , ruby_obj , check_expected : true )
27
+ def benchmark_encoding ( benchmark_name , ruby_obj , check_expected : true , except : [ ] )
25
28
json_output = JSON . dump ( ruby_obj )
26
29
puts "== Encoding #{ benchmark_name } (#{ json_output . bytesize } bytes)"
27
30
31
+ impls = implementations ( ruby_obj ) . select { |name | RUN [ name ] }
32
+ except . each { |i | impls . delete ( i ) }
33
+
28
34
Benchmark . ips do |x |
29
35
expected = ::JSON . dump ( ruby_obj ) if check_expected
30
- implementations ( ruby_obj ) . select { | name | RUN [ name ] } . values . each do |name , block |
36
+ impls . values . each do |name , block |
31
37
begin
32
38
result = block . call
33
39
if check_expected && expected != result
@@ -45,9 +51,26 @@ def benchmark_encoding(benchmark_name, ruby_obj, check_expected: true)
45
51
puts
46
52
end
47
53
54
+ # On the first two micro benchmarks, the limitting factor is that we have to create a Generator::State object for every
55
+ # call to `JSON.dump`, so we cause 2 allocations per call where alternatives only do one allocation.
56
+ # The performance difference is mostly more time spent in GC because of this extra pressure.
57
+ # If we re-use the same `JSON::State` instance, we're faster than Oj on the array benchmark, and much closer
58
+ # on the Hash one.
48
59
benchmark_encoding "small nested array" , [ [ 1 , 2 , 3 , 4 , 5 ] ] *10
49
60
benchmark_encoding "small hash" , { "username" => "jhawthorn" , "id" => 123 , "event" => "wrote json serializer" }
50
- benchmark_encoding "twitter.json" , JSON . load_file ( "#{ __dir__ } /data/twitter.json" )
51
- benchmark_encoding "citm_catalog.json" , JSON . load_file ( "#{ __dir__ } /data/citm_catalog.json" )
52
- benchmark_encoding "canada.json" , JSON . load_file ( "#{ __dir__ } /data/canada.json" ) , check_expected : false
53
- benchmark_encoding "many #to_json calls" , [ { Object . new => Object . new , 12 => 54.3 , Integer => Float , Time . now => Date . today } ] * 20
61
+
62
+ # On these two benchmark we perform well.
63
+ benchmark_encoding "twitter.json" , JSON . load_file ( "#{ __dir__ } /data/twitter.json" ) , except : %i( json_state )
64
+ benchmark_encoding "citm_catalog.json" , JSON . load_file ( "#{ __dir__ } /data/citm_catalog.json" ) , except : %i( json_state )
65
+
66
+ # This benchmark spent the overwhelming majority of its time in `ruby_dtoa`. We rely on Ruby's implementation
67
+ # which uses a relatively old version of dtoa.c from David M. Gay.
68
+ # Oj is noticeably faster here because it limits the precision of floats, breaking roundtriping. That's not
69
+ # something we should emulate.
70
+ # Since a few years there are now much faster float to string implementations such as Ryu, Dragonbox, etc,
71
+ # but all these are implemented in C++11 or newer, making it hard if not impossible to include them.
72
+ # Short of a pure C99 implementation of these newer algorithms, there isn't much that can be done to match
73
+ # Oj speed without losing precision.
74
+ benchmark_encoding "canada.json" , JSON . load_file ( "#{ __dir__ } /data/canada.json" ) , check_expected : false , except : %i( json_state )
75
+
76
+ benchmark_encoding "many #to_json calls" , [ { Object . new => Object . new , 12 => 54.3 , Integer => Float , Time . now => Date . today } ] * 20 , except : %i( json_state )
0 commit comments