Branch: claude/perf-combined (04e717e8) — an octopus merge of the six
open perf PRs into main (9b078038):
| PR | Branch | Change |
|---|---|---|
| #3290 | claude/perf-locale-lookup |
Memoized I18n.locale_available? in Config.locale |
| #3294 | claude/perf-numerify |
Single-pass, per-digit-allocation-free Base.numerify |
| #3289 | claude/perf-fetch-parse |
Fast paths in Base.fetch/fetch_all/parse, Name.first_name |
| #3293 | claude/perf-number |
Arithmetic instead of string building in Number |
| #3292 | claude/perf-lorem-words |
Avoid word-list copies in Lorem.words |
| #3291 | claude/perf-internet |
Optimized username/email/password/base64 |
The merged tree is byte-identical to the original optimization branch
minus the two changes without PRs (Alphanumeric, Char helpers).
Full test suite on the merged branch: 2179 tests, 272,979 assertions,
0 failures, 0 errors.
- Ruby 3.4.9 (arm64-darwin25)
- benchmark-ips 2.15.1 (warmup 2s / measure 5s per entry)
- memory_profiler 1.1.0 (per call, averaged over 1000 calls)
| Benchmark | main (i/s) | combined (i/s) | Speedup |
|---|---|---|---|
Name.first_name |
28.516k (±1.0%) | 182.401k (±0.8%) | 6.4x |
Name.name |
17.696k (±1.3%) | 79.916k (±1.7%) | 4.5x |
Address.city |
20.381k (±1.0%) | 84.599k (±3.7%) | 4.2x |
Address.full_address |
7.014k (±2.1%) | 23.828k (±3.0%) | 3.4x |
Lorem.word |
121.195k (±2.3%) | 435.156k (±3.0%) | 3.6x |
Lorem.words(number: 4) |
122.186k (±2.1%) | 429.163k (±3.4%) | 3.5x |
Lorem.sentence |
41.440k (±0.6%) | 134.481k (±2.7%) | 3.2x |
PhoneNumber.phone_number |
99.174k (±0.6%) | 236.551k (±3.3%) | 2.4x |
Internet.username |
40.601k (±1.1%) | 117.966k (±3.7%) | 2.9x |
Internet.email |
21.015k (±0.7%) | 42.770k (±4.4%) | 2.0x |
Internet.password |
405.010k (±0.5%) | 432.914k (±2.4%) | 1.07x |
Number.number(digits: 10) |
511.696k (±2.0%) | 4.764M (±0.9%) | 9.3x |
Number.decimal(l_digits: 2) |
887.261k (±1.1%) | 1.628M (±2.1%) | 1.8x |
numerify('###-###-####') |
480.796k (±1.0%) | 600.401k (±0.9%) | 1.25x |
Geometric mean across the 14 entries: ~3.0x.
| Benchmark | main | combined | Reduction |
|---|---|---|---|
Name.first_name |
278.0 obj / 16,008 B | 19.0 obj / 1,843 B | -93% obj / -88% B |
Name.name |
449.9 obj / 25,741 B | 48.6 obj / 4,308 B | -89% / -83% |
Address.city |
394.7 obj / 22,683 B | 46.0 obj / 4,133 B | -88% / -82% |
Address.full_address |
1,158.3 obj / 66,649 B | 178.0 obj / 15,471 B | -85% / -77% |
Lorem.word |
68.0 obj / 7,784 B | 5.0 obj / 600 B | -93% / -92% |
Lorem.words(number: 4) |
68.0 obj / 7,824 B | 5.0 obj / 640 B | -93% / -92% |
Lorem.sentence |
201.0 obj / 15,438 B | 18.0 obj / 2,095 B | -91% / -86% |
PhoneNumber.phone_number |
92.0 obj / 5,096 B | 8.0 obj / 1,056 B | -91% / -79% |
Internet.username |
99.4 obj / 8,780 B | 29.1 obj / 2,484 B | -71% / -72% |
Internet.email |
263.3 obj / 19,788 B | 87.1 obj / 7,209 B | -67% / -64% |
Internet.password |
20.0 obj / 2,400 B | 6.0 obj / 1,184 B | -70% / -51% |
Number.number(digits: 10) |
14.0 obj / 920 B | 1.0 obj / 40 B | -93% / -96% |
Number.decimal(l_digits: 2) |
13.0 obj / 560 B | 5.0 obj / 200 B | -62% / -64% |
numerify('###-###-####') |
25.0 obj / 1,296 B | 3.0 obj / 416 B | -88% / -68% |
- The biggest single contributor is the
Config.localelookup fix (#3290): onmaineverytranslate/fetchcall rebuilds the entireI18n.available_localesarray, which is why allocation counts onmainare hundreds of objects per call for translate-backed generators. The other PRs compound on top of it — e.g.PhoneNumber.phone_numbermeasured againstmainwith only the numerify change was 70 obj/call; with all six changes it is 8. - These changes compound multiplicatively because they sit at different layers (locale lookup → fetch/parse → numerify → generator-specific).
- The two effectively-flat wall-time rows (
Internet.password,numerify) don't touchtranslate, so they only benefit from their own PR; their allocation profiles still drop 70–88%.
require 'benchmark/ips'
require 'faker'
Benchmark.ips do |x|
x.config(warmup: 2, time: 5)
x.report('Name.first_name') { Faker::Name.first_name }
x.report('Name.name') { Faker::Name.name }
x.report('Address.city') { Faker::Address.city }
x.report('Address.full_address') { Faker::Address.full_address }
x.report('Lorem.word') { Faker::Lorem.word }
x.report('Lorem.words(number: 4)') { Faker::Lorem.words(number: 4) }
x.report('Lorem.sentence') { Faker::Lorem.sentence }
x.report('PhoneNumber.phone_number') { Faker::PhoneNumber.phone_number }
x.report('Internet.username') { Faker::Internet.username }
x.report('Internet.email') { Faker::Internet.email }
x.report('Internet.password') { Faker::Internet.password }
x.report('Number.number(digits: 10)') { Faker::Number.number(digits: 10) }
x.report('Number.decimal(l_digits: 2)') { Faker::Number.decimal(l_digits: 2) }
x.report("numerify('###-###-####')") { Faker::Base.numerify('###-###-####') }
endrequire 'memory_profiler'
require 'faker'
BENCHES = {
'Name.first_name' => -> { Faker::Name.first_name },
'Name.name' => -> { Faker::Name.name },
'Address.city' => -> { Faker::Address.city },
'Address.full_address' => -> { Faker::Address.full_address },
'Lorem.word' => -> { Faker::Lorem.word },
'Lorem.words(number: 4)' => -> { Faker::Lorem.words(number: 4) },
'Lorem.sentence' => -> { Faker::Lorem.sentence },
'PhoneNumber.phone_number' => -> { Faker::PhoneNumber.phone_number },
'Internet.username' => -> { Faker::Internet.username },
'Internet.email' => -> { Faker::Internet.email },
'Internet.password' => -> { Faker::Internet.password },
'Number.number(digits: 10)' => -> { Faker::Number.number(digits: 10) },
'Number.decimal(l_digits: 2)' => -> { Faker::Number.decimal(l_digits: 2) },
"numerify('###-###-####')" => -> { Faker::Base.numerify('###-###-####') }
}.freeze
# Warm up I18n translation caches so they don't count against the first bench
BENCHES.each_value(&:call)
N = 1000
BENCHES.each do |name, blk|
report = MemoryProfiler.report { N.times { blk.call } }
puts format('%-30s per call: %8.1f objects / %10.1f bytes',
name,
report.total_allocated.fdiv(N),
report.total_allocated_memsize.fdiv(N))
end