miden-crypto/src/hash/rescue/arch/x86_64_avx2.rs
src/hash/rescue/arch/x86_64_avx2.rsRaw
1use core::arch::x86_64::*;
2
3// The following AVX2 implementation has been copied from plonky2:
4// https://github.com/0xPolygonZero/plonky2/blob/main/plonky2/src/hash/arch/x86_64/poseidon_goldilocks_avx2_bmi2.rs
5
6// Preliminary notes:
7// 1. AVX does not support addition with carry but 128-bit (2-word) addition can be easily emulated.
8// The method recognizes that for a + b overflowed iff (a + b) < a:
9// - i. res_lo = a_lo + b_lo
10// - ii. carry_mask = res_lo < a_lo
11// - iii. res_hi = a_hi + b_hi - carry_mask Notice that carry_mask is subtracted, not added.
12// This is because AVX comparison instructions return -1 (all bits 1) for true and 0 for false.
13//
14// 2. AVX does not have unsigned 64-bit comparisons. Those can be emulated with signed comparisons
15// by recognizing that a <u b iff a + (1 << 63) <s b + (1 << 63), where the addition wraps around
16// and the comparisons are unsigned and signed respectively. The shift function adds/subtracts 1
17// << 63 to enable this trick. Addition with carry example:
18// - i. a_lo_s = shift(a_lo)
19// - ii. res_lo_s = a_lo_s + b_lo
20// - iii. carry_mask = res_lo_s <s a_lo_s
21// - iv. res_lo = shift(res_lo_s)
22// - v. res_hi = a_hi + b_hi - carry_mask The suffix _s denotes a value that has been
23// shifted by 1 << 63.
24//
25// The result of addition is shifted if exactly one of the operands is shifted, as is the case on
26// line ii. Line iii. performs a signed comparison res_lo_s <s a_lo_s on shifted values to
27// emulate unsigned comparison res_lo <u a_lo on unshifted values. Finally, line iv. reverses the
28// shift so the result can be returned. When performing a chain of calculations, we can often
29// save instructions by letting the shift propagate through and only undoing it when necessary.
30// For example, to compute the addition of three two-word (128-bit) numbers we can do:
31// - i. a_lo_s = shift(a_lo)
32// - ii. tmp_lo_s = a_lo_s + b_lo
33// - iii. tmp_carry_mask = tmp_lo_s <s a_lo_s
34// - iv. tmp_hi = a_hi + b_hi - tmp_carry_mask
35// - v. res_lo_s = tmp_lo_s + c_lo vi. res_carry_mask = res_lo_s <s tmp_lo_s
36// - vii. res_lo = shift(res_lo_s)
37// - viii. res_hi = tmp_hi + c_hi - res_carry_mask
38// Notice that the above 3-value addition still only requires two calls to shift, just like our
39// 2-value addition.
40
41#[inline(always)]
42pub fn branch_hint() {
43 // NOTE: These are the currently supported assembly architectures. See the
44 // [nightly reference](https://doc.rust-lang.org/nightly/reference/inline-assembly.html) for
45 // the most up-to-date list.
46 #[cfg(any(
47 target_arch = "aarch64",
48 target_arch = "arm",
49 target_arch = "riscv32",
50 target_arch = "riscv64",
51 target_arch = "x86",
52 target_arch = "x86_64",
53 ))]
54 unsafe {
55 core::arch::asm!("", options(nomem, nostack, preserves_flags));
56 }
57}
58
59macro_rules! map3 {
60 ($f:ident:: < $l:literal > , $v:ident) => {
61 ($f::<$l>($v.0), $f::<$l>($v.1), $f::<$l>($v.2))
62 };
63 ($f:ident:: < $l:literal > , $v1:ident, $v2:ident) => {
64 ($f::<$l>($v1.0, $v2.0), $f::<$l>($v1.1, $v2.1), $f::<$l>($v1.2, $v2.2))
65 };
66 ($f:ident, $v:ident) => {
67 ($f($v.0), $f($v.1), $f($v.2))
68 };
69 ($f:ident, $v0:ident, $v1:ident) => {
70 ($f($v0.0, $v1.0), $f($v0.1, $v1.1), $f($v0.2, $v1.2))
71 };
72 ($f:ident,rep $v0:ident, $v1:ident) => {
73 ($f($v0, $v1.0), $f($v0, $v1.1), $f($v0, $v1.2))
74 };
75
76 ($f:ident, $v0:ident,rep $v1:ident) => {
77 ($f($v0.0, $v1), $f($v0.1, $v1), $f($v0.2, $v1))
78 };
79}
80
81#[inline(always)]
82unsafe fn square3(
83 x: (__m256i, __m256i, __m256i),
84) -> ((__m256i, __m256i, __m256i), (__m256i, __m256i, __m256i)) {
85 let x_hi = {
86 // Move high bits to low position. The high bits of x_hi are ignored. Swizzle is faster than
87 // bitshift. This instruction only has a floating-point flavor, so we cast to/from float.
88 // This is safe and free.
89 let x_ps = map3!(_mm256_castsi256_ps, x);
90 let x_hi_ps = map3!(_mm256_movehdup_ps, x_ps);
91 map3!(_mm256_castps_si256, x_hi_ps)
92 };
93
94 // All pairwise multiplications.
95 let mul_ll = map3!(_mm256_mul_epu32, x, x);
96 let mul_lh = map3!(_mm256_mul_epu32, x, x_hi);
97 let mul_hh = map3!(_mm256_mul_epu32, x_hi, x_hi);
98
99 // Bignum addition, but mul_lh is shifted by 33 bits (not 32).
100 let mul_ll_hi = map3!(_mm256_srli_epi64::<33>, mul_ll);
101 let t0 = map3!(_mm256_add_epi64, mul_lh, mul_ll_hi);
102 let t0_hi = map3!(_mm256_srli_epi64::<31>, t0);
103 let res_hi = map3!(_mm256_add_epi64, mul_hh, t0_hi);
104
105 // Form low result by adding the mul_ll and the low 31 bits of mul_lh (shifted to the high
106 // position).
107 let mul_lh_lo = map3!(_mm256_slli_epi64::<33>, mul_lh);
108 let res_lo = map3!(_mm256_add_epi64, mul_ll, mul_lh_lo);
109
110 (res_lo, res_hi)
111}
112
113#[inline(always)]
114unsafe fn mul3(
115 x: (__m256i, __m256i, __m256i),
116 y: (__m256i, __m256i, __m256i),
117) -> ((__m256i, __m256i, __m256i), (__m256i, __m256i, __m256i)) {
118 let epsilon = _mm256_set1_epi64x(0xffffffff);
119 let x_hi = {
120 // Move high bits to low position. The high bits of x_hi are ignored. Swizzle is faster than
121 // bitshift. This instruction only has a floating-point flavor, so we cast to/from float.
122 // This is safe and free.
123 let x_ps = map3!(_mm256_castsi256_ps, x);
124 let x_hi_ps = map3!(_mm256_movehdup_ps, x_ps);
125 map3!(_mm256_castps_si256, x_hi_ps)
126 };
127 let y_hi = {
128 let y_ps = map3!(_mm256_castsi256_ps, y);
129 let y_hi_ps = map3!(_mm256_movehdup_ps, y_ps);
130 map3!(_mm256_castps_si256, y_hi_ps)
131 };
132
133 // All four pairwise multiplications
134 let mul_ll = map3!(_mm256_mul_epu32, x, y);
135 let mul_lh = map3!(_mm256_mul_epu32, x, y_hi);
136 let mul_hl = map3!(_mm256_mul_epu32, x_hi, y);
137 let mul_hh = map3!(_mm256_mul_epu32, x_hi, y_hi);
138
139 // Bignum addition
140 // Extract high 32 bits of mul_ll and add to mul_hl. This cannot overflow.
141 let mul_ll_hi = map3!(_mm256_srli_epi64::<32>, mul_ll);
142 let t0 = map3!(_mm256_add_epi64, mul_hl, mul_ll_hi);
143 // Extract low 32 bits of t0 and add to mul_lh. Again, this cannot overflow.
144 // Also, extract high 32 bits of t0 and add to mul_hh.
145 let t0_lo = map3!(_mm256_and_si256, t0, rep epsilon);
146 let t0_hi = map3!(_mm256_srli_epi64::<32>, t0);
147 let t1 = map3!(_mm256_add_epi64, mul_lh, t0_lo);
148 let t2 = map3!(_mm256_add_epi64, mul_hh, t0_hi);
149 // Lastly, extract the high 32 bits of t1 and add to t2.
150 let t1_hi = map3!(_mm256_srli_epi64::<32>, t1);
151 let res_hi = map3!(_mm256_add_epi64, t2, t1_hi);
152
153 // Form res_lo by combining the low half of mul_ll with the low half of t1 (shifted into high
154 // position).
155 let t1_lo = {
156 let t1_ps = map3!(_mm256_castsi256_ps, t1);
157 let t1_lo_ps = map3!(_mm256_moveldup_ps, t1_ps);
158 map3!(_mm256_castps_si256, t1_lo_ps)
159 };
160 let res_lo = map3!(_mm256_blend_epi32::<0xaa>, mul_ll, t1_lo);
161
162 (res_lo, res_hi)
163}
164
165/// Addition, where the second operand is `0 <= y < 0xffffffff00000001`.
166#[inline(always)]
167unsafe fn add_small(
168 x_s: (__m256i, __m256i, __m256i),
169 y: (__m256i, __m256i, __m256i),
170) -> (__m256i, __m256i, __m256i) {
171 let res_wrapped_s = map3!(_mm256_add_epi64, x_s, y);
172 let mask = map3!(_mm256_cmpgt_epi32, x_s, res_wrapped_s);
173 let wrapback_amt = map3!(_mm256_srli_epi64::<32>, mask); // EPSILON if overflowed else 0.
174 let res_s = map3!(_mm256_add_epi64, res_wrapped_s, wrapback_amt);
175 res_s
176}
177
178#[inline(always)]
179unsafe fn maybe_adj_sub(res_wrapped_s: __m256i, mask: __m256i) -> __m256i {
180 // The subtraction is very unlikely to overflow so we're best off branching.
181 // The even u32s in `mask` are meaningless, so we want to ignore them. `_mm256_testz_pd`
182 // branches depending on the sign bit of double-precision (64-bit) floats. Bit cast `mask` to
183 // floating-point (this is free).
184 let mask_pd = _mm256_castsi256_pd(mask);
185 // `_mm256_testz_pd(mask_pd, mask_pd) == 1` iff all sign bits are 0, meaning that underflow
186 // did not occur for any of the vector elements.
187 if _mm256_testz_pd(mask_pd, mask_pd) == 1 {
188 res_wrapped_s
189 } else {
190 branch_hint();
191 // Highly unlikely: underflow did occur. Find adjustment per element and apply it.
192 let adj_amount = _mm256_srli_epi64::<32>(mask); // EPSILON if underflow.
193 _mm256_sub_epi64(res_wrapped_s, adj_amount)
194 }
195}
196
197/// Addition, where the second operand is much smaller than `0xffffffff00000001`.
198#[inline(always)]
199unsafe fn sub_tiny(
200 x_s: (__m256i, __m256i, __m256i),
201 y: (__m256i, __m256i, __m256i),
202) -> (__m256i, __m256i, __m256i) {
203 let res_wrapped_s = map3!(_mm256_sub_epi64, x_s, y);
204 let mask = map3!(_mm256_cmpgt_epi32, res_wrapped_s, x_s);
205 let res_s = map3!(maybe_adj_sub, res_wrapped_s, mask);
206 res_s
207}
208
209#[inline(always)]
210unsafe fn reduce3(
211 (lo0, hi0): ((__m256i, __m256i, __m256i), (__m256i, __m256i, __m256i)),
212) -> (__m256i, __m256i, __m256i) {
213 let sign_bit = _mm256_set1_epi64x(i64::MIN);
214 let epsilon = _mm256_set1_epi64x(0xffffffff);
215 let lo0_s = map3!(_mm256_xor_si256, lo0, rep sign_bit);
216 let hi_hi0 = map3!(_mm256_srli_epi64::<32>, hi0);
217 let lo1_s = sub_tiny(lo0_s, hi_hi0);
218 let t1 = map3!(_mm256_mul_epu32, hi0, rep epsilon);
219 let lo2_s = add_small(lo1_s, t1);
220 let lo2 = map3!(_mm256_xor_si256, lo2_s, rep sign_bit);
221 lo2
222}
223
224#[inline(always)]
225unsafe fn mul_reduce(
226 a: (__m256i, __m256i, __m256i),
227 b: (__m256i, __m256i, __m256i),
228) -> (__m256i, __m256i, __m256i) {
229 reduce3(mul3(a, b))
230}
231
232#[inline(always)]
233unsafe fn square_reduce(state: (__m256i, __m256i, __m256i)) -> (__m256i, __m256i, __m256i) {
234 reduce3(square3(state))
235}
236
237#[inline(always)]
238unsafe fn exp_acc(
239 high: (__m256i, __m256i, __m256i),
240 low: (__m256i, __m256i, __m256i),
241 exp: usize,
242) -> (__m256i, __m256i, __m256i) {
243 let mut result = high;
244 for _ in 0..exp {
245 result = square_reduce(result);
246 }
247 mul_reduce(result, low)
248}
249
250#[inline(always)]
251unsafe fn do_apply_sbox(state: (__m256i, __m256i, __m256i)) -> (__m256i, __m256i, __m256i) {
252 let state2 = square_reduce(state);
253 let state4_unreduced = square3(state2);
254 let state3_unreduced = mul3(state2, state);
255 let state4 = reduce3(state4_unreduced);
256 let state3 = reduce3(state3_unreduced);
257 let state7_unreduced = mul3(state3, state4);
258 let state7 = reduce3(state7_unreduced);
259 state7
260}
261
262#[inline(always)]
263unsafe fn do_apply_inv_sbox(state: (__m256i, __m256i, __m256i)) -> (__m256i, __m256i, __m256i) {
264 // compute base^10540996611094048183 using 72 multiplications per array element
265 // 10540996611094048183 = b1001001001001001001001001001000110110110110110110110110110110111
266
267 // compute base^10
268 let t1 = square_reduce(state);
269
270 // compute base^100
271 let t2 = square_reduce(t1);
272
273 // compute base^100100
274 let t3 = exp_acc(t2, t2, 3);
275
276 // compute base^100100100100
277 let t4 = exp_acc(t3, t3, 6);
278
279 // compute base^100100100100100100100100
280 let t5 = exp_acc(t4, t4, 12);
281
282 // compute base^100100100100100100100100100100
283 let t6 = exp_acc(t5, t3, 6);
284
285 // compute base^1001001001001001001001001001000100100100100100100100100100100
286 let t7 = exp_acc(t6, t6, 31);
287
288 // compute base^1001001001001001001001001001000110110110110110110110110110110111
289 let a = square_reduce(square_reduce(mul_reduce(square_reduce(t7), t6)));
290 let b = mul_reduce(t1, mul_reduce(t2, state));
291 mul_reduce(a, b)
292}
293
294#[inline(always)]
295unsafe fn avx2_load(state: &[u64; 12]) -> (__m256i, __m256i, __m256i) {
296 (
297 _mm256_loadu_si256((&state[0..4]).as_ptr().cast::<__m256i>()),
298 _mm256_loadu_si256((&state[4..8]).as_ptr().cast::<__m256i>()),
299 _mm256_loadu_si256((&state[8..12]).as_ptr().cast::<__m256i>()),
300 )
301}
302
303#[inline(always)]
304unsafe fn avx2_store(buf: &mut [u64; 12], state: (__m256i, __m256i, __m256i)) {
305 _mm256_storeu_si256((&mut buf[0..4]).as_mut_ptr().cast::<__m256i>(), state.0);
306 _mm256_storeu_si256((&mut buf[4..8]).as_mut_ptr().cast::<__m256i>(), state.1);
307 _mm256_storeu_si256((&mut buf[8..12]).as_mut_ptr().cast::<__m256i>(), state.2);
308}
309
310#[inline(always)]
311pub unsafe fn apply_sbox(buffer: &mut [u64; 12]) {
312 let mut state = avx2_load(&buffer);
313 state = do_apply_sbox(state);
314 avx2_store(buffer, state);
315}
316
317#[inline(always)]
318pub unsafe fn apply_inv_sbox(buffer: &mut [u64; 12]) {
319 let mut state = avx2_load(&buffer);
320 state = do_apply_inv_sbox(state);
321 avx2_store(buffer, state);
322}