Skip to main content

binius_field/arch/x86_64/
m128.rs

1// Copyright 2024-2025 Irreducible Inc.
2// Copyright 2026 The Binius Developers
3
4use std::{
5	arch::x86_64::*,
6	mem,
7	ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, BitXor, BitXorAssign, Not, Shl, Shr},
8};
9
10use binius_utils::{
11	DeserializeBytes, FixedSizeSerializeBytes, SerializationError, SerializeBytes,
12	bytes::{Buf, BufMut},
13	serialization::{assert_enough_data_for, assert_enough_space_for},
14};
15use bytemuck::{Pod, Zeroable};
16use rand::{
17	distr::{Distribution, StandardUniform},
18	prelude::*,
19};
20use seq_macro::seq;
21
22use crate::{
23	BinaryField,
24	divisible::{Divisible, impl_divisible_memcast, impl_divisible_self},
25	packed_fields::primitive::PackedPrimitiveType,
26	underlier::{SmallU, Underlier, impl_divisible_bitmask},
27};
28
29pub const fn m128i_from_u128(x: u128) -> __m128i {
30	// Static assertion that u128 and __m128i have equal alignment
31	let _: [(); align_of::<u128>()] = [(); align_of::<__m128i>()];
32	unsafe { mem::transmute(x) }
33}
34
35/// 128-bit value that is used for 128-bit SIMD operations
36#[derive(Copy, Clone)]
37#[repr(transparent)]
38pub struct M128(pub(super) __m128i);
39
40impl M128 {
41	#[inline(always)]
42	pub const fn from_u128(val: u128) -> Self {
43		Self(m128i_from_u128(val))
44	}
45}
46
47impl From<__m128i> for M128 {
48	#[inline(always)]
49	fn from(value: __m128i) -> Self {
50		Self(value)
51	}
52}
53
54impl From<u128> for M128 {
55	fn from(value: u128) -> Self {
56		Self(m128i_from_u128(value))
57	}
58}
59
60impl From<u64> for M128 {
61	fn from(value: u64) -> Self {
62		Self::from(value as u128)
63	}
64}
65
66impl From<u32> for M128 {
67	fn from(value: u32) -> Self {
68		Self::from(value as u128)
69	}
70}
71
72impl From<u16> for M128 {
73	fn from(value: u16) -> Self {
74		Self::from(value as u128)
75	}
76}
77
78impl From<u8> for M128 {
79	fn from(value: u8) -> Self {
80		Self::from(value as u128)
81	}
82}
83
84impl<const N: usize> From<SmallU<N>> for M128 {
85	fn from(value: SmallU<N>) -> Self {
86		Self::from(value.val() as u128)
87	}
88}
89
90impl From<M128> for u128 {
91	fn from(value: M128) -> Self {
92		const {
93			assert!(
94				align_of::<u128>() == 16,
95				"the store below needs a 16-byte aligned destination"
96			);
97		}
98		let mut result = 0u128;
99		unsafe {
100			// Safety: u128 is 16-byte aligned, as the const assertion above checks.
101			_mm_store_si128(&raw mut result as *mut __m128i, value.0);
102		};
103		result
104	}
105}
106
107impl From<M128> for __m128i {
108	#[inline(always)]
109	fn from(value: M128) -> Self {
110		value.0
111	}
112}
113
114impl SerializeBytes for M128 {
115	fn serialize(&self, mut write_buf: impl BufMut) -> Result<(), SerializationError> {
116		assert_enough_space_for(&write_buf, std::mem::size_of::<Self>())?;
117
118		let raw_value: u128 = (*self).into();
119
120		write_buf.put_u128_le(raw_value);
121		Ok(())
122	}
123}
124
125impl DeserializeBytes for M128 {
126	fn deserialize(mut read_buf: impl Buf) -> Result<Self, SerializationError>
127	where
128		Self: Sized,
129	{
130		assert_enough_data_for(&read_buf, std::mem::size_of::<Self>())?;
131
132		let raw_value = read_buf.get_u128_le();
133
134		Ok(Self::from(raw_value))
135	}
136}
137
138impl FixedSizeSerializeBytes for M128 {
139	const BYTE_SIZE: usize = 16;
140}
141
142impl_divisible_bitmask!(M128, 1, 2, 4);
143
144impl Default for M128 {
145	#[inline(always)]
146	fn default() -> Self {
147		Self(unsafe { _mm_setzero_si128() })
148	}
149}
150
151impl BitAnd for M128 {
152	type Output = Self;
153
154	#[inline(always)]
155	fn bitand(self, rhs: Self) -> Self::Output {
156		Self(unsafe { _mm_and_si128(self.0, rhs.0) })
157	}
158}
159
160impl BitAndAssign for M128 {
161	#[inline(always)]
162	fn bitand_assign(&mut self, rhs: Self) {
163		*self = *self & rhs;
164	}
165}
166
167impl BitOr for M128 {
168	type Output = Self;
169
170	#[inline(always)]
171	fn bitor(self, rhs: Self) -> Self::Output {
172		Self(unsafe { _mm_or_si128(self.0, rhs.0) })
173	}
174}
175
176impl BitOrAssign for M128 {
177	#[inline(always)]
178	fn bitor_assign(&mut self, rhs: Self) {
179		*self = *self | rhs;
180	}
181}
182
183impl BitXor for M128 {
184	type Output = Self;
185
186	#[inline(always)]
187	fn bitxor(self, rhs: Self) -> Self::Output {
188		Self(unsafe { _mm_xor_si128(self.0, rhs.0) })
189	}
190}
191
192impl BitXorAssign for M128 {
193	#[inline(always)]
194	fn bitxor_assign(&mut self, rhs: Self) {
195		*self = *self ^ rhs;
196	}
197}
198
199impl Not for M128 {
200	type Output = Self;
201
202	fn not(self) -> Self::Output {
203		const ONES: M128 = M128::from_u128(u128::MAX);
204
205		self ^ ONES
206	}
207}
208
209/// `std::cmp::max` isn't const, so we need our own implementation
210const fn max_i32(left: i32, right: i32) -> i32 {
211	if left > right { left } else { right }
212}
213
214/// This solution shows 4X better performance.
215/// We have to use macro because parameter `count` in _mm_slli_epi64/_mm_srli_epi64 should be passed
216/// as constant and Rust currently doesn't allow passing expressions (`count - 64`) where variable
217/// is a generic constant parameter. Source: <https://stackoverflow.com/questions/34478328/the-best-way-to-shift-a-m128i/34482688#34482688>
218macro_rules! bitshift_128b {
219	($val:expr, $shift:ident, $byte_shift:ident, $bit_shift_64:ident, $bit_shift_64_opposite:ident, $or:ident) => {
220		unsafe {
221			let carry = $byte_shift($val, 8);
222			seq!(N in 64..128 {
223				if $shift == N {
224					return $bit_shift_64(
225						carry,
226						crate::arch::x86_64::m128::max_i32((N - 64) as i32, 0) as _,
227					).into();
228				}
229			});
230			seq!(N in 0..64 {
231				if $shift == N {
232					let carry = $bit_shift_64_opposite(
233						carry,
234						crate::arch::x86_64::m128::max_i32((64 - N) as i32, 0) as _,
235					);
236
237					let val = $bit_shift_64($val, N);
238					return $or(val, carry).into();
239				}
240			});
241
242			return Default::default()
243		}
244	};
245}
246
247impl Shr<usize> for M128 {
248	type Output = Self;
249
250	#[inline(always)]
251	fn shr(self, rhs: usize) -> Self::Output {
252		// This implementation is effective when `rhs` is known at compile-time.
253		// In our code this is always the case.
254		bitshift_128b!(self.0, rhs, _mm_bsrli_si128, _mm_srli_epi64, _mm_slli_epi64, _mm_or_si128)
255	}
256}
257
258impl Shl<usize> for M128 {
259	type Output = Self;
260
261	#[inline(always)]
262	fn shl(self, rhs: usize) -> Self::Output {
263		// This implementation is effective when `rhs` is known at compile-time.
264		// In our code this is always the case.
265		bitshift_128b!(self.0, rhs, _mm_bslli_si128, _mm_slli_epi64, _mm_srli_epi64, _mm_or_si128);
266	}
267}
268
269impl PartialEq for M128 {
270	fn eq(&self, other: &Self) -> bool {
271		unsafe {
272			let neq = _mm_xor_si128(self.0, other.0);
273			_mm_test_all_zeros(neq, neq) == 1
274		}
275	}
276}
277
278impl Eq for M128 {}
279
280impl PartialOrd for M128 {
281	fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
282		Some(self.cmp(other))
283	}
284}
285
286impl Ord for M128 {
287	fn cmp(&self, other: &Self) -> std::cmp::Ordering {
288		u128::from(*self).cmp(&u128::from(*other))
289	}
290}
291
292impl std::hash::Hash for M128 {
293	fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
294		u128::from(*self).hash(state);
295	}
296}
297
298impl std::fmt::LowerHex for M128 {
299	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
300		std::fmt::LowerHex::fmt(&u128::from(*self), f)
301	}
302}
303
304impl Distribution<M128> for StandardUniform {
305	fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> M128 {
306		M128(rng.random())
307	}
308}
309
310impl std::fmt::Display for M128 {
311	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
312		let data: u128 = (*self).into();
313		write!(f, "{data:02X?}")
314	}
315}
316
317impl std::fmt::Debug for M128 {
318	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
319		write!(f, "M128({self})")
320	}
321}
322
323impl Underlier for M128 {
324	const LOG_BITS: usize = 7;
325	const ZERO: Self = { Self::from_u128(0) };
326	const ONE: Self = { Self::from_u128(1) };
327	const ONES: Self = { Self::from_u128(u128::MAX) };
328
329	#[inline(always)]
330	fn interleave(self, other: Self, log_block_len: usize) -> (Self, Self) {
331		unsafe {
332			let (c, d) = interleave_bits(
333				Into::<Self>::into(self).into(),
334				Into::<Self>::into(other).into(),
335				log_block_len,
336			);
337			(Self::from(c), Self::from(d))
338		}
339	}
340}
341
342unsafe impl Zeroable for M128 {}
343
344unsafe impl Pod for M128 {}
345
346unsafe impl Send for M128 {}
347
348unsafe impl Sync for M128 {}
349
350impl<Scalar: BinaryField> From<__m128i> for PackedPrimitiveType<M128, Scalar> {
351	fn from(value: __m128i) -> Self {
352		M128::from(value).into()
353	}
354}
355
356impl<Scalar: BinaryField> From<u128> for PackedPrimitiveType<M128, Scalar> {
357	fn from(value: u128) -> Self {
358		M128::from(value).into()
359	}
360}
361
362impl<Scalar: BinaryField> From<PackedPrimitiveType<M128, Scalar>> for __m128i {
363	fn from(value: PackedPrimitiveType<M128, Scalar>) -> Self {
364		value.to_underlier().into()
365	}
366}
367
368#[inline]
369unsafe fn interleave_bits(a: __m128i, b: __m128i, log_block_len: usize) -> (__m128i, __m128i) {
370	match log_block_len {
371		0 => unsafe {
372			let mask = _mm_set1_epi8(0x55i8);
373			interleave_bits_imm::<1>(a, b, mask)
374		},
375		1 => unsafe {
376			let mask = _mm_set1_epi8(0x33i8);
377			interleave_bits_imm::<2>(a, b, mask)
378		},
379		2 => unsafe {
380			let mask = _mm_set1_epi8(0x0fi8);
381			interleave_bits_imm::<4>(a, b, mask)
382		},
383		3 => unsafe {
384			let shuffle = _mm_set_epi8(15, 13, 11, 9, 7, 5, 3, 1, 14, 12, 10, 8, 6, 4, 2, 0);
385			let a = _mm_shuffle_epi8(a, shuffle);
386			let b = _mm_shuffle_epi8(b, shuffle);
387			let a_prime = _mm_unpacklo_epi8(a, b);
388			let b_prime = _mm_unpackhi_epi8(a, b);
389			(a_prime, b_prime)
390		},
391		4 => unsafe {
392			let shuffle = _mm_set_epi8(15, 14, 11, 10, 7, 6, 3, 2, 13, 12, 9, 8, 5, 4, 1, 0);
393			let a = _mm_shuffle_epi8(a, shuffle);
394			let b = _mm_shuffle_epi8(b, shuffle);
395			let a_prime = _mm_unpacklo_epi16(a, b);
396			let b_prime = _mm_unpackhi_epi16(a, b);
397			(a_prime, b_prime)
398		},
399		5 => unsafe {
400			let shuffle = _mm_set_epi8(15, 14, 13, 12, 7, 6, 5, 4, 11, 10, 9, 8, 3, 2, 1, 0);
401			let a = _mm_shuffle_epi8(a, shuffle);
402			let b = _mm_shuffle_epi8(b, shuffle);
403			let a_prime = _mm_unpacklo_epi32(a, b);
404			let b_prime = _mm_unpackhi_epi32(a, b);
405			(a_prime, b_prime)
406		},
407		6 => unsafe {
408			let a_prime = _mm_unpacklo_epi64(a, b);
409			let b_prime = _mm_unpackhi_epi64(a, b);
410			(a_prime, b_prime)
411		},
412		_ => panic!("unsupported block length"),
413	}
414}
415
416#[inline]
417unsafe fn interleave_bits_imm<const BLOCK_LEN: i32>(
418	a: __m128i,
419	b: __m128i,
420	mask: __m128i,
421) -> (__m128i, __m128i) {
422	unsafe {
423		let t = _mm_and_si128(_mm_xor_si128(_mm_srli_epi64::<BLOCK_LEN>(a), b), mask);
424		let a_prime = _mm_xor_si128(a, _mm_slli_epi64::<BLOCK_LEN>(t));
425		let b_prime = _mm_xor_si128(b, t);
426		(a_prime, b_prime)
427	}
428}
429
430// Reflexive divisibility, needed when M128 is itself a field underlier (a width-1 packed field).
431impl_divisible_self!(M128);
432
433impl_divisible_memcast!(
434	M128,
435	u128 => |val| M128::from(val),
436	u64 => |val| unsafe { M128(_mm_set1_epi64x(val as i64)) },
437	u32 => |val| unsafe { M128(_mm_set1_epi32(val as i32)) },
438	u16 => |val| unsafe { M128(_mm_set1_epi16(val as i16)) },
439	u8 => |val| unsafe { M128(_mm_set1_epi8(val as i8)) },
440);
441
442#[cfg(test)]
443mod tests {
444	use binius_utils::bytes::BytesMut;
445	use proptest::{arbitrary::any, proptest};
446	use rand::prelude::*;
447
448	use super::*;
449
450	fn check_roundtrip<T>(val: M128)
451	where
452		T: From<M128>,
453		M128: From<T>,
454	{
455		assert_eq!(M128::from(T::from(val)), val);
456	}
457
458	#[test]
459	fn test_constants() {
460		assert_eq!(M128::default(), M128::ZERO);
461		assert_eq!(M128::from(0u128), M128::ZERO);
462		assert_eq!(M128::from(1u128), M128::ONE);
463	}
464
465	fn get(value: M128, log_block_len: usize, index: usize) -> M128 {
466		(value >> (index << log_block_len)) & M128::from(1u128 << log_block_len)
467	}
468
469	proptest! {
470		#[test]
471		fn test_conversion(a in any::<u128>()) {
472			check_roundtrip::<u128>(a.into());
473			check_roundtrip::<__m128i>(a.into());
474		}
475
476		#[test]
477		fn test_binary_bit_operations(a in any::<u128>(), b in any::<u128>()) {
478			assert_eq!(M128::from(a & b), M128::from(a) & M128::from(b));
479			assert_eq!(M128::from(a | b), M128::from(a) | M128::from(b));
480			assert_eq!(M128::from(a ^ b), M128::from(a) ^ M128::from(b));
481		}
482
483		#[test]
484		fn test_negate(a in any::<u128>()) {
485			assert_eq!(M128::from(!a), !M128::from(a));
486		}
487
488		#[test]
489		fn test_shifts(a in any::<u128>(), b in 0..128usize) {
490			assert_eq!(M128::from(a << b), M128::from(a) << b);
491			assert_eq!(M128::from(a >> b), M128::from(a) >> b);
492		}
493
494		#[test]
495		fn test_interleave_bits(a in any::<u128>(), b in any::<u128>(), height in 0usize..7) {
496			let a = M128::from(a);
497			let b = M128::from(b);
498
499			let (c, d) = unsafe {interleave_bits(a.0, b.0, height)};
500			let (c, d) = (M128::from(c), M128::from(d));
501
502			for i in (0..128>>height).step_by(2) {
503				assert_eq!(get(c, height, i), get(a, height, i));
504				assert_eq!(get(c, height, i+1), get(b, height, i));
505				assert_eq!(get(d, height, i), get(a, height, i+1));
506				assert_eq!(get(d, height, i+1), get(b, height, i+1));
507			}
508		}
509	}
510
511	#[test]
512	fn test_eq() {
513		let a = M128::from(0u128);
514		let b = M128::from(42u128);
515		let c = M128::from(u128::MAX);
516
517		assert_eq!(a, a);
518		assert_eq!(b, b);
519		assert_eq!(c, c);
520
521		assert_ne!(a, b);
522		assert_ne!(a, c);
523		assert_ne!(b, c);
524	}
525
526	#[test]
527	fn test_serialize_and_deserialize_m128() {
528		let mut rng = StdRng::from_seed([0; 32]);
529
530		let original_value = M128::from(rng.random::<u128>());
531
532		let mut buf = BytesMut::new();
533		original_value.serialize(&mut buf).unwrap();
534
535		let deserialized_value = M128::deserialize(buf.freeze()).unwrap();
536
537		assert_eq!(original_value, deserialized_value);
538	}
539}