Looking for expert @njuffa review on a CUDA device function that chains
SHA-256 followed by RIPEMD-160 entirely in registers (no shared
memory, no global stores between the two hashes). Target architecture
is sm_89 (Ada Lovelace).
WHAT THE FUNCTION DOES
Single device function _GetHash160Comp() that:
- Takes a 256-bit input plus a 1-byte parity flag
- Runs one SHA-256 block transform (64 rounds, 33-byte payload)
- Pipes the 8 × uint32_t SHA-256 output directly into
RIPEMD-160 as register-resident input (no memory roundtrip) - Runs one RIPEMD-160 block transform (80 rounds with parallel
left/right lines) - Writes the final 160-bit digest
No shared, no constant lookup tables for SHA-256 K.
RIPEMD-160 still uses an 8-element constant K160 array.
WHAT IS ALREADY IMPLEMENTED
-
SHA-256 K-CONSTANTS AS IMMEDIATES
All 64 round constants are define K0..K63 macros, expanded
inline at each round. This eliminates the typical constant
memory loads (LDC instructions in SASS) and lets the compiler
fuse them as 32-bit immediates into the IADD3/IMAD pipeline. -
ROTR VIA __funnelshift_r
ROR is implemented as funnel shift (SHF.R.WRAP on Ada) instead
of (x>>n)|(x<<(32-n)). This produces single-instruction rotation
in SASS rather than the 3-instruction shift/shift/OR pattern. -
PIPE-PARALLEL ROUND STEP
Each SHA-256 round computes the next round’s S1/Ch/S0/Maj while
committing the current round’s state update. Macro
SHA256_STEP_PIPE_K does:t1 = h + s1e + ch + Ki + Wi; aN = t1 + s0a + maj; eN = d + t1; // already compute s1eN/chN/s0aN/majN for next iteration s1eN = S1(eN); chN = Ch(eN,e,f); s0aN = S0(aN); majN = Maj(aN,a,b);Goal: expose ILP to the scheduler so the next round’s
computation overlaps with the current round’s state rotation. -
PHASE SPLITTING (REGISTER KILL ZONES)
The 64 rounds are wrapped into four C++ scope blocks { } at
rounds 0-15, 16-31, 32-47, 48-63. Same for RIPEMD-160. The
intent is to give the compiler explicit lifetime cutoffs so
intermediate temporaries don’t keep registers alive across
phase boundaries. -
WMIX_INIT_REGS / WMIX_REGS
Message-schedule update done entirely in registers — w0..w15
are uint32_t locals throughout the entire transform. No
memory backing for the W array. Constants from the hardcoded
33-byte payload (e.g. 0x00A50000u, 0x10420023u, 0x00000108u
length encoding) are folded into the first WMIX directly. -
SHA-256 → RIPEMD-160 BRIDGE
The 8 × uint32_t SHA-256 output is byte-swapped via
__byte_perm and passed as 8 explicit register arguments to
RIPEMD160TransformRegs(). The remaining 8 input words are
compile-time constants (0x80, zero padding, length 256).
Compiler should be able to fold these as immediates. -
RIPEMD-160 PARALLEL LINES INTERLEAVED
The two RIPEMD-160 lines (a1..e1 and a2..e2) are written
alternating in source order:R11(a1,b1,c1,d1,e1, W(0), 11); R12(a2,b2,c2,d2,e2, W(5), 8); R11(e1,a1,b1,c1,d1, W(1), 14); R12(e2,a2,b2,c2,d2, W(14), 9); ...Goal: maximize ILP by giving the scheduler two independent
dependency chains visible at any point. -
EARLY FINAL COMBINATION
In the last RIPEMD-160 phase, s[2] is computed early because
neither e1 nor a2 change after that point. This shortens the
critical path on the final state combination by one round
worth of dependency.
QUESTIONS FOR REVIEW
Q1: REGISTER PRESSURE
Both transforms together push the live state high. SHA-256
needs 8 state regs + 16 W regs + 4 pipe regs (s1e, ch, s0a,
maj) + temps = ~30+ live across the full transform. RIPEMD-160
needs 10 state regs (two parallel lines) + 16 W regs + temps.
Both are inlined into the same kernel.
Is the phase-splitting via C++ scope blocks actually achieving
the intended register kill, or does the Ada compiler already
do liveness analysis well enough that the scopes are no-ops?
Has anyone verified via SASS that registers are actually
being recycled across the scope boundaries?
Q2: PIPE-PARALLEL ROUND IDIOM
The SHA256_STEP_PIPE_K macro speculatively computes
s1eN/chN/s0aN/majN for the NEXT round while finishing the
CURRENT round. Is this still beneficial on Ada with its
improved scheduler, or is it actually counterproductive
(extra register pressure for marginal ILP gain)?
Q3: __byte_perm BRIDGE
The SHA-256 → RIPEMD-160 bridge does 8 × __byte_perm to
byte-swap the digest. On Ada, __byte_perm maps to PRMT, which
has a fixed latency and competes for the same pipeline as
the integer ALU. Could a manually-unrolled rotation
(ROL16 + AND mask) be faster, or is PRMT genuinely the
fastest path?
Q4: RIPEMD-160 INTERLEAVING
The alternating R11/R12 pattern in source code is intended to
expose two independent dependency chains to the scheduler.
Does the Ada compiler actually preserve this interleaving in
SASS, or does it reorder back into “all line-1, then all
line-2”? If the latter, is there a way to force the
interleaving (e.g. asm volatile barriers between R1x calls)?
Q5: K160 CONSTANT LOADS
RIPEMD-160 still uses constant K160[8] indexed by round.
Would converting these to 8 define immediates (like SHA-256 K)
likely improve SASS, or are LDC instructions on the constant
cache effectively free on Ada due to the broadcast cache?
Q6: f1/f2/f3/f4/f5 BOOLEAN FUNCTIONS
The RIPEMD-160 round functions f1..f5 use mixed AND/OR/XOR/NOT
patterns. Has anyone tried encoding these via LOP3 with
explicit ternary truth tables on Ada? The compiler often
misses LOP3 fusion opportunities for 3-operand boolean
expressions.
hash_sass.txt (452.5 KB)
I’ll attach the relevant SASS dump
Any feedback on the questions above — particularly Q1 (register
kill zones) and Q4 (interleaving preservation) — would be
extremely valuable. I want to understand WHY a given technique
helps or hurts on Ada, not just receive a black-box rewrite.