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>
Remove unnecessary length member, make the cache GC compation and generational friendly
1f7efbf to
f96dec0
Compare
|
@czj Looks pretty good! I added two more commits. Are they OK? |
|
Thanks for taking this further @larskanis! Yes, both look good to me. Dropping |
To make it usable for possible other encoders/decoders using a cache in the future.
f96dec0 to
100c63c
Compare
A miss called rb_ractor_shareable_p(), which walks the object, and stored the value into the old cache marker, so it got promoted. A column of mostly distinct values like 1234.56 decoded 1.8x slower than without the cache. Values seen once now leave only their key, and are stored when seen again. Assisted-by: Claude Opus 5.5 [Claude Code]
|
@larskanis I pushed 93961cf, which changes the cache a bit, so a heads-up: on a column of mostly distinct values (1M random |
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, so a lookup is one multiply, a compare and an uncontendedrb_nativethread_lock_tpair. A text is cached the second time it's seen in a row in its slot: the first time only leaves its key. Caching every miss made a column of mostly distinct values, like 1M random prices, 1.8× slower than without the cache, becauserb_ractor_shareable_p()walks each new object and the stored values got promoted to the old generation; with this rule that column decodes at the uncached speed. 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 updates them on compaction, and apthread_atforkchild handler resets the lock and empties the cache, in case another Ractor held it during the fork.The cache is on for everyone, and
PG::Coder::DISABLE_CACHEturns it off per coder. Equal texts now share one frozen object, soequal?andobject_idchange meaning while the values stay immutable.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 (64 KB on 64-bit). On the numeric columns of our e-commerce app,
PG::Result#valuesgets 2.5–4.2× faster, with 0.6–1.7 objects allocated per value instead of 3.2–4, and the decoded values are identical to the uncached ones.Measurements
numeric(10,2)That's
PG::Result#valueswith aTypeMapByColumnof numeric decoders, on up to 200,000 rows per table of representative production data from our app, each figure the median of 3 processes of 15 interleaved runs; the last row is the worst case, generated withgenerate_series. Each run compares the cached decoder againstDISABLE_CACHE, which runs master's exact code. 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 scripts are here and here.The specs cover identity, caching on the second sighting, the length limit, the flag, invalid input, the exception modes, GC liveness and compaction, and 4 parallel Ractors.