Hi Stephen, Thanks, I sincerely appreciate for your time and patience in reviewing this. The latest version already addresses many of the issues you/AI had pointed out. Here are some details of the design choices for this rwlock.
This lock targets problems that RCU cannot solve: datapath threads that must block readers while a writer performs in-place mutation. RCU's zero-reader-wait property requires copy-update semantics — that option is unavailable for many complex datastruct mutations, the object's identity is embedded in other structures, etc. For those cases a rwlock is the correct primitive, and the question is which rwlock minimizes reader cost. We agree RCU should be preferred wherever it applies. This is precisely for where it does not. Two concrete patterns, frequently seen in software data paths, drive the design: 1. Control-side config objects read per packet or per flow by multiple datapath threads, written rarely by a single control thread. 2. Objects that can be mutated from any datapath thread but are read far more often than written. Write events are on the order of 10^-7 of packet events — roughly once per 100 ms at 1 Mpps. Reader critical sections are 10–100 ns. Writers are at most a few 100 ns when used correctly. This submission is not a bug fix!. The contribution is elimination of the atomic RMW on the reader fast path. Every DPDK rwlock primitive — rte_rwlock, rte_pflock, rte_ticketlock — requires an atomic increment or CAS on every reader lock and unlock. prwlock readers use only a plain store, a memory fence, and a plain load on entry, and a release store on exit. No atomic RMW anywhere on the reader path, contended or not. That property is not available in any current DPDK primitive, which is why no DPDK primitive is cited as equivalent. A three-state flag (ABSENT/WAITING/ACTIVE) does encode similar state, but the version counter does more than distinguish states. When the writer increments GV to V, any reader that has updated TV[i] == V satisfies the scan even while still inside the critical section (CS) boundary spin — the writer skips it. To get the same behavior would result in a very similar version based implementation. The cited prior work (brlock, percpu_rw_semaphore, Dice/Shavit) either requires exclusive CPU pinning for all threads, kernel primitives unavailable in userspace, or does not eliminate the atomic RMW on the reader path. On RP = Absent` == `rte_rcu_qsbr_thread_offline()` / IPI replacement: These are not equivalent, and understanding why requires going back to what the "heartbeat" in the original paper actually was. The kernel PRW lock used IPIs (Inter-Processor Interrupts) to notify readers when a writer was spinning: hardware interrupts, delivered preemptively to the remote CPU, handled in tens of nanoseconds. That is the mechanism that kept writer wait bounded at CS granularity. When porting to userspace, IPIs are unavailable. The version counter is the only substitute that stays in the same latency class, in nanoseconds. QSBR's `thread_offline()` is not a replacement for this. It is a process-wide cooperative signal — "not accessing any RCU-protected object" — that requires the thread to reach a designated quiescent point on its own execution schedule. A DPDK datapath lcore is never offline in that sense; it processes packets continuously. Using `thread_offline()` / `thread_online()` to signal is semantically wrong (it signals across all locks, not one) and likely infeasible (millions of calls per second per lock instance, each more expensive than the release store it replaces). More fundamentally, QSBR is lock-unaware at fine granularity: a writer on lock-A must wait for all lcores to reach a quiescent state, including lcores that have never touched lock-A and are actively holding lock-B. QSBR epoch introduces cross-lock serialization where none logically exists. We had a QSBR-based implementation a decade ago. Here a 50 ns write produces a ~50 µs reader stall because every reader arriving after the writer must spin on writer_status for the entire grace period. rte_rcu_qsbr_quiescent() per loop iteration is correct usage of lib/rcu. A packet processing loop takes 10 - 100 µs; in practice, with security and stateful service processing, it can exceed that. That is the minimum writer wait, and it becomes the reader blocking time for every reader arriving while the writer waits. For a lock whose readers execute in 10–100 ns, a 10–100 µs wait is fundamentally wrong. Per-CPU rwlocks carry an assumption each reader thread must be the sole thread on its CPU or atomic incr/decr are required for accounting. Here we do allow for non-pinned thread also to be able to take the lock in some very rare and infrequent cases. The latest version includes profiling results for two representative workloads, both showing substantial throughput gains over atomic-RMW rwlocks on the reader-heavy paths this lock targets. Fairness of readers between successive writers: This has not been an observed issue in the target workloads; if it becomes one, a writer-count backoff is a straightforward addition. AI flagged Correctness violations: * Lemma 8.1 / TryWriteLock GV rollover (16-bit). Valid and I shall fix it. * Write unlock memory barrier: This has been fixed in this last version. * Plain-C data races on GV: I will look into it for C11 conformance. * The compact (16-bit) variant was introduced for memory-constrained deployments where hundreds of lock instances must fit in a small footprint. This was changed in latest version and performance profiled is with cache lined aligned per-thread objects. * Write-Unlock broadcast invalidates N reader-owned lines per write: Correct, but this is rare as write lock itself is rare so doesn’t effect datapath performance. Also, this was done for defensive reasons, and can be safely removed, except for in upgrade. * Non-atomic upgrade: This has been addressed in the latest version. Thanks Mukul From: Stephen Hemminger <[email protected]> Date: Tuesday, September 8, 2026 at 10:55 AM To: Mukul Katiyar <[email protected]> Cc: [email protected] <[email protected]> Subject: Re: [RFC] Highly efficient reader-writer lock (EPRW) for mostly-read applications CAUTION: This email originated from outside of the organization. Do not click links or open attachments unless you recognize the sender and know the content is safe. On Tue, 8 Sep 2026 03:55:12 +0000 Mukul Katiyar <[email protected]> wrote: > Hi all, > > Sharing a userspace reader-writer lock that has been running in production in > a DPDK-based network function for several years and wanted to check if there > would be interest in contributing it to DPDK as rte_eprwlock. > > The Enhanced Passive Reader-Writer (EPRW) lock eliminates atomic operations > on the reader fast path, giving near-flat per-reader performance as core > count grows. It is compatible with poll-mode lcore discipline — no heartbeat > or periodic refresh required from registered threads. > > Details, correctness proof, memory ordering analysis (x86-TSO and ARM), and > performance evaluation against rte_rwlock and pthread_rwlock_t are in a > preprint at: > https://nam10.safelinks.protection.outlook.com/?url=https%3A%2F%2Fzenodo.org%2Frecords%2F22636501&data=05%7C02%7Cmukul%40versa-networks.com%7Cc15a028a60544172794f08df0dd2654a%7Cd39a23bd897c45f1a3f5c5213b673627%7C0%7C0%7C639244869461676917%7CUnknown%7CTWFpbGZsb3d8eyJFbXB0eU1hcGkiOnRydWUsIlYiOiIwLjAuMDAwMCIsIlAiOiJXaW4zMiIsIkFOIjoiTWFpbCIsIldUIjoyfQ%3D%3D%7C0%7C%7C%7C&sdata=a5uPwrJifNq2SQ5yh38Iko6luqsAWWwvRiLDho3gtd8%3D&reserved=0<https://zenodo.org/records/22636501> > > Would this be a useful addition to DPDK? > > Regards, > Mukul Katiyar > Versa Networks > As an exercise, also point Fable AI to do analysis of the paper versus current DPDK code. Surprisingly AI is relatively good at understanding locking; probably because it has been trained on a huge data set of academic papers. Short version: this is a write-up of Versa fixing a bug in their own userspace port of Liu's PRW lock. It is not a critique of rte_rwlock and does not cite any DPDK primitive (DPDK is cited as "Intel Corporation 2024", which tells you how closely they looked). "Outdated DPDK" is too generous; there is no DPDK survey at all. The DPDK relevance is only the deployment context. Analysis: 1. What the algorithm actually is. Once the writer scan checks `RP[i] == Present && TV[i] != GV`, the version counter only distinguishes "spinning at boundary" from "inside CS". A three-state per-thread flag (ABSENT/WAITING/ACTIVE) does the same job with no GV, no unlock broadcast, no rollover. The only thing TV buys is a WAITING to ACTIVE transition without a store+fence, because the writer's GV++ invalidates the match for it. That is the contended path, so it does not affect throughput. Strip that and you have a per-thread-flag rwlock: Hsieh and Weihl 1992, Linux 2.4 brlock, Dice/Shavit read indicators, percpu_rw_semaphore slow path. The "heartbeat" was self-inflicted: they dropped the kernel IPI and did not add an offline state. `RP = Absent` is `rte_rcu_qsbr_thread_offline()`. 2. The premise contradicts DPDK practice. Section 3 says per-lcore periodic reporting is incompatible with poll-mode discipline. `rte_rcu_qsbr_quiescent()` per loop iteration is exactly how lib/rcu is used, with online/offline for idle threads. Section 10 admits the QSBR analogy but not that DPDK ships it. liburcu (Desnoyers et al., TPDS 2012) is the canonical treatment of replacing kernel IPI/quiescence in userspace and is not cited either. RCU also gives readers zero wait and real reclamation; EPRW writers still spin on every in-CS reader. 3. No comparison, no numbers. Missing: rte_rwlock (4 bytes, WAIT bit so writers cannot starve), rte_pflock (bounded wait both sides), rte_seqlock/seqcount, rte_rcu_qsbr, rte_mcslock, rte_ticketlock. Microbenchmarks "left for future work". EPRW has no fairness: a reader spinning on W must catch a window between one writer's `W = Absent` and the next writer's CAS, and back-to-back writers skip it forever since its TV matches. That is the case pflock was added for. 4. Concrete defects: - Lemma 8.1 is wrong. `Try_Write_Lock` increments GV and on failure releases W without the broadcast. A caller retrying try_wlock against a reader stuck in its CS (preempted control thread, slow path) gets 65536 failures, GV wraps, `TV[i] == GV`, false boundary match, exclusion violated. The 16-bit variant is unsafe as published. Trivial fix, but the proof did not cover its own try path. - ARMv8 `Read_Lock`: the exit load of W has no acquire. The DMB after the RP store does not order that load against later CS loads, so a reader can see pre-write data. Same in `Try_Read_Lock`. Section 6 walks every barrier site and misses this one. Fine on TSO. - Plain-C data races: readers load GV while the writer does a non-atomic increment; TV[i] is stored by thread i and by the broadcasting writer. Works on hardware with aligned words, UB in C11. Any DPDK version would need rte_stdatomic relaxed ops anyway. - Compact mode packs 3-byte slots: 32 threads in 3 cache lines, so every read lock/unlock is a store to a shared line. That reintroduces the coherence traffic PRW exists to avoid. The 64-byte alignment in the original is the whole point. No measurement. - Write_Unlock broadcast invalidates N reader-owned lines per write on top of the scan. Fine for read-mostly, but it is why per-lcore-slot locks do not fit "hundreds of locks", which is their own compact-mode motivation. - `TV[tid] = GV` in the Write_Lock contention loop is dead: a plain contender has RP Absent, so the winner's scan skips it regardless. Only Upgrade needs it. - "Non-atomic upgrade" (return 1) is a read unlock followed by a write lock; data can change in between. The name invites misuse. - Reader fast path uses MFENCE. rte_smp_mb() uses `lock addl` on x86 for a reason; xchg for the RP store folds store and barrier.

