Skip to content

Instantly share code, notes, and snippets.

@connorshea
Last active July 2, 2026 22:52
Show Gist options
  • Select an option

  • Save connorshea/2f5b785a5fdf24b4ffaa5693f26476ef to your computer and use it in GitHub Desktop.

Select an option

Save connorshea/2f5b785a5fdf24b4ffaa5693f26476ef to your computer and use it in GitHub Desktop.
perf improvements in faker thanks to the various PRs I've opened today

Combined perf-PR benchmark (PRs #3289–#3294)

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.

Environment

  • 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)

Wall time (benchmark-ips)

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.

Allocations (memory_profiler, per call)

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%

Notes

  • The biggest single contributor is the Config.locale lookup fix (#3290): on main every translate/fetch call rebuilds the entire I18n.available_locales array, which is why allocation counts on main are hundreds of objects per call for translate-backed generators. The other PRs compound on top of it — e.g. PhoneNumber.phone_number measured against main with 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 touch translate, so they only benefit from their own PR; their allocation profiles still drop 70–88%.

Benchmark scripts

Wall time

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('###-###-####') }
end

Allocations

require '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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment