mirror of
https://github.com/amadvance/snapraid.git
synced 2026-10-26 10:51:54 -04:00
1140 lines
38 KiB
Text
1140 lines
38 KiB
Text
SnapRAID RAID library performance notes
|
|
=======================================
|
|
|
|
Scope
|
|
-----
|
|
|
|
These notes summarize performance experiments, implementation choices,
|
|
rejected ideas, recovery optimizations, and non-obvious conclusions
|
|
reached while reviewing and tuning the SnapRAID RAID library.
|
|
|
|
The focus is the 1..6 parity implementation, especially x86 AVX2,
|
|
AVX512BW, GFNI, SSSE3, and AArch64/AArch32 NEON backends.
|
|
|
|
This is intentionally a record of what has already been tried and what was
|
|
learned from it, so that the same ideas are not repeatedly re-investigated.
|
|
|
|
Detailed project-by-project comparisons with other RAID and erasure-coding
|
|
engines are kept in OTHERS. This file keeps only conclusions that affect
|
|
SnapRAID implementation or optimization policy.
|
|
|
|
The main rule is to optimize the real SnapRAID workload, not only isolated
|
|
inner loops.
|
|
|
|
|
|
Implementation overview
|
|
-----------------------
|
|
|
|
Generation:
|
|
|
|
* The 1..6 parity generation paths are considered effectively closed from
|
|
a performance point of view after coefficient-residency optimization and
|
|
the separation of the AES G23 and common x2 hot paths.
|
|
|
|
* Non-GFNI AES generation uses dedicated x2-only and G23 implementations.
|
|
When the number of data disks does not reach the first G23 x3 transition,
|
|
AES dispatches directly to the same branch-free x2 implementation structure
|
|
used by RAID, with the active AES polynomial and Cauchy tables.
|
|
|
|
* The G23 implementation is reserved for larger arrays that actually require
|
|
x3 transitions. This avoids carrying G23 boundary control flow through the
|
|
overwhelmingly common small-array generation path.
|
|
|
|
* Relevant scalar, SSE/SSSE3, AVX2, AVX512BW, AArch64/AArch32 NEON, RAID GFNI
|
|
affine, and AES GFNI paths are implemented.
|
|
|
|
* Specialized GEN2 and GEN3 paths keep useful multiplication tables or
|
|
coefficient representations in vector registers when register pressure
|
|
permits it.
|
|
|
|
* This residency optimization preserves the preferred chunk-major,
|
|
multi-output traversal. It does not add another pass over source blocks.
|
|
|
|
* RAID GFNI Horner Q is implemented where it is useful and where register
|
|
pressure allows it.
|
|
|
|
* AVX512 128-byte processing, non-GFNI AVX512BW Horner, standalone
|
|
VPTERNLOGQ transformations, VPBLENDVB, cached parity stores, and large
|
|
pre-expanded GFNI constants have been investigated. Some are rejected, while
|
|
VPTERNLOGQ is now used where it is part of a larger profitable GEN2/GEN3
|
|
restructuring.
|
|
|
|
Recovery:
|
|
|
|
* The normal one-data-disk recovery with P available is already handled by
|
|
raid_rec1of1(), which is essentially an XOR generation pass with pointer
|
|
remapping.
|
|
|
|
* Generic REC1 non-P SIMD kernels have been locally optimized and spatially
|
|
unrolled where useful.
|
|
|
|
* Generic REC2 non-GFNI kernels hoist coefficient tables and reuse delta nibble
|
|
splits.
|
|
|
|
* NEON REC2 processes two 16-byte blocks per iteration, for a 32-byte recovery
|
|
chunk on the optimized AArch64 path.
|
|
|
|
* RECX keeps delta values in SIMD registers on the optimized paths and reuses
|
|
pre-split low/high nibbles instead of repeatedly reconstructing them.
|
|
|
|
* Non-GFNI RECX precomputes shuffle-table pointers in C before entering the hot
|
|
data loop.
|
|
|
|
* RAID GFNI RECX precomputes the 8-byte affine representation of every inverse
|
|
matrix coefficient in C before entering the hot data loop.
|
|
|
|
* A recurring RECX local cost is repeated coefficient table/matrix loading
|
|
or broadcasting inside the data loop.
|
|
|
|
* Specialized SIMD P+Q rec2of2 recovery is a distinct optimization
|
|
opportunity. The scalar int8 implementation already has the formula.
|
|
|
|
Architecture and validation:
|
|
|
|
* AArch32 has a separate NEON backend for generation and recovery. Cross-build,
|
|
QEMU user-mode testing, and CI coverage are available for the ARM32 path.
|
|
|
|
* Matrix inversion has dedicated multithreaded stress testing. With at most six
|
|
parity levels, inversion setup is not a significant performance target.
|
|
|
|
|
|
Real workload assumptions
|
|
-------------------------
|
|
|
|
* SnapRAID processes large blocks: typically 256 KiB per data/parity block.
|
|
|
|
* A normal stripe therefore touches a large amount of memory. With 12 data
|
|
disks, one 256 KiB stripe already means about 3 MiB of data input before
|
|
considering parity output.
|
|
|
|
* This large-block behavior is very important. Optimizations that reread a
|
|
source buffer, increase the number of passes, or enlarge lookup tables can be
|
|
worse even if the inner instruction sequence looks better.
|
|
|
|
* This is also the first filter for ideas taken from other RAID or
|
|
erasure-coding engines. A design optimized for page-sized recovery,
|
|
cache-resident tiles, or generic small shards may make a different memory
|
|
tradeoff and should not be copied mechanically.
|
|
|
|
* Prefer reducing work inside the existing single-pass traversal before
|
|
considering a traversal change. Saving a broadcast or table load is useful;
|
|
saving registers by rereading a complete 256 KiB source block usually is not.
|
|
|
|
* SnapRAID uses a queue before parity data is written to disk. Real measurements
|
|
with actual disk writes show that very small queues are worse. Performance
|
|
improves clearly up to about queue depth 32, and still improves after that,
|
|
but with diminishing returns.
|
|
|
|
* With 256 KiB blocks, queue depth 32 means this much parity output can be in
|
|
flight:
|
|
|
|
1 parity = 8 MiB
|
|
2 parity = 16 MiB
|
|
3 parity = 24 MiB
|
|
6 parity = 48 MiB
|
|
|
|
Therefore a parity block produced by the RAID kernel is normally not consumed
|
|
immediately.
|
|
|
|
* A memory read of a 256 KiB block that is no longer cached is still very cheap
|
|
compared with real storage I/O. Trying to keep many queued parity blocks hot
|
|
in cache is therefore not necessarily useful.
|
|
|
|
|
|
MOVNT / non-temporal stores
|
|
---------------------------
|
|
|
|
* Generation backends use MOVNTDQ / VMOVNTDQ for parity output where applicable.
|
|
|
|
* The synthetic speedtest naturally favors non-temporal stores because generated
|
|
parity is not consumed afterwards. This benchmark therefore measures producer
|
|
throughput, not the complete producer -> queue -> writer pipeline.
|
|
|
|
* However, the real SnapRAID queue makes the workload much closer to the
|
|
non-temporal case than to an immediate-consumer case.
|
|
|
|
* Cached stores could potentially help only with very short queues, where the
|
|
writer consumes parity while it is still hot. Queue depths around 1..4 are the
|
|
most plausible area where this could happen.
|
|
|
|
* Those very short queues are not representative of the real fastest disk
|
|
pipeline. Real measurements already show that queues up to about 32 provide
|
|
substantial improvements.
|
|
|
|
* At realistic queue depths, cached parity output would occupy many MiB of cache
|
|
and could evict useful input data. MOVNT avoids this cache pollution and
|
|
avoids normal cache allocation/write-allocate behavior for parity output.
|
|
|
|
* Conclusion: keep MOVNT for the normal generation path. Replacing MOVNT with
|
|
cached stores is not considered an open optimization.
|
|
|
|
|
|
Typical recovery case
|
|
---------------------
|
|
|
|
* The common recovery case is one failed data disk while parity P is still
|
|
available.
|
|
|
|
* raid_rec() selects the first available parity. Therefore, for a single data
|
|
failure with P available, recovery uses P.
|
|
|
|
* REC1 implementations special-case ip[0] == 0 and call raid_rec1of1().
|
|
|
|
* raid_rec1of1() remaps pointers so that P is used as an XOR input and the
|
|
failed data buffer is used as the GEN1 output, then calls
|
|
raid_gen(nd, 1, ...).
|
|
|
|
* Therefore the common single-disk recovery path does NOT call
|
|
raid_delta_gen().
|
|
|
|
* The data traffic is already close to the minimum:
|
|
|
|
read P
|
|
read all surviving data blocks
|
|
XOR them
|
|
write the recovered data block once
|
|
|
|
* There are no temporary syndrome buffers, no GF multiplication, and no matrix
|
|
inversion in this common path.
|
|
|
|
* This means that large architectural recovery changes mostly optimize rare
|
|
failure cases, not the usual single-disk recovery.
|
|
|
|
* Two simultaneous data disk failures are already an exceptional case in normal
|
|
use. Three or more simultaneous data failures are even less important as
|
|
optimization targets.
|
|
|
|
|
|
Recovery speedtest interpretation
|
|
---------------------------------
|
|
|
|
The recovery speed tests contain separate rows for the important fast paths and
|
|
for the generic recovery paths.
|
|
|
|
REC1OF1:
|
|
|
|
* The row uses ip[0] == 0.
|
|
|
|
* Every REC1 wrapper therefore enters raid_rec1of1() instead of executing its
|
|
normal GF recovery body.
|
|
|
|
* The speed test explicitly selects the generation function used by the
|
|
raid_rec1of1() call with raid_gen_force(1, ...).
|
|
|
|
* There is no GFNI GEN1 because PAR1 is only XOR. Therefore:
|
|
|
|
rec1of1 avx2 -> raid_gen1_avx2
|
|
rec1of1 gfni -> raid_gen1_avx2
|
|
|
|
rec1of1 avx512 -> raid_gen1_avx512bw
|
|
rec1of1 gfni512 -> raid_gen1_avx512bw
|
|
|
|
This is intentional. The GFNI recovery wrapper is selected, but the fast path
|
|
naturally reduces to the best available XOR generator.
|
|
|
|
REC1:
|
|
|
|
* The generic REC1 row uses ip + 1 to avoid the PAR1 fast path.
|
|
|
|
* It therefore measures recovery from Q or a later parity and executes the
|
|
backend-specific REC1 GF multiplication kernel.
|
|
|
|
REC2OF2:
|
|
|
|
* The row uses ip = { 0, 1 }, meaning P and Q.
|
|
|
|
* raid_rec2_int8() has an existing specialized P+Q fast path and calls
|
|
raid_rec2of2_int8().
|
|
|
|
* SIMD REC2 implementations do not have the equivalent specialized P+Q
|
|
fast path. They execute the generic 2x2 recovery algorithm with P and Q.
|
|
|
|
* This makes the row useful as a direct benchmark for deciding whether a
|
|
specialized SIMD rec2of2 implementation is worthwhile.
|
|
|
|
REC2:
|
|
|
|
* The generic REC2 row uses ip + 1, avoiding the P+Q fast case.
|
|
|
|
* It measures the normal generic 2x2 recovery path.
|
|
|
|
Delta generation:
|
|
|
|
* Each recovery benchmark explicitly forces a matching generation backend with
|
|
raid_gen_force() before entering the timed recovery function.
|
|
|
|
* This is important because raid_delta_gen() internally calls raid_gen().
|
|
|
|
* The benchmark therefore avoids accidentally comparing a recovery kernel while
|
|
using an unrelated generation backend for its delta step.
|
|
|
|
|
|
raid_delta_gen() fusion
|
|
-----------------------
|
|
|
|
* A fused recovery design was studied, where selected syndromes would be
|
|
accumulated directly instead of:
|
|
|
|
raid_delta_gen()
|
|
temporary parity output
|
|
reload temporary parity
|
|
solve recovery matrix
|
|
|
|
* The fused design could also compute only the parity rows actually selected by
|
|
ip[].
|
|
|
|
* This matters because raid_delta_gen() currently computes:
|
|
|
|
np = ip[nr - 1] + 1
|
|
|
|
and raid_gen() can generate only a prefix of parity rows.
|
|
|
|
* Example:
|
|
|
|
nr = 3
|
|
ip = { 0, 2, 5 }
|
|
|
|
requires GEN6 today even though only P, R and U are needed.
|
|
|
|
* A fused RECX could calculate exactly those three syndromes.
|
|
|
|
* The idea is technically valid and could reduce temporary memory traffic and
|
|
unnecessary parity work.
|
|
|
|
* However, the common rec1of1/P case already bypasses raid_delta_gen() entirely.
|
|
|
|
* Because the main benefit applies to less common multiple-failure or missing-P
|
|
cases, this direction was intentionally deprioritized.
|
|
|
|
* Decision: do not pursue raid_delta_gen() + REC fusion for now.
|
|
|
|
|
|
Generation: AVX512 width and unrolling
|
|
--------------------------------------
|
|
|
|
* Wider processing is not automatically faster.
|
|
|
|
* AVX512 128-byte processing was tested and can be slower than the existing
|
|
64-byte path.
|
|
|
|
* Therefore do not assume that x2/x4 spatial unrolling used by some RAID
|
|
engines is beneficial for SnapRAID.
|
|
|
|
* The block size is already large, memory traffic is substantial, and wider
|
|
unrolling can increase register pressure, front-end work, and AVX512 power or
|
|
frequency effects without reducing total memory traffic.
|
|
|
|
* Standalone VPTERNLOGQ transformations were tested on Zen 5 and did not improve
|
|
performance.
|
|
|
|
* This is not a general rejection of VPTERNLOGQ. AVX512BW GEN2/GEN3
|
|
kernels use VPTERNLOGQ where it is part of a larger table-residency
|
|
restructuring that reduces other work. Judge the complete kernel, not an
|
|
instruction in isolation.
|
|
|
|
* Conclusion: keep the conservative AVX512 processing width unless a
|
|
specific benchmark proves otherwise.
|
|
|
|
|
|
Generation: Horner
|
|
------------------
|
|
|
|
* Classic RAID6 P+Q generation uses the Horner recurrence for Q with the
|
|
standard RAID polynomial/generator.
|
|
|
|
* Non-GFNI AES Cauchy generation also evaluates Q with Horner's method. Its
|
|
coefficient sequence uses multiplication by 2 almost everywhere, with
|
|
multiplication by 3 only at the fixed G23 coset boundaries. The optimized
|
|
implementation of this recurrence is described in the next section.
|
|
|
|
* A Horner-style AVX512BW non-GFNI implementation was tested on Zen 5 and did
|
|
not improve performance.
|
|
|
|
* Therefore Horner is NOT considered an open optimization for the AVX512BW
|
|
non-GFNI generation path.
|
|
|
|
* GFNI is different.
|
|
|
|
* For RAID GFNI generation, Horner Q is useful because multiplying by the fixed
|
|
generator can use one fixed affine matrix instead of a different matrix for
|
|
every data coefficient.
|
|
|
|
* RAID GFNI Horner Q is implemented as follows:
|
|
|
|
AVX2 RAID GFNI GEN2..GEN5:
|
|
Horner Q with a fixed multiply-by-2 affine matrix resident in a
|
|
register.
|
|
|
|
AVX2 RAID GFNI GEN6:
|
|
keep the direct-coefficient implementation because register pressure
|
|
is too high to keep the extra fixed matrix efficiently.
|
|
|
|
AVX512 RAID GFNI GEN2..GEN6:
|
|
Horner Q with the fixed matrix resident in a register.
|
|
|
|
AES GFNI:
|
|
unchanged, direct VGF2P8MULB coefficient multiplication.
|
|
|
|
* D0/final coefficient 1 is handled directly where appropriate.
|
|
|
|
* Conclusion: GFNI Horner Q is implemented where useful. Non-GFNI AVX512BW
|
|
Horner was tested and rejected.
|
|
|
|
|
|
Generation: AES G23 x2/G23 split
|
|
--------------------------------
|
|
|
|
The AES Extended Cauchy Q sequence is evaluated in descending Horner order.
|
|
|
|
Most transitions multiply the Q accumulator by 2. Multiplication by 3 is
|
|
required only at the four boundaries between the five G23 cosets:
|
|
|
|
d = 203, 152, 101, 50
|
|
|
|
Therefore arrays with at most 51 data disks never execute an x3 transition.
|
|
For nd = 51 the first Horner step is d = 49. The first x3 transition appears
|
|
only at nd = 52, where the first processed step is d = 50.
|
|
|
|
The dispatch condition is centralized in:
|
|
|
|
raid_g23_x2_only(nd)
|
|
|
|
which currently means:
|
|
|
|
nd <= 51
|
|
|
|
The value 51 is not merely a performance tuning threshold. It is the largest
|
|
data-disk count for which the AES G23 Q recurrence contains only x2
|
|
transitions. The threshold may safely be lowered for performance experiments,
|
|
but it cannot be increased without making the x2-only implementation
|
|
incorrect.
|
|
|
|
Earlier implementations kept x2 and G23 behavior in a single generator and
|
|
tested G23 state inside or around the hot data loop. This was correct, but it
|
|
hurt generated code even when no x3 transition was actually executed.
|
|
|
|
The current implementation therefore uses two separate paths:
|
|
|
|
*_x2()
|
|
branch-free Horner recurrence using only Q *= 2
|
|
|
|
*_g23()
|
|
segmented Horner recurrence using Q *= 2 between boundaries
|
|
and Q *= 3 at the fixed G23 boundaries
|
|
|
|
AES wrappers dispatch before entering the hot kernel:
|
|
|
|
if (raid_g23_x2_only(nd))
|
|
*_x2(...);
|
|
else
|
|
*_g23(...);
|
|
|
|
RAID always selects the x2 path.
|
|
|
|
The G23 implementation computes the first relevant boundary from:
|
|
|
|
raid_g23_boundary(l - 1)
|
|
|
|
where l = nd - 1.
|
|
|
|
Using l - 1 is important. Q is initialized directly from D[l], so D[l] does
|
|
not perform a Horner multiplication. The first transition occurs before adding
|
|
D[l - 1].
|
|
|
|
Between boundaries, the G23 implementations run dedicated x2-only loops.
|
|
The x3 transition is handled separately at the boundary. Implementations that
|
|
process two data disks per iteration must stop pairing before a boundary so
|
|
that a pair never crosses an x3 transition.
|
|
|
|
For GEN3 and higher, D0 is normally handled separately to avoid unnecessary
|
|
Cauchy-table work. Since the lowest G23 boundary is d = 50, the final D0
|
|
transition is always Q *= 2.
|
|
|
|
This separation was introduced for performance, not correctness. A generic
|
|
G23 implementation with no active boundary is mathematically equivalent to
|
|
the x2 path, but its additional control-flow structure can still produce
|
|
significantly worse machine code.
|
|
|
|
This was confirmed on an Intel family 6 model 165 CPU with GCC 11.2.0,
|
|
where forcing the generic G23 control flow on small arrays caused a measurable
|
|
throughput loss across SIMD backends even without executing any x3 transition.
|
|
|
|
|
|
Generation: coefficient residency
|
|
---------------------------------
|
|
|
|
Recent GEN2/GEN3 work established coefficient residency as a practical
|
|
generation optimization, not only a theoretical one.
|
|
|
|
* Non-GFNI SIMD GEN2/GEN3 kernels preload useful low/high multiplication tables
|
|
into vector registers when the register file has enough space.
|
|
|
|
* The resident set can be partial. When there are more data disks than can be
|
|
covered by spare registers, later disks fall back to loading their
|
|
coefficient tables normally.
|
|
|
|
* GEN3 has higher register pressure because both Q and R coefficients are
|
|
needed. Residency is therefore deliberately bounded by the available
|
|
register file rather than forcing spills or another source pass.
|
|
|
|
* Appropriate GFNI generation paths also preload coefficient representations
|
|
when doing so reduces repeated setup without increasing source traffic.
|
|
|
|
* AVX512BW can use VPTERNLOGQ as part of these resident-table kernels to combine
|
|
the accumulator with low/high products. The useful optimization is the whole
|
|
scheduling and residency change, not VPTERNLOGQ by itself.
|
|
|
|
* The source chunk is still loaded once and contributes to all parity outputs
|
|
before moving on. The optimization therefore preserves the most important
|
|
large-block memory property.
|
|
|
|
General conclusion:
|
|
|
|
* First use spare registers to remove repeated coefficient setup while
|
|
preserving one-pass multi-output generation.
|
|
|
|
* Do not trade coefficient residency for extra 256 KiB source passes.
|
|
|
|
* Generation success does not imply that RECX can use the same register map.
|
|
RECX must also keep multiple deltas and reconstruction state live, so its
|
|
coefficient-residency opportunity remains narrower.
|
|
|
|
Overall generation conclusion:
|
|
|
|
* For the 1..6 parity implementation, no major generation optimization remains
|
|
with a strong reason to be investigated.
|
|
|
|
* Further generation changes should require new hardware evidence, a changed
|
|
workload, or measurements that identify a concrete bottleneck.
|
|
|
|
|
|
GFNI field implementation
|
|
-------------------------
|
|
|
|
* AES GFNI multiplication maps naturally to VGF2P8MULB because the instruction
|
|
uses the AES polynomial.
|
|
|
|
* Standard RAID polynomial 0x11d cannot use VGF2P8MULB directly.
|
|
|
|
* RAID GFNI therefore uses VGF2P8AFFINEQB with precomputed affine matrices.
|
|
|
|
* The global affine matrix table stores 8 bytes per coefficient.
|
|
|
|
* A generic exhaustive software test validates all coefficient/input pairs
|
|
against the normal RAID GF multiplication table.
|
|
|
|
* Dedicated hardware affine-matrix validators were intentionally not added.
|
|
Normal generation/recovery tests exercise the hardware path.
|
|
|
|
* RAID and AES GFNI recovery/generation have polynomial-specific names:
|
|
|
|
*_gfni_raid
|
|
*_gfni_aes
|
|
|
|
with backend tags:
|
|
|
|
gfni
|
|
gfni512
|
|
|
|
* This naming and registration split is intentional and should be preserved.
|
|
|
|
|
|
GFNI RECX coefficient precomputation
|
|
------------------------------------
|
|
|
|
RAID GFNI RECX previously performed this dependency chain for every coefficient
|
|
inside every data chunk:
|
|
|
|
load coefficient byte from V[]
|
|
use it to index raid_gfaffine_raid[]
|
|
load the 8-byte affine matrix
|
|
broadcast the matrix
|
|
VGF2P8AFFINEQB
|
|
|
|
The current implementation precomputes:
|
|
|
|
uint8_t C[N * N][8]
|
|
|
|
and copies the complete 8-byte affine representation for every inverse matrix
|
|
coefficient before entering the data loop.
|
|
|
|
The hot loop therefore becomes:
|
|
|
|
direct load/broadcast from C[]
|
|
VGF2P8AFFINEQB
|
|
|
|
This removes the coefficient-byte -> indexed affine-table dependency from the
|
|
hot loop while keeping the compact 8-byte representation.
|
|
|
|
Important details:
|
|
|
|
* All 8 bytes must be copied. An affine matrix coefficient is not one byte.
|
|
|
|
* Using a byte array and memcpy() keeps the representation explicit and avoids
|
|
strict-aliasing assumptions.
|
|
|
|
* AVX2 and AVX512 RAID GFNI RECX both use this precomputation.
|
|
|
|
* AES GFNI does not need this conversion because VGF2P8MULB consumes the
|
|
coefficient byte directly.
|
|
|
|
* The remaining repeated operation is the per-chunk broadcast of the 8-byte
|
|
affine matrix.
|
|
|
|
|
|
GFNI pre-expanded affine tables
|
|
-------------------------------
|
|
|
|
* Some high-performance generic erasure-code engines use pre-expanded GFNI
|
|
constants, for example a 32-byte representation ready for vector use.
|
|
|
|
* For SnapRAID this would increase the RAID affine table from about 2 KiB to
|
|
about 8 KiB.
|
|
|
|
* With large 256 KiB processing blocks, the small 8-byte entries plus
|
|
broadcast have a better cache-footprint tradeoff.
|
|
|
|
* No convincing reason was found to trade four times the table footprint for
|
|
fewer broadcasts.
|
|
|
|
* Decision: keep compact 8-byte affine matrices.
|
|
|
|
|
|
External implementation comparisons
|
|
-----------------------------------
|
|
|
|
Detailed comparisons are in OTHERS. The implementations studied include
|
|
specialized RAID engines, filesystem RAID, and general Reed-Solomon libraries.
|
|
|
|
The important conclusions are general rather than project-specific.
|
|
|
|
Classify an external advantage before trying to copy it:
|
|
|
|
algorithm advantage
|
|
the method performs genuinely less mathematical work
|
|
|
|
implementation advantage
|
|
the same work is scheduled, vectorized, or prepared better
|
|
|
|
workload-specific advantage
|
|
the method wins because the external workload has different block,
|
|
cache, parity, update, or latency assumptions
|
|
|
|
This distinction prevents a good optimization for another engine from becoming
|
|
an automatic SnapRAID TODO.
|
|
|
|
Memory traversal:
|
|
|
|
* Several modern high-throughput engines confirm that loading one source chunk
|
|
and updating multiple outputs from it is a strong general strategy.
|
|
|
|
* Other engines deliberately process cache-resident regions and repeatedly
|
|
update cached parity. That can be correct when parity is consumed again
|
|
immediately, but it is a different memory model from SnapRAID generation.
|
|
|
|
* Whole-region operations that keep one coefficient resident can be efficient
|
|
for generic erasure coding. For SnapRAID, the same loop orientation can
|
|
require rereading large source blocks and is normally a poor trade.
|
|
|
|
* The preferred order remains: preserve the one-pass source traversal, then use
|
|
spare registers, hoisting, and precomputation to reduce local coefficient
|
|
cost.
|
|
|
|
Specialization:
|
|
|
|
* SnapRAID can specialize aggressively because parity is limited to 1..6 and
|
|
the matrix structure is known.
|
|
|
|
* Generic engines may use large generated N-input/M-output kernel families,
|
|
general decoding matrices, structured solvers, or broader runtime machinery.
|
|
These are valuable for their scale but do not automatically reduce work in
|
|
the N <= 6 SnapRAID domain.
|
|
|
|
* The most useful external ideas are usually local: fuse multiple outputs,
|
|
precompute coefficient representations, reuse source splits, special-case
|
|
coefficient 0/1 where simple, and exploit algebraic fast paths.
|
|
|
|
Cache and stores:
|
|
|
|
* Store policy follows output lifetime. Cached stores are appropriate when an
|
|
algorithm immediately reloads parity; non-temporal stores are appropriate
|
|
when final parity is queued and consumed later.
|
|
|
|
* Therefore cached stores in another fast implementation are not evidence that
|
|
SnapRAID should change its MOVNT policy.
|
|
|
|
Recovery:
|
|
|
|
* General matrix inversion or structured Cauchy solvers become important at
|
|
larger scale. For at most six missing blocks, moving 256 KiB regions is far
|
|
more important than reducing the tiny matrix-setup cost.
|
|
|
|
* Algorithmic failure-case specializations are different: if a fixed recovery
|
|
case provably needs fewer GF multiplications, it remains worth measuring.
|
|
The specialized P+Q rec2of2 case is an example.
|
|
|
|
Runtime and architecture:
|
|
|
|
* Some engines benchmark implementations at startup, while others dispatch only
|
|
from CPU capabilities. Runtime calibration remains low priority for SnapRAID
|
|
because the kernel set is small and the workload is stable.
|
|
|
|
* Other engines support additional SIMD families such as SVE, RVV, or VSX.
|
|
These are useful porting references, not hot-path optimization tasks
|
|
without a target platform requirement.
|
|
|
|
|
|
Memory layout, displacement and stride noise
|
|
--------------------------------------------
|
|
|
|
* The memory layout is not a neutral detail.
|
|
|
|
* Fixed displacement between buffers was observed to help.
|
|
|
|
* STRIDE_NOISE behavior differs by CPU.
|
|
|
|
* On Intel, stride noise had little effect in the observed tests.
|
|
|
|
* On Zen 5, disabling stride noise caused a large throughput reduction.
|
|
|
|
* This indicates that hardware prefetch/cache behavior is already strongly
|
|
affected by the chosen buffer layout.
|
|
|
|
* Therefore new software-prefetch or traversal changes should not be assumed to
|
|
help. They must be benchmarked against the existing displacement/stride
|
|
behavior.
|
|
|
|
* This is another reason to avoid adding extra passes over 256 KiB blocks.
|
|
|
|
|
|
REC1 optimizations
|
|
------------------
|
|
|
|
The important normal REC1 case is already raid_rec1of1() with P and is close to
|
|
the minimum possible memory traffic.
|
|
|
|
For generic REC1, used when P is unavailable:
|
|
|
|
* AVX2 processes 64 bytes per iteration using two independent 32-byte chains.
|
|
|
|
* SSSE3 x86-64 processes 64 bytes per iteration using four independent 16-byte
|
|
chains. The x86-32 path keeps the smaller register-compatible fallback.
|
|
|
|
* NEON processes 64 bytes per iteration using four independent 16-byte chains.
|
|
|
|
The arithmetic work per byte is mostly unchanged. The purpose of the unrolling
|
|
is:
|
|
|
|
* reduce loop-control overhead
|
|
* expose more independent work
|
|
* improve instruction scheduling around shuffle/table operations
|
|
|
|
This is a local optimization, not an architectural change.
|
|
|
|
REC1 status:
|
|
|
|
* rec1of1/P is essentially closed.
|
|
|
|
* Generic non-P REC1 may still receive small local tuning if measurements show a
|
|
clear benefit.
|
|
|
|
* No major REC1 algorithmic optimization is currently expected.
|
|
|
|
|
|
REC2 optimizations
|
|
------------------
|
|
|
|
Generic non-GFNI REC2 table/broadcast hoisting is implemented.
|
|
|
|
AVX2 x86-64:
|
|
|
|
* keep the 8 low/high shuffle tables for V0..V3 resident in ymm8..ymm15
|
|
* keep the low-nibble mask resident
|
|
* split d0/d1 once per chunk
|
|
* reuse the split values for both recovered outputs
|
|
|
|
AVX512BW:
|
|
|
|
* keep the 8 low/high shuffle tables resident in zmm16..zmm23
|
|
* keep the low-nibble mask resident
|
|
* split d0/d1 once per chunk
|
|
* reuse the split values for both recovered outputs
|
|
|
|
SSSE3 x86-64:
|
|
|
|
* keep all four coefficient low/high tables in XMM registers
|
|
* split both deltas once
|
|
* reuse the split values for both outputs
|
|
|
|
NEON:
|
|
|
|
* keep the four low/high coefficient tables in v20..v27
|
|
* keep the low-nibble mask in v28
|
|
* process two independent 16-byte blocks per iteration
|
|
* split both deltas once per block
|
|
* reuse the split values for both outputs
|
|
* the 32-byte unrolled kernel uses 25 vector registers
|
|
|
|
Important portability result:
|
|
|
|
* x86-32 has only the low eight XMM/YMM registers available to these old ISA
|
|
paths.
|
|
|
|
* Optimizations that depend on xmm8..xmm15 or ymm8..ymm15 must remain restricted
|
|
to CONFIG_X86_64.
|
|
|
|
REC2 status:
|
|
|
|
* Generic REC2 local table/split reuse is implemented.
|
|
|
|
* Scalar int8 already has specialized rec2of2 P+Q recovery.
|
|
|
|
* Specialized SIMD rec2of2 P+Q is a possible further optimization.
|
|
|
|
|
|
Specialized rec2of2 P+Q
|
|
-----------------------
|
|
|
|
For failures of two data disks recovered specifically with P and Q, the system
|
|
can be reduced to:
|
|
|
|
Dy = C0 * Pd ^ C1 * Qd
|
|
Dx = Pd ^ Dy
|
|
|
|
This needs only two GF multiplications for Dy, followed by one XOR for Dx,
|
|
instead of the generic 2x2 reconstruction with four GF multiplications.
|
|
|
|
The scalar int8 implementation already uses this specialization.
|
|
|
|
SIMD REC2 backends use the generic 2x2 matrix solution even when
|
|
ip = { 0, 1 }.
|
|
|
|
Potential targets include:
|
|
|
|
* SSSE3
|
|
* AVX2
|
|
* AVX512BW
|
|
* AVX2 GFNI
|
|
* AVX512 GFNI
|
|
* NEON
|
|
|
|
The new rec2of2 benchmark row exists specifically to measure whether these
|
|
specializations are worth adding.
|
|
|
|
Practical priority remains low because simultaneous loss of two data disks is an
|
|
exceptional real-world case.
|
|
|
|
|
|
RECX delta handling
|
|
-------------------
|
|
|
|
RECX previously had avoidable temporary-buffer and repeated split work.
|
|
|
|
The optimized structure keeps the delta values for each data
|
|
chunk in SIMD registers.
|
|
|
|
For non-GFNI paths:
|
|
|
|
* each delta is split into low/high nibbles once
|
|
* the split values remain in SIMD registers while all recovered outputs are
|
|
calculated
|
|
* the same split values are reused across output rows
|
|
* stack temporary delta buffers are avoided on the optimized paths
|
|
|
|
For GFNI paths:
|
|
|
|
* the byte deltas remain directly in SIMD registers
|
|
* no nibble split is needed
|
|
|
|
This closes the old questions:
|
|
|
|
can pd[k] stay in a register longer?
|
|
can low/high nibble splits be reused across outputs?
|
|
|
|
For the optimized paths, the answer is yes and this work is implemented.
|
|
|
|
|
|
RECX shuffle-table pointer precomputation
|
|
----------------------------------------
|
|
|
|
Non-GFNI RECX previously performed coefficient address calculation inside the
|
|
hot loop:
|
|
|
|
load coefficient byte from V[]
|
|
multiply the coefficient index by 32
|
|
add the raid_gfmulpshufb base address
|
|
load low/high shuffle tables
|
|
|
|
The implementation precomputes in C:
|
|
|
|
T[j] = &raid_gfmulpshufb[V[j]][0][0];
|
|
|
|
The hot assembly then only performs:
|
|
|
|
load T[j]
|
|
load or broadcast low table at T[j] + 0
|
|
load or broadcast high table at T[j] + 16
|
|
|
|
This has two advantages:
|
|
|
|
1. It removes coefficient-index address arithmetic from the hot assembly.
|
|
|
|
2. It keeps C object layout and address calculation in C, leaving the assembly
|
|
focused on SIMD operations.
|
|
|
|
Optimized users include:
|
|
|
|
* AVX2 RECX x86-64
|
|
* AVX512BW RECX
|
|
* SSSE3 RECX x86-64
|
|
* NEON RECX
|
|
|
|
For x86-32 AVX2/SSSE3 fallbacks, T[] is not unnecessarily built when the old
|
|
fallback body does not use it.
|
|
|
|
The remaining cost is that the low/high tables are still loaded or broadcast for
|
|
every coefficient of every data chunk.
|
|
|
|
|
|
RECX register state
|
|
-------------------
|
|
|
|
The RECX structure already uses registers aggressively.
|
|
|
|
AVX512BW:
|
|
|
|
* up to six delta values are kept as low/high pairs in zmm0..zmm11
|
|
* zmm12..zmm15 are reconstruction temporaries/accumulators
|
|
* zmm31 keeps the low-nibble mask
|
|
* coefficient shuffle tables are still broadcast from T[] during reconstruction
|
|
|
|
AVX512 RAID GFNI:
|
|
|
|
* up to six byte deltas are kept in zmm0..zmm5
|
|
* temporary/accumulator registers are used for reconstruction
|
|
* coefficient affine matrices come directly from precomputed C[]
|
|
* one 8-byte affine matrix is still broadcast per coefficient per chunk
|
|
|
|
AVX2 RAID GFNI:
|
|
|
|
* the two 32-byte halves of up to six delta blocks occupy most of the available
|
|
YMM register file
|
|
* C[] removes indexed affine-table lookup
|
|
* register pressure leaves less room for keeping many coefficient matrices
|
|
resident
|
|
|
|
AVX2/SSSE3 non-GFNI:
|
|
|
|
* the optimized x86-64 paths keep pre-split deltas in vector registers
|
|
* register pressure is significantly higher than AVX512
|
|
* coefficient table residency is therefore much more constrained
|
|
|
|
NEON:
|
|
|
|
* pre-split deltas are kept in vector registers
|
|
* T[] provides direct pointers to low/high multiplication tables
|
|
* reconstruction still loads each coefficient table pair as needed
|
|
|
|
|
|
RECX coefficient residency
|
|
--------------------------
|
|
|
|
The main RECX local optimization question is:
|
|
|
|
can repeated coefficient loads/broadcasts be reduced by keeping useful
|
|
coefficient tables or affine matrices resident in spare vector registers?
|
|
|
|
This is most plausible on AVX512 because of the larger register file.
|
|
|
|
Possible experiments:
|
|
|
|
* keep one output row of coefficients resident across data chunks
|
|
|
|
* keep a useful prefix of coefficient tables resident when N permits it
|
|
|
|
* on AVX512 RAID GFNI, keep several 8-byte affine matrices broadcast in ZMM
|
|
registers instead of broadcasting them again for every chunk
|
|
|
|
* on AVX512BW, keep some low/high shuffle-table pairs resident
|
|
|
|
Constraints:
|
|
|
|
* Do not add a second pass over the 256 KiB block.
|
|
|
|
* Do not spill deltas or accumulators merely to save coefficient broadcasts.
|
|
|
|
* Do not add large N-specific kernel families unless measurements justify the
|
|
complexity.
|
|
|
|
* AVX2 and SSSE3 register pressure is already high, so the likely opportunity is
|
|
much smaller there.
|
|
|
|
* NEON has more architectural registers than old x86 SIMD but RECX already
|
|
uses a significant part of them; any residency experiment should be
|
|
measured carefully.
|
|
|
|
This is a narrow local problem. Delta residency, nibble reuse, coefficient
|
|
address precomputation, and GFNI affine lookup precomputation are already
|
|
implemented.
|
|
|
|
|
|
Recovery optimization policy
|
|
----------------------------
|
|
|
|
For recovery, avoid new architecture-level changes unless a very strong case
|
|
appears.
|
|
|
|
Preferred optimizations are basic/local:
|
|
|
|
* hoist loop-invariant values
|
|
* keep masks/tables/matrices resident when register pressure allows it
|
|
* avoid repeated loads or broadcasts
|
|
* initialize accumulators from the first useful term
|
|
* keep address calculations in C when doing so simplifies inline assembly
|
|
* exploit coefficient 0 or 1 only when it stays simple and does not add harmful
|
|
branching or complexity
|
|
* use limited spatial unrolling only when it exposes ILP without excessive
|
|
register pressure
|
|
|
|
Avoid unless new measurements provide a concrete reason:
|
|
|
|
* raid_delta_gen() fusion
|
|
* extra full passes over source blocks
|
|
* aggressive spatial unrolling
|
|
* software prefetch without clear evidence
|
|
* changing MOVNT policy
|
|
* major loop-orientation changes
|
|
* large pre-expanded lookup tables
|
|
* broad runtime calibration machinery
|
|
|
|
|
|
Recovery matrix setup and inversion
|
|
-----------------------------------
|
|
|
|
* Recovery matrices are at most 6x6 for the 1..6 parity implementation.
|
|
|
|
* The arithmetic cost of constructing and inverting such a matrix is tiny
|
|
compared with reading and writing 256 KiB data regions.
|
|
|
|
* More elaborate structured Cauchy solvers, inversion caches, or asymptotically
|
|
faster coding methods are therefore not useful performance targets for the
|
|
1..6 parity limit.
|
|
|
|
* A dedicated multithreaded inversion stress test exists to validate the matrix
|
|
code independently of the data-path SIMD kernels.
|
|
|
|
* At parity counts far beyond six, matrix construction and structured
|
|
solvers should be re-evaluated together with the whole memory traversal.
|
|
They should not be optimized in isolation.
|
|
|
|
|
|
Inline assembly and portability constraints
|
|
-------------------------------------------
|
|
|
|
The SIMD code intentionally uses multiple small or large asm volatile blocks and
|
|
keeps SIMD state live across them.
|
|
|
|
Important project constraints:
|
|
|
|
* Assembly blocks that dereference pointers internally must model memory effects
|
|
appropriately. RECX pointer-based blocks use a "memory" clobber.
|
|
|
|
* The x86 SSE/AVX end helpers declare SIMD clobbers needed by the project ABI
|
|
strategy, including the Windows x64 callee-saved XMM registers.
|
|
|
|
* The NEON end helper declares v0..v31 clobbered so that GCC/Clang preserve
|
|
AAPCS64 callee-saved v8..v15 when required.
|
|
|
|
* The end-of-function dummy-clobber mechanism is a documented project technique.
|
|
It is not a fully explicit extended-asm dataflow model, so future compiler
|
|
portability changes should be reviewed carefully.
|
|
|
|
* Do not assume x86-32 has xmm8..xmm15 or ymm8..ymm15.
|
|
|
|
* AArch32 NEON is a separate backend. Do not assume the AArch64 register or ABI
|
|
model when changing ARM32 assembly.
|
|
|
|
* RAID thread-safety requirements are documented. Avoid introducing mutable
|
|
global calibration or dispatch state without preserving those requirements.
|
|
|
|
* RAID block sizes passed to the SIMD code are multiples of 64, so the
|
|
32-byte and 64-byte unrolled loops do not need tail handling.
|
|
|
|
These are established implementation constraints, not correctness findings.
|
|
|
|
|
|
Scaling beyond 6 parity levels
|
|
------------------------------
|
|
|
|
The implementation deliberately targets at most six parity levels.
|
|
|
|
* At this scale, fixed-parity specialization keeps matrix setup small and
|
|
enables tight kernels with explicit register allocation.
|
|
|
|
* General-purpose erasure-code engines demonstrate other designs, including
|
|
generic matrix kernels, generated multi-input/multi-output kernels, and
|
|
algorithms intended for much larger shard counts.
|
|
|
|
* Those designs solve a different scaling problem. They should not be adopted
|
|
merely to generalize a domain where the fixed 1..6 structure is useful.
|
|
|
|
* If the parity limit is increased substantially, generic intrinsics or
|
|
generated kernels should be compared with handwritten specialization.
|
|
Memory traversal
|
|
must remain a first-class design constraint.
|
|
|
|
* Scaling beyond six parity levels is primarily a functional and architectural
|
|
change, not an unresolved performance problem of the 1..6 implementation.
|
|
|
|
|
|
Short list of ideas already tried or rejected
|
|
---------------------------------------------
|
|
|
|
* AVX512 128-byte generation processing:
|
|
tried, can be slower.
|
|
|
|
* Non-GFNI AVX512BW Horner:
|
|
tried on Zen 5, no improvement.
|
|
|
|
* Standalone VPTERNLOGQ substitutions:
|
|
tried on Zen 5, no improvement. This does not reject integrated use as part
|
|
of the profitable GEN2/GEN3 resident-table kernels.
|
|
|
|
* VPBLENDVB x2:
|
|
tried, no benefit / worse.
|
|
|
|
* MOVNT -> cached stores:
|
|
not appropriate for the normal queued disk-write workload; keep MOVNT.
|
|
|
|
* GFNI 32-byte pre-expanded affine constants:
|
|
rejected due to larger cache footprint.
|
|
|
|
* Split outputs into multiple full source passes:
|
|
rejected because 256 KiB source blocks would be reread.
|
|
|
|
* Whole-region/output-major rewrites intended mainly to keep one coefficient
|
|
resident:
|
|
poor fit when they add source rereads. Use register residency inside the
|
|
existing traversal instead.
|
|
|
|
* RECX source-major rewrite:
|
|
low priority; hot tiny delta working set and register-pressure tradeoff.
|
|
|
|
* raid_delta_gen() + REC fusion:
|
|
technically valid but intentionally deprioritized because the normal
|
|
rec1of1/P case bypasses raid_delta_gen() already.
|
|
|
|
* Software prefetch:
|
|
no strong reason to add it given the existing long streams and proven
|
|
sensitivity to displacement/stride tuning.
|
|
|
|
* Runtime backend microbenchmark selection:
|
|
possible, but low value relative to complexity.
|
|
|
|
* Large generic N-input/M-output kernel families:
|
|
useful for general erasure coding, but not justified for the fixed
|
|
1..6 parity domain without a concrete benchmark need.
|
|
|
|
* Structured or asymptotically faster matrix solvers:
|
|
unnecessary at N <= 6 because matrix setup is tiny compared with data
|
|
movement.
|