NAME
Data::Fenwick::Shared - shared-memory Fenwick tree (binary indexed tree; point or range update) for Linux
SYNOPSIS
use Data::Fenwick::Shared;
# a tree over positions 1..1_000_000, anonymous mapping
my $fen = Data::Fenwick::Shared->new(undef, 1_000_000);
$fen->update(5, 3); # add 3 at position 5
$fen->update(9, 7); # add 7 at position 9
$fen->prefix(9); # 10 (sum of positions 1..9)
$fen->range(5, 9); # 10 (sum of positions 5..9)
$fen->point(5); # 3 (value at position 5)
$fen->total; # 10 (sum of all positions)
$fen->set(5, 100); # set position 5 to 100 (returns the old value)
# rank / weighted lookup: smallest position whose prefix sum reaches a target
$fen->find(50); # first position i with prefix(i) >= 50
# share across processes via a backing file
my $shared = Data::Fenwick::Shared->new("/tmp/counts.fen", 1_000_000);
# range-update mode: add to a whole range in O(log n), then query ranges
my $rng = Data::Fenwick::Shared->new_range(undef, 1_000_000);
$rng->range_add(10, 20, 5); # add 5 to every position in [10, 20]
$rng->range(10, 20); # 55 (sum over the range)
DESCRIPTION
A Fenwick tree (binary indexed tree) in shared memory: a fixed-size array of n signed 64-bit integer positions that supports point update and prefix-sum query in O(log n) each, plus an O(log n) binary search for the position at which a running total is reached. It is the compact, update-friendly structure behind cumulative-frequency tables, running rank/order statistics, and weighted random sampling.
Positions are numbered 1 to n (1-indexed). update($i, $delta) adds a (possibly negative) delta at position $i; prefix($i) returns the sum of positions 1..$i; range($l, $r) the sum of $l..$r; point($i) the current value at a single position; and total the sum of everything. set overwrites a position with an absolute value. find($target) returns the smallest position whose prefix sum is at least $target (meaningful when all stored values are non-negative) -- the operation that turns a Fenwick tree into a weighted sampler or a rank index.
The tree lives in a shared mapping, so several processes update and query one structure: any process that opens the same backing file, inherits the anonymous mapping across fork, or reopens a passed memfd sees the others' updates and contributes its own. A write-preferring futex rwlock with dead-process recovery guards mutation, so many processes may update and query concurrently. Two trees of equal size n can be merged by element-wise addition (a Fenwick tree is linear, so the merge of tree(A) and tree(B) is tree(A+B)).
Values are signed 64-bit integers; sums that overflow 64 bits wrap, as with any native integer arithmetic. Memory is (n+1) * 8 bytes for the tree plus a fixed header. Linux-only. Requires 64-bit Perl.
Range-update mode
A tree created with new_range supports range update as well as range query: range_add($l, $r, $delta) adds a delta to every position in [$l, $r] in O(log n), and prefix/range/point/total report the resulting sums. It uses the classic two-BIT technique (a second binary indexed tree tracking the weighted difference), so a range-mode tree costs twice the memory (2 * (n+1) * 8 bytes) but adds O(log n) range updates a plain Fenwick tree cannot do. update($i, $delta) and set still work (a point update is just range_add($i, $i, $delta)). find and merge are not available in range mode (the two-BIT layout has no single-BIT binary lift); use a point tree for those. The mode is recorded in the header, so a reopened segment stays range mode. Note that the 0.02 on-disk format is incompatible with 0.01: a file created by 0.01 cannot be opened and must be recreated.
METHODS
Constructors
my $fen = Data::Fenwick::Shared->new($path, $n);
my $fen = Data::Fenwick::Shared->new(undef, $n); # anonymous
my $fen = Data::Fenwick::Shared->new_memfd($name, $n);
my $fen = Data::Fenwick::Shared->new_from_fd($fd);
# range-update mode (two BITs) -- same arguments
my $fen = Data::Fenwick::Shared->new_range($path, $n);
my $fen = Data::Fenwick::Shared->new_range_memfd($name, $n);
$path is the backing file (undef or omitted for an anonymous mapping). $n is the number of positions (at least 1); positions are then addressed as 1..$n. new and new_memfd croak if $n is less than 1 or exceeds the tree cap. When reopening an existing file or memfd, the stored n wins and the caller's $n argument is ignored -- but a positive $n placeholder is still required, since the constructor validates $n before the stored value wins. new_memfd creates a Linux memfd (transferable via its memfd descriptor); new_from_fd reopens one in another process. An optional file mode may be passed as the last argument to new (e.g. 0660) to opt a newly-created backing file into cross-user sharing; it defaults to 0600 (owner-only).
Updating
$fen->update($i, $delta); # add $delta at position $i (1 <= $i <= n)
$fen->range_add($l, $r, $delta); # add $delta to every position in [$l, $r] (range mode)
my $old = $fen->set($i, $value); # set position $i to $value; returns the old value
$fen->clear; # reset every position to 0
range_add adds a delta to a whole inclusive range in O(log n) and requires a range-mode tree (new_range); it croaks on a point-mode tree. update adds a signed delta at a single position and returns nothing. set overwrites a position with an absolute value and returns its previous value (it is update($i, $value - point($i)) done atomically under one lock). Both croak if $i is outside 1..n. clear zeroes the whole tree.
Querying
my $s = $fen->prefix($i); # sum of positions 1..$i (0 <= $i <= n; prefix(0) == 0)
my $s = $fen->range($l, $r); # sum of positions $l..$r (1 <= $l <= $r <= n)
my $v = $fen->point($i); # value at position $i
my $t = $fen->total; # sum of all positions (== prefix(n))
my $i = $fen->find($target); # smallest position with prefix >= $target
prefix, range, point, and total are O(log n) reads returning signed integers. find binary-searches the tree for the smallest position whose prefix sum is at least $target, returning that position or n+1 if no prefix reaches it. find is only meaningful when every stored value is non-negative (a cumulative distribution): it is the core of weighted sampling (draw $target uniformly in [1, total] and find the bucket) and of order-statistic / rank queries. Out-of-range positions croak. find requires a point-mode tree (it croaks in range mode).
Merging, introspection, lifecycle
$fen->merge($other); # element-wise add (point mode; both must have equal n)
$fen->size; # n, the number of positions
$fen->is_range; # true for a range-mode (two-BIT) tree
$fen->stats; # { size, total, ops, mmap_size, range }
$fen->path; $fen->memfd; $fen->sync; $fen->unlink;
merge adds another tree's contents into this one position by position; both trees must have the same n or it croaks, and both must be point-mode (merge croaks in range mode). The other tree is snapshotted under its own read lock, so two processes may merge concurrently without deadlock. is_range reports whether the tree is range mode. size (also capacity) is n. sync flushes the mapping to its backing store (a no-op for anonymous and memfd trees); unlink removes the backing file (also callable as Class->unlink($path)); path returns the backing path (undef for anonymous, memfd, or fd-reopened trees) and memfd the backing descriptor -- the memfd of a new_memfd tree or the dup'd fd of a new_from_fd tree, and -1 for file-backed or anonymous trees.
STATS
stats() returns a hashref: size (the number of positions n), total (the current sum of all positions), ops (running count of write-path calls -- update, range_add, set, merge, clear), mmap_size (bytes of the shared mapping), and range (1 for a range-mode tree, 0 for a point-mode tree).
SHARING ACROSS PROCESSES
The tree lives in a shared mapping, shared the same three ways as the rest of the family: a backing file (every process calls new($path, $n) on the same path with a matching $n), an anonymous mapping inherited across fork, or a memfd whose descriptor is passed to an unrelated process (over a UNIX socket via SCM_RIGHTS, or via /proc/$pid/fd/$n) and reopened with new_from_fd($fd). Because the mapping is shared, every process updates and queries the same tree.
# producer and consumer share one running-sum tree with no coordination
my $fen = Data::Fenwick::Shared->new(undef, 1000); # before fork
unless (fork) { $fen->update($_, 1) for 1 .. 500; exit }
wait;
print $fen->total, "\n"; # 500 -- the child's updates
SECURITY
Backing files are created with mode 0600 (owner-only) by default. To share a backing file across users, pass an explicit octal file mode such as 0660 as the last argument to new; the mode is applied only when the file is created. The file is opened with O_NOFOLLOW (a symlink at the path is refused) and O_EXCL; the on-disk header is validated when the file is attached. Any process you grant write access to a shared mapping is trusted not to corrupt it while others are using it.
CRASH SAFETY
Mutation is guarded by a futex-based write-preferring rwlock with PID-encoded ownership; if a holder dies, the next contender detects the dead owner and recovers. Each update is a short O(log n) sequence of int64 stores, so a crash leaves the tree consistent up to the last completed operation. Limitation: PID reuse is not detected (very unlikely in practice).
Reader-slot exhaustion (slotless readers): dead-process recovery attributes a crashed lock holder's contribution through its reader-slot. The slot table holds 1024 entries (one per concurrent reader process). If more than that many reader processes share one mapping at once, a reader that cannot claim a slot proceeds "slotless" -- it still takes the read lock but leaves no per-process record. If such a slotless reader is then killed while holding the read lock, its share of the lock cannot be attributed to a dead process, so writer recovery cannot reclaim it and writers may block until the mapping is recreated. Reaching this needs more than 1024 concurrent reader processes on one mapping plus a crash in the brief read-lock window; the dead-process slot reclaim keeps the table from filling with stale entries, so in practice it is very unlikely.
SEE ALSO
Data::SortedSet::Shared (order-statistics ZSET), Data::NDArray::Shared (dense numeric arrays), Data::Histogram::Shared (HdrHistogram), and the rest of the Data::*::Shared family.
AUTHOR
vividsnow
LICENSE
This is free software; you can redistribute it and/or modify it under the same terms as Perl itself.