Skip to content

Reduce BGSAVE/AOF COW by freeing SDS strings in fork child instead of madvise#15260

Open
YangboLong wants to merge 2 commits into
redis:unstablefrom
YangboLong:reduce_cow
Open

Reduce BGSAVE/AOF COW by freeing SDS strings in fork child instead of madvise#15260
YangboLong wants to merge 2 commits into
redis:unstablefrom
YangboLong:reduce_cow

Conversation

@YangboLong

@YangboLong YangboLong commented May 25, 2026

Copy link
Copy Markdown
Contributor

This PR is based on Valkey PR #905 and the prerequisite zfree_with_size merged via #15071.

Replace dismissMemory(MADV_DONTNEED) with sdsfree() in the fork child's dismiss path for SDS strings. madvise operates at page granularity and is ineffective for typical string values (< 4KB). sdsfree() lets jemalloc coalesce freed regions and release whole pages back to the OS.

  • object.c: dismissSds() calls sdsfree(). Bypass the page-size threshold and THP check for OBJ_STRING in dismissObject() since sdsfree works at any allocation size and doesn't depend on madvise.
  • rdb.c: Lazily-allocated 8KB reusable LZF compression buffer to prevent jemalloc from reusing just-freed pages for compression output.
  • aof.c: Same threshold bypass for the AOF rewrite fork child.

Results (3M keys, 80/20 read/write, 10% fragmentation, setup similar to Valkey PR #905):

Value size COW overhead vs vanilla Child RSS released
2000B 65% (1127→736 MB) 79.4% (6222→1282 MB)
500B 96% (542→519 MB) 12.8% (1682→1467 MB)

Without this change, child RSS never decreases during BGSAVE because madvise cannot release sub-page allocations.

Valkey PR #905](valkey-io/valkey#905) reported ~53% COW overhead for 2000B and ~80% for 500B with up to 12M keys.


Note

Medium Risk
Changes fork-child memory release for persistence (strings freed via jemalloc, LZF static buffer); scoped to child with refcount checks, but incorrect free timing could affect rewrite/snapshot correctness.

Overview
This PR reduces copy-on-write (CoW) during BGSAVE and AOF rewrite in the fork child by changing how serialized string data is released after each key is written.

String dismiss path: dismissSds() now calls sdsfree() instead of dismissMemory(MADV_DONTNEED), so jemalloc can return small string allocations to the OS rather than relying on page-granular madvise. dismissObject() skips the transparent-huge-pages guard and the half-page size_hint cutoff for OBJ_STRING, while non-string types still use the existing madvise-based dismiss rules.

Fork-child call sites: aof.c and rdb.c always invoke dismissObject() when server.in_fork_child (they no longer require serialized size above half a page before calling in).

RDB LZF: rdbSaveLzfStringObject uses a lazily allocated 8KB static compression buffer for small outputs so the child does not zmalloc into pages just freed by dismissSds(), which would dirty shared pages and worsen CoW.

Reviewed by Cursor Bugbot for commit d8de0fe. Bugbot is set up for automated code reviews on this repo. Configure here.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Reviewed by Cursor Bugbot for commit 02d8ec2. Configure here.

Comment thread src/object.c
@sundb sundb requested a review from ShooterIT May 26, 2026 00:37
@ShooterIT

Copy link
Copy Markdown
Member

i am not sure if this idea can work fine, if there are lots of different size string, sdsfree may touch metadata, especially there are other data types (like list, hash, set), and cause CoW of an entire page. if redis doesn't have enough traffic, this way may cause more CoW memory.

@YangboLong

YangboLong commented May 26, 2026

Copy link
Copy Markdown
Contributor Author

I'm not an expert on memory allocators. But it looks like sdsfree shouldn't trigger COW on the data pages (the entire dismissObject() path is compile-time gated on macro USE_JEMALLOC and runs on jemalloc builds), but only on metadata pages with negligible overhead. The original Valkey PR discussed a similar concern. They ran idle-system measurements (no write traffic during BGSAVE) and found only ~1-3% COW overhead in the worst case. Would you like me to run similar idle-system tests, potentially with different object types mixed in?

@fcostaoliveira

Copy link
Copy Markdown
Collaborator

This change touches performance-sensitive code paths. Adding the action:run-benchmark label will trigger the CE Performance suite so we can see the impact before merge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants