Repository navigation
Conversation
Money columns repeat a handful of values (0.00, VAT rates, round prices), and every one of them went through BigDecimal() with a fresh String. Values up to 8 characters now go through a process-wide cache of 4096 slots keyed by their raw text, so a hit returns the same frozen BigDecimal without allocating. A native lock guards the slots against Ractors running in parallel, and is never held across BigDecimal(). The flag PG::Coder::NUMERIC_UNCACHED restores a new object for every value. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A cached NaN or Infinity skipped BigDecimal.mode exceptions, so only texts ending with a digit are cached now. Values are stored only when rb_ractor_shareable_p() says they are, since the cache hands them to every Ractor. The lock and the GC marker are set up once with the TextDecoder module, and a forked child resets the lock and empties the cache, in case another Ractor held it during the fork. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Following #762.
PG::TextDecoder::Numericnow keeps a process-wide cache of the frozen BigDecimals it returns for finite texts of up to 8 characters, keyed by the raw text, so values like0.00,1.200or1299.00come back without allocating a String or a BigDecimal. Longer texts,NaNandInfinity(soBigDecimal.modeexceptions still fire), and anythingBigDecimal()rejects go throughBigDecimal()exactly as before.The cache is a direct-mapped table of 4096 slots. The key is the text padded into a
uint64_t, plus its length, so a lookup is one multiply, two compares and an uncontendedrb_nativethread_lock_tpair. The lock is never held acrossBigDecimal(), which can raise, and onlyrb_ractor_shareable_p()results are stored, so any Ractor can receive them. A hidden TypedData object marks the cached values for the GC, and apthread_atforkchild handler resets the lock and empties the cache, in case another Ractor held it during the fork.The open question is the default, and I'd like your call on it. As written, the cache is on for everyone, and
PG::Coder::NUMERIC_UNCACHEDturns it off per coder. That's what lets Rails andBasicTypeMapForResultsusers benefit without changing anything, but it also means every pg user suddenly gets shared objects for equal values.equal?andobject_idchange meaning, while the values themselves stay immutable. If you'd rather keep identity unchanged by default, I'll flip it to an opt-inNUMERIC_CACHEDflag.On the size: I started with the 512 slots from the issue, but collisions in a direct-mapped table cost more than I expected, so this uses 4096 slots (96 KB on 64-bit). On the numeric columns of our e-commerce app,
PG::Result#valuesgets 2.9–5.1× faster, with 0.4–1.6 objects allocated per value instead of 3.2–4, and the decoded values are identical to the uncached ones.Measurements
That's
PG::Result#valueswith aTypeMapByColumnof numeric decoders, on up to 200,000 rows per table of a production dump. Each build compares the cached decoder againstNUMERIC_UNCACHED, which runs master's exact code, in medians of 15 interleaved runs. The setup is Ruby 4.0.9, bigdecimal 4.1.3, PostgreSQL 18.6 on macOS arm64. The row arrays account for much of the remaining allocations (a full row array per value on the single-column tables). The script is here.The specs cover identity, the length limit, the flag, invalid input, the exception modes, GC liveness (it crashes when the cached values aren't marked) and 4 parallel Ractors.
Would you rather keep it on by default as here, or make it opt-in?
🤖 Generated with Claude Code, but prompted, read, and verified by @czj, a caring human :)