Skip to content

Commit 3e8cfd5

Browse files
committed
libext: tune prefetch ring to N=4/M=4 from a device-queue-depth sweep
Sweep on a local-NVMe host (, ext4 4K/no-journal, O_DIRECT so reads hit the device, 512 MiB, median of 3): config 64K 128K 256K prefetch mem/file N=1 M=1 ~0.69 ~0.85 ~0.85 256 KiB (~QD1) N=4 M=2 ~1.20 - ~1.28 1 MiB N=4 M=4 ~1.26 ~1.23 ~1.29 1 MiB <- knee N=8 M=4 ~1.28 ~1.28 ~1.31 2 MiB N=8 M=8 ~1.28 ~1.28 ~1.29 2 MiB N=16 M=16 ~1.25 - ~1.21 4 MiB Throughput climbs steeply from N=1/M=1 to the knee at N=4/M=4 (~1.28 GB/s) and then plateaus: deeper rings or more workers give no further gain. The cap is downstream of this code -- OSv's virtio-blk make_request() serialises submission under a single _lock and a single virtqueue, so effective device queue depth tops out at ~2-4 regardless of how many prefetch windows are in flight. (Linux fio O_DIRECT on the same raw NVMe: ~1.32 GB/s @128k QD1, ~1.68 GB/s @128k QD>=2, i.e. the device itself saturates at QD2; the remaining OSv gap is the guest virtio-blk path, not prefetch depth.) So N=4/M=4 is the sweet spot: same throughput as the deeper configs at half the memory (1 MiB vs 2 MiB per open file). It also matches the prior 2-window async design (~1.25 GB/s here) while using a general N-window ring. Make N and M build-time tunables (-DRA_WINDOWS_N / -DRA_WORKERS_M via $(RA_FLAGS)) so the sweep is reproducible when the virtio-blk submission path is later parallelised. No VERIFY FAIL at any config or block size (4K/64K/128K/256K/262143/1M, files smaller than the ring).
1 parent 02a5d53 commit 3e8cfd5

2 files changed

Lines changed: 20 additions & 5 deletions

File tree

modules/libext/Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ include ../common.gmk
33
module_out := $(out)/modules/libext
44

55
CXXFLAGS = -fPIC -std=gnu++11 $(INCLUDES) -I../lwext4/upstream/lwext4/include -I../lwext4/upstream/lwext4/build_lib_only/include \
6-
-D_KERNEL -D_GNU_SOURCE -Wall -fno-exceptions -fno-rtti -O2
6+
-D_KERNEL -D_GNU_SOURCE -Wall -fno-exceptions -fno-rtti -O2 $(RA_FLAGS)
77

88
# the build target executable:
99
TARGET = libext

modules/libext/ext_vnops.cc

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -122,12 +122,27 @@ struct auto_inode_ref {
122122
// ra_lock. A slot in FILLING is never freed/reused until its worker completes;
123123
// invalidate/seek/teardown all drain in-flight fills first (no use-after-free).
124124
//
125-
// Memory: RA_WINDOW * RA_WINDOWS per open file (256K * 8 = 2 MiB). Buffers are
125+
// Memory: RA_WINDOW * RA_WINDOWS per open file (256K * 4 = 1 MiB by default).
126+
// Buffers are
126127
// allocated lazily as slots are first armed, so a file shorter than the ring
127128
// only allocates the windows it actually uses.
128129
static const size_t RA_WINDOW = 256 * 1024; // per-window bytes
129-
static const unsigned RA_WINDOWS = 8; // ring depth (windows ahead)
130-
static const unsigned RA_WORKERS = 4; // concurrent fill threads => QD
130+
// N (ring depth) and M (worker count = device queue depth) are the two tuning
131+
// knobs; override at build time with -DRA_WINDOWS_N=<n> -DRA_WORKERS_M=<m> for
132+
// the sweep. Defaults from a device-queue-depth sweep: throughput climbs from
133+
// ~0.69 GB/s at N=1/M=1 to a ~1.28 GB/s plateau at N=4/M=4; going deeper
134+
// (N=8/M=8, N=16/M=16) yields no further gain because OSv's virtio-blk
135+
// make_request() serialises submission under a single _lock/virtqueue, capping
136+
// the effective device queue depth at ~2-4. N=4/M=4 sits at the knee and uses
137+
// only 1 MiB of prefetch memory per open file (4 x 256 KiB).
138+
#ifndef RA_WINDOWS_N
139+
#define RA_WINDOWS_N 4
140+
#endif
141+
#ifndef RA_WORKERS_M
142+
#define RA_WORKERS_M 4
143+
#endif
144+
static const unsigned RA_WINDOWS = RA_WINDOWS_N; // ring depth (windows ahead)
145+
static const unsigned RA_WORKERS = RA_WORKERS_M; // concurrent fill threads => QD
131146

132147
enum ra_slot_state { RA_EMPTY = 0, RA_FILLING, RA_READY };
133148

@@ -1899,7 +1914,7 @@ ext_inactive(vnode_t *vp)
18991914
// page-sized, page-aligned chunk at uio_offset into a freshly allocated page
19001915
// and hand it to pagecache::map_read_cached_page(), which takes ownership.
19011916
//
1902-
// ponytail: allocate-and-copy from lwext4, not a zero-copy borrow-and-pin.
1917+
// note: allocate-and-copy from lwext4, not a zero-copy borrow-and-pin.
19031918
// lwext4's block cache buffers are not page-aligned/shareable the way ROFS's
19041919
// read-around cache is, so copying is the correct first step; a borrow-and-pin
19051920
// bridge like the ZFS ARC one can come later if it shows up hot.

0 commit comments

Comments
 (0)