Yet again instead of tweets, a blog post. The backlog got out of hand – 40 of them this time.
Usual caveat: every number below is whatever the author measured on their own machine with their own workload. Some are microbenchmarks. Don’t compare them against each other, and don’t assume they’ll show up in your app. Click through if you care about methodology.
Jean Boussier shows up often enough that he gets a section instead of bullets scattered through the post.
Monitor#synchronize goes from about 19.8M to 23.7M calls a second; a plain Mutex manages around 25M on the same machine, so most of the gap is closed.File.read used to allocate a buffer, hit EOF, then enlarge and issue a second read – on the happy path, every time. Read one extra byte up front and the common case stops over-allocating. ObjectSpace.memsize_of on a 10-byte file drops from 1075 bytes retained to 40.Six ZJIT and YJIT PRs from Takashi Kokubun in this batch.
fib 5x, optcarrot 1.5x, liquid-render 14%, activerecord 5.8%.optcarrot at the current --zjit-max-versions=2 default.optcarrot 1384.4ms → 1064.3ms.invokeblock finally gets the specialize-and-compile treatment that send, getivar and setivar already had. chunky-png 17%, a loops-times microbenchmark 2.28x.psych-load regression fixed by restoring an inline shape-transition write for the common case, an embedded object whose next shape keeps the same capacity. 1152.4ms → 980.8ms.Two more from outside the JIT team.
jmp_ptr_bytes() was reserving five instructions per patch point on arm64, when a single branch there reaches ±128MiB – comfortably past the 64MiB default code region. Trim the reservation to what’s actually needed: a branch-heavy while loop 1.70x faster, a 3-way if/elsif loop 1.23x.string-dup 277.3ms → 132.8ms, string-dup-chilled 277.5ms → 131.2ms.super dispatch onto the stack, fixing a race where two Ractors could fight over the shared copy. Side effect: super calls passing keyword arguments are 2.20x faster, since they no longer need a heap-allocated calldata. Plain positional super comes out marginally slower on his machine.block.call falling back to a slower dispatch path than yield, despite the two costing about the same in principle. No arguments: 32.95M → 57.37M i/s. Two arguments: 31.25M → 51.62M i/s. Both land right next to yield‘s own numbers.Three from Koichi Sasada, all aimed at the same problem: Ractors used to make garbage collection worse the more of them you ran.
spawn+join throughput goes 6,224/s → 33,666/s.String#inspect so it can skip runs of unescaped ASCII rather than decoding character by character. 12.39x on ASCII text, 3.45x on mixed content. UTF-8 and binary strings unaffected; a fully escape-heavy string comes out about 5% slower.Array#join can memcpy straight into the result buffer instead of negotiating encodings element by element. Up to 2.94x on a 100,000-element array, 2.48x with no separator at all. Yaroslav Markin.#upcase and #downcase had an ASCII byte-loop fast path. #capitalize didn’t, which left it roughly 10x slower than it needed to be on the same input – 5.89x faster on a single character once fixed, 12.70x on a 1000-character string.String#ascii_only? and #valid_encoding? as leaf builtins so they skip the CFUNC frame push. 1.2-1.4x in the plain interpreter, up to 2.08x under YJIT. Sampo Kuokkanen both times.[1,2,3].include?(x) compiled to duparray, allocating a throwaway copy of the array on every single call. A million calls now allocate 2 objects total instead of 1,000,004, and the hot path itself is 1.68x faster on top of that. Sergey Fedorov.JSON::ResumableParser was fully decoding an incomplete number on every chunk that extended it – building a bignum each time and throwing it away until the number was finally complete. Defer the decode and a quadratic cost goes linear: a 128,000-digit number fed in 128-byte chunks, 3.07s → 8.13ms. That’s Masataka “Pocke” Kuwabara, and at roughly 378x it’s the largest ratio on this list.fastASet. JRuby users get somewhere between 4.97x and 10.39x depending on the test file, best on citm_catalog.json.json_decode_integer so more 19-20 digit integers hit the fast path instead of falling through to bignum. 2.04x on a file of large integers.new_range_fixnum and gen_new_range. 3.41x and 3.56x on tight range-allocation loops..new get reset back to Class.new, which drops the STI type check and unlocks Ruby 4.0’s fast-path allocation. 15-17% on Ruby 4, 8-17% on 3.4 and 3.3. Mike Dalessio./^email$/, you can pull the literal out and check a Hash instead of testing every regexp in turn. 4.5x when all the filters are exact matches. Alex Watt.this_week?, this_month? and this_year? were walking their whole range via Range#include? and its #succ iteration, which means this_year? was stepping through roughly 365 dates on every call. Range#cover? just compares endpoints. 10-100x depending on the period.ActionController::Parameters#deep_transform_keys! was rebuilding the whole parameters hash instead of mutating it, leaving an in-place helper that someone had already written sitting there uncalled. Wire it up and a 200-entry, 3-level params hash allocates 1,612 objects instead of 5,825. Kenta Ishizaki, who also has the Range#cover? fix above.tomoya ishida replaced the naive expansion in BigMath.erf and erfc with repeated Taylor expansions at increasing precision, binary splitting each step. The gains scale with precision, so the numbers get silly at the top end: BigMath.erf(10, 100000) goes from 13.38s to 1.04s, and the worst case in the PR – erfc of a full-precision number at 100,000 digits – from 1137s, nearly nineteen minutes, to 5.27s.
Two from Hiroshi SHIBATA.
Exception#detailed_message call. ruby -e1 on macOS: 114ms → 30ms.File.stat on Windows was opening a full file handle – five-plus syscalls – and require‘s realpath resolution repeats that for every parent directory. On Windows 11 24H2 and later, a single GetFileInformationByName replaces the lot. File.stat 3.9x, require "rubocop" 1.35x, require "active_support/all" 1.55x.rm -rf node_modules into the asset-precompile layer of the generated Dockerfile and switched chown -R to COPY --chown. About 13s and about 50s off his builds, respectively.parse_expression_terminator walked the entire receiver chain on every infix operator, even when binding power alone already settled the question. Check binding power first: parsing "a" followed by 8000 .b calls drops from 44.8ms to 0.46ms. Shugo Maeda.async regenerating a cancellation cause and backtrace for every descendant task during bulk cancellation. Reusing the originating one takes a 5,461-task cancellation from 103,833 allocated objects to 21,929.exist? checks and uploads in Rails’ storage-mirroring service, which had been running them sequentially despite already owning a thread pool of the right size. With simulated 50ms cloud latency, syncing to five mirrors goes 250ms → 50ms – the bottleneck is round-trips, not bandwidth.That’s the backlog cleared. Go read the ones that caught your eye; the methodology sections are usually more interesting than the numbers. Thanks to everyone above for doing the work and then writing it up.
The post Ruby and Rails Performance Roundup: The Backlog Edition appeared first on Closer to Code.
Canonical announces that the Advantech AOM-2721 is officially joining the list of Ubuntu Certified Hardware.…
This article provides a guide demonstrating how to deploy OpenProject on Ubuntu VPS. What is…
Canonical is pleased to announce that NVIDIA’s newly introduced NVIDIA Nemotron 3.5 Lightning, an open,…
If you have been using an ecommerce store to place online orders or have been…
This article provides a guide to deploy Shoutcast streaming server on Ubuntu VPS. What is…