Skip to content

Conversation

@jho406
Copy link
Contributor

@jho406 jho406 commented Jan 15, 2026

Performance improvements

PropsTemplate is fast, but can always be improved. This commit reduces the
amount of object allocations by removing cloning of options passed to an array.
Cloning options was convienent as it allows us somewhere to store multifeched
objects, expand procs, and store templates for faster template lookups, but
this can be expensive.

This commit removes that approach and instead stores multifecthed objected in
the cache object, and mutates the options for one thing (to store a template
for arrays). This change made Props::Base beat Panko making it the faster
JSON builder in ruby land. Props::Template itself benefited as well, and now
outpaces alba and turbostreamer.

Running benchmark with the following configuration:
YJIT: enabled
Oj.optimize_rails: enabled
-- create_table(:posts, {force: true})
   -> 0.0070s
-- create_table(:comments, {force: true})
   -> 0.0002s
-- create_table(:users, {force: true})
   -> 0.0006s
Checking outputs...

=== WITH GC ===
ruby 3.4.8 (2025-12-17 revision 995b59f666) +YJIT +PRISM [arm64-darwin25]
Warming up --------------------------------------
                alba    98.000 i/100ms
         alba_inline     3.000 i/100ms
               panko   137.000 i/100ms
      props_template   103.000 i/100ms
props_template_with_set
                       120.000 i/100ms
props_base_with_extensions
                       121.000 i/100ms
       turbostreamer    93.000 i/100ms
          props_base   138.000 i/100ms
     props_base_json   131.000 i/100ms
Calculating -------------------------------------
                alba    961.436 (± 0.8%) i/s    (1.04 ms/i) -      4.900k in   5.096913s
         alba_inline     12.574 (±23.9%) i/s   (79.53 ms/i) -     60.000 in   5.053625s
               panko      1.348k (± 1.0%) i/s  (741.75 μs/i) -      6.850k in   5.081588s
      props_template      1.024k (± 0.6%) i/s  (976.52 μs/i) -      5.150k in   5.029253s
props_template_with_set
                          1.184k (± 1.6%) i/s  (844.87 μs/i) -      6.000k in   5.070565s
props_base_with_extensions
                          1.195k (± 1.0%) i/s  (836.99 μs/i) -      6.050k in   5.064289s
       turbostreamer    917.855 (± 0.7%) i/s    (1.09 ms/i) -      4.650k in   5.066375s
          props_base      1.357k (± 0.8%) i/s  (736.83 μs/i) -      6.900k in   5.084468s
     props_base_json      1.298k (± 0.8%) i/s  (770.67 μs/i) -      6.550k in   5.048256s

Comparison:
          props_base:     1357.2 i/s
               panko:     1348.2 i/s - same-ish: difference falls within error
     props_base_json:     1297.6 i/s - 1.05x  slower
props_base_with_extensions:     1194.8 i/s - 1.14x  slower
props_template_with_set:     1183.6 i/s - 1.15x  slower
      props_template:     1024.0 i/s - 1.33x  slower
                alba:      961.4 i/s - 1.41x  slower
       turbostreamer:      917.9 i/s - 1.48x  slower
         alba_inline:       12.6 i/s - 107.93x  slower

=== WITHOUT GC ===
ruby 3.4.8 (2025-12-17 revision 995b59f666) +YJIT +PRISM [arm64-darwin25]
Warming up --------------------------------------
                alba    68.000 i/100ms
         alba_inline     1.000 i/100ms
               panko   132.000 i/100ms
      props_template    77.000 i/100ms
props_template_with_set
                       118.000 i/100ms
props_base_with_extensions
                       127.000 i/100ms
       turbostreamer    97.000 i/100ms
          props_base   147.000 i/100ms
     props_base_json   137.000 i/100ms
Calculating -------------------------------------
                alba    588.791 (±48.7%) i/s    (1.70 ms/i) -      2.380k in   5.174567s
         alba_inline      6.598 (± 0.0%) i/s  (151.56 ms/i) -     33.000 in   5.023939s
               panko      1.076k (±22.2%) i/s  (929.07 μs/i) -      5.148k in   5.035475s
      props_template    708.005 (±45.1%) i/s    (1.41 ms/i) -      2.926k in   5.147218s
props_template_with_set
                        873.069 (±45.4%) i/s    (1.15 ms/i) -      3.422k in   5.107466s
props_base_with_extensions
                        924.202 (±43.3%) i/s    (1.08 ms/i) -      3.683k in   5.196687s
       turbostreamer^[[C    732.770 (±39.3%) i/s    (1.36 ms/i) -      2.910k in   5.017891s
          props_base      1.058k (±45.0%) i/s  (945.05 μs/i) -      3.969k in   5.158039s
     props_base_json    998.945 (±44.3%) i/s    (1.00 ms/i) -      3.836k in   5.483344s

Comparison:
               panko:     1076.3 i/s
          props_base:     1058.1 i/s - same-ish: difference falls within error
     props_base_json:      998.9 i/s - same-ish: difference falls within error
props_base_with_extensions:      924.2 i/s - same-ish: difference falls within error
props_template_with_set:      873.1 i/s - same-ish: difference falls within error
       turbostreamer:      732.8 i/s - same-ish: difference falls within error
      props_template:      708.0 i/s - same-ish: difference falls within error
                alba:      588.8 i/s - same-ish: difference falls within error
         alba_inline:        6.6 i/s - 163.14x  slower

Calculating -------------------------------------
                alba   833.929k memsize (     0.000  retained)
                         9.804k objects (     0.000  retained)
                         6.000  strings (     0.000  retained)
         alba_inline     2.817M memsize (     0.000  retained)
                        22.204k objects (     0.000  retained)
                        38.000  strings (     0.000  retained)
               panko   259.178k memsize (     0.000  retained)
                         3.033k objects (     0.000  retained)
                         2.000  strings (     0.000  retained)
      props_template   457.698k memsize (     0.000  retained)
                         7.718k objects (     0.000  retained)
                         6.000  strings (     0.000  retained)
props_template_with_set
                       457.698k memsize (     0.000  retained)
                         7.718k objects (     0.000  retained)
                         6.000  strings (     0.000  retained)
props_base_with_extensions
                       457.578k memsize (     0.000  retained)
                         7.716k objects (     0.000  retained)
                         6.000  strings (     0.000  retained)
       turbostreamer   641.720k memsize (     0.000  retained)
                         9.741k objects (     0.000  retained)
                        33.000  strings (     0.000  retained)
          props_base   428.858k memsize (     0.000  retained)
                         7.406k objects (     0.000  retained)
                         6.000  strings (     0.000  retained)
     props_base_json   642.985k memsize (     0.000  retained)
                         5.308k objects (     0.000  retained)
                         2.000  strings (     0.000  retained)

Comparison:
               panko:     259178 allocated
          props_base:     428858 allocated - 1.65x more
props_base_with_extensions:     457578 allocated - 1.77x more
      props_template:     457698 allocated - 1.77x more
props_template_with_set:     457698 allocated - 1.77x more
       turbostreamer:     641720 allocated - 2.48x more
     props_base_json:     642985 allocated - 2.48x more
                alba:     833929 allocated - 3.22x more
         alba_inline:    2816849 allocated - 10.87x more
Ruby version: 3.4.8
Oj version: 3.16.13

PropsTemplate is fast, but can always be improved. This commit reduces the
amount of object allocations by removing cloning of options passed to an array.
Cloning options was convienent as it allows us somewhere to store multifeched
objects, expand procs, and store templates for faster template lookups, but
this can be expensive.

This commit removes that approach and instead stores multifecthed objected in
the cache object, and mutates the options for one thing (to store a template
for arrays). This change made Props::Base beat Panko making it the faster
JSON builder in ruby land. Props::Template itself benefited as well, and now
outpaces alba and turbostreamer.

Running benchmark with the following configuration:
YJIT: enabled
Oj.optimize_rails: enabled
-- create_table(:posts, {force: true})
   -> 0.0070s
-- create_table(:comments, {force: true})
   -> 0.0002s
-- create_table(:users, {force: true})
   -> 0.0006s
Checking outputs...

=== WITH GC ===
ruby 3.4.8 (2025-12-17 revision 995b59f666) +YJIT +PRISM [arm64-darwin25]
Warming up --------------------------------------
                alba    98.000 i/100ms
         alba_inline     3.000 i/100ms
               panko   137.000 i/100ms
      props_template   103.000 i/100ms
props_template_with_set
                       120.000 i/100ms
props_base_with_extensions
                       121.000 i/100ms
       turbostreamer    93.000 i/100ms
          props_base   138.000 i/100ms
     props_base_json   131.000 i/100ms
Calculating -------------------------------------
                alba    961.436 (± 0.8%) i/s    (1.04 ms/i) -      4.900k in   5.096913s
         alba_inline     12.574 (±23.9%) i/s   (79.53 ms/i) -     60.000 in   5.053625s
               panko      1.348k (± 1.0%) i/s  (741.75 μs/i) -      6.850k in   5.081588s
      props_template      1.024k (± 0.6%) i/s  (976.52 μs/i) -      5.150k in   5.029253s
props_template_with_set
                          1.184k (± 1.6%) i/s  (844.87 μs/i) -      6.000k in   5.070565s
props_base_with_extensions
                          1.195k (± 1.0%) i/s  (836.99 μs/i) -      6.050k in   5.064289s
       turbostreamer    917.855 (± 0.7%) i/s    (1.09 ms/i) -      4.650k in   5.066375s
          props_base      1.357k (± 0.8%) i/s  (736.83 μs/i) -      6.900k in   5.084468s
     props_base_json      1.298k (± 0.8%) i/s  (770.67 μs/i) -      6.550k in   5.048256s

Comparison:
          props_base:     1357.2 i/s
               panko:     1348.2 i/s - same-ish: difference falls within error
     props_base_json:     1297.6 i/s - 1.05x  slower
props_base_with_extensions:     1194.8 i/s - 1.14x  slower
props_template_with_set:     1183.6 i/s - 1.15x  slower
      props_template:     1024.0 i/s - 1.33x  slower
                alba:      961.4 i/s - 1.41x  slower
       turbostreamer:      917.9 i/s - 1.48x  slower
         alba_inline:       12.6 i/s - 107.93x  slower

=== WITHOUT GC ===
ruby 3.4.8 (2025-12-17 revision 995b59f666) +YJIT +PRISM [arm64-darwin25]
Warming up --------------------------------------
                alba    68.000 i/100ms
         alba_inline     1.000 i/100ms
               panko   132.000 i/100ms
      props_template    77.000 i/100ms
props_template_with_set
                       118.000 i/100ms
props_base_with_extensions
                       127.000 i/100ms
       turbostreamer    97.000 i/100ms
          props_base   147.000 i/100ms
     props_base_json   137.000 i/100ms
Calculating -------------------------------------
                alba    588.791 (±48.7%) i/s    (1.70 ms/i) -      2.380k in   5.174567s
         alba_inline      6.598 (± 0.0%) i/s  (151.56 ms/i) -     33.000 in   5.023939s
               panko      1.076k (±22.2%) i/s  (929.07 μs/i) -      5.148k in   5.035475s
      props_template    708.005 (±45.1%) i/s    (1.41 ms/i) -      2.926k in   5.147218s
props_template_with_set
                        873.069 (±45.4%) i/s    (1.15 ms/i) -      3.422k in   5.107466s
props_base_with_extensions
                        924.202 (±43.3%) i/s    (1.08 ms/i) -      3.683k in   5.196687s
       turbostreamer^[[C    732.770 (±39.3%) i/s    (1.36 ms/i) -      2.910k in   5.017891s
          props_base      1.058k (±45.0%) i/s  (945.05 μs/i) -      3.969k in   5.158039s
     props_base_json    998.945 (±44.3%) i/s    (1.00 ms/i) -      3.836k in   5.483344s

Comparison:
               panko:     1076.3 i/s
          props_base:     1058.1 i/s - same-ish: difference falls within error
     props_base_json:      998.9 i/s - same-ish: difference falls within error
props_base_with_extensions:      924.2 i/s - same-ish: difference falls within error
props_template_with_set:      873.1 i/s - same-ish: difference falls within error
       turbostreamer:      732.8 i/s - same-ish: difference falls within error
      props_template:      708.0 i/s - same-ish: difference falls within error
                alba:      588.8 i/s - same-ish: difference falls within error
         alba_inline:        6.6 i/s - 163.14x  slower

Calculating -------------------------------------
                alba   833.929k memsize (     0.000  retained)
                         9.804k objects (     0.000  retained)
                         6.000  strings (     0.000  retained)
         alba_inline     2.817M memsize (     0.000  retained)
                        22.204k objects (     0.000  retained)
                        38.000  strings (     0.000  retained)
               panko   259.178k memsize (     0.000  retained)
                         3.033k objects (     0.000  retained)
                         2.000  strings (     0.000  retained)
      props_template   457.698k memsize (     0.000  retained)
                         7.718k objects (     0.000  retained)
                         6.000  strings (     0.000  retained)
props_template_with_set
                       457.698k memsize (     0.000  retained)
                         7.718k objects (     0.000  retained)
                         6.000  strings (     0.000  retained)
props_base_with_extensions
                       457.578k memsize (     0.000  retained)
                         7.716k objects (     0.000  retained)
                         6.000  strings (     0.000  retained)
       turbostreamer   641.720k memsize (     0.000  retained)
                         9.741k objects (     0.000  retained)
                        33.000  strings (     0.000  retained)
          props_base   428.858k memsize (     0.000  retained)
                         7.406k objects (     0.000  retained)
                         6.000  strings (     0.000  retained)
     props_base_json   642.985k memsize (     0.000  retained)
                         5.308k objects (     0.000  retained)
                         2.000  strings (     0.000  retained)

Comparison:
               panko:     259178 allocated
          props_base:     428858 allocated - 1.65x more
props_base_with_extensions:     457578 allocated - 1.77x more
      props_template:     457698 allocated - 1.77x more
props_template_with_set:     457698 allocated - 1.77x more
       turbostreamer:     641720 allocated - 2.48x more
     props_base_json:     642985 allocated - 2.48x more
                alba:     833929 allocated - 3.22x more
         alba_inline:    2816849 allocated - 10.87x more
Ruby version: 3.4.8
Oj version: 3.16.13
For large caches, this prevents split from reading the entire string.
@jho406 jho406 merged commit 52834bf into main Jan 16, 2026
24 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants