Skip to content

Commit 9c38c2b

Browse files
committed
WIP Substantially rework the quire algorithms.
Main changes: - Represent the quire as a little-endian fixed-size array of bytes. - Work in 64-bit chunks: e.g. to add two `q16`s, we only need to do 4 additions-with-carry. WIP commit: algorithms in `mod add` are changed, algorithms in `mod convert` not.
1 parent d5b35b6 commit 9c38c2b

6 files changed

Lines changed: 173 additions & 101 deletions

File tree

‎src/posit/quire/add.rs‎

Lines changed: 113 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,32 @@
11
use super::*;
22

3+
use crate::fallthrough;
4+
use crate::underlying::const_as;
5+
6+
/// Aux function: adapter for `Int::multiword_shl<64>`.
7+
///
8+
/// Compute the result of a multiword left-shift. The return value is `(hi, lo, shift_words)`, such
9+
/// that, in terms of infinite precision arithmetic:
10+
///
11+
/// ```ignore
12+
/// self << n = (hi << Self::BITS + lo) << (shift_words * 64)
13+
/// ```
14+
fn multiword_shl_64<Int: crate::Int>(x: Int, n: u32) -> (u64, u64, usize) {
15+
use crate::underlying::Sealed;
16+
let x = const_as::<Int, i64>(x);
17+
let (hi, lo, offset_bits) = x.multiword_shl::<64>(n);
18+
let offset_bytes = offset_bits as usize / 64;
19+
(hi as u64, lo as u64, offset_bytes)
20+
}
21+
322
impl<
423
const N: u32,
524
const ES: u32,
625
const SIZE: usize,
726
> Quire<N, ES, SIZE> {
827
/// The core algorithm of the quire: adding a fixed-point number, represented as an array of
9-
/// `limbs` in big-endian order, shifted `offset` bytes from the right, and sign-extended to the
10-
/// size of the quire, to `self`.
28+
/// `limbs` in little-endian order, shifted `offset` bytes from the right, and sign-extended to
29+
/// the size of the quire, to `self`.
1130
///
1231
/// In other words, the "logical" number to add to the quire is `limbs`, padded with `offset`
1332
/// bits on the right equal to 0, and infinite bits on the left equal to the msb of bytes.
@@ -22,58 +41,77 @@ impl<
2241
///
2342
/// # Safety
2443
///
25-
/// `limbs.len() * size_of::<Int>() + offset < SIZE` must hold, or calling this function
44+
/// `limbs.len() * size_of::<Int>() + offset ≤ SIZE` must hold, or calling this function
2645
/// is *undefined behaviour*.
27-
pub(crate) unsafe fn accumulate<const L: usize, Int: crate::Int>(
46+
///
47+
/// # Visual example:
48+
///
49+
/// ```text
50+
/// self = [self[0], self[1], self[2], self[3], self[4], self[6], self[6], self[7]]
51+
/// limbs = [limbs[0], limbs[1]]
52+
/// offset = 3
53+
/// ```
54+
///
55+
/// then the result is the result of adding the little-endian bignums
56+
///
57+
/// ```text
58+
/// [self[0], self[1], self[2], self[3] , self[4] , self[5] , self[6] , self[7] ]
59+
/// [0 , 0 , 0 , limbs[0], limbs[1], implicit, implicit, implicit]
60+
/// ```
61+
///
62+
/// where `implicit = if limbs[1] ≥ 0 {0} else {-1}`.
63+
/*#[inline(always)]*/ // TODO Unclear: benefits round_from(posit) by 30% but worsens += posit by 70%
64+
pub(crate) unsafe fn accumulate<const L: usize>(
2865
&mut self,
29-
limbs: [Int; L],
66+
limbs: &[u64; L],
3067
offset: usize,
3168
) {
32-
// The `accumulate` function has three parts:
33-
//
34-
// - We need to add (with carry) the `limbs` to the quire.
35-
// - Then we need to propagate any carry that might exist, up until the end of the quire.
36-
// - Then we need to check if we overflow the quire sum limit (highly unlikely), in which
37-
// case the result should be NaR.
38-
debug_assert!(core::mem::size_of_val(&limbs) + offset < SIZE);
69+
let quire: &mut [u64] = self.as_u64_array_mut();
70+
let len_u64 = quire.len();
3971

40-
// Add the `limbs` with carry.
41-
//
42-
// SAFETY: As a precondition, `offset < SIZE`, so `ptr` is in bounds of the array. But more
43-
// strongly, `offset + L * size_of::<Int>() < SIZE`, so `ptr` will _remain_ in bounds
44-
// throughout this loop. Finally, the `offset` is guaranteed to be aligned to `Int`, and so is
45-
// the quire itself (which is aligned to 128 bits). Therefore: the `src` and `dst` pointers
46-
// are always in bounds and aligned to `Int`.
47-
//
48-
// The following snippet seems to have great codegen: the fixed-size loop of `carrying_add`s is
49-
// actually compiled to a sequence of `adc` instructions on x86!
50-
let base_dst = unsafe { self.0.as_mut_ptr_range().end.byte_sub(offset) } as *mut Int;
51-
let base_src = limbs.as_ptr_range().end as *mut Int;
52-
let mut carry = false;
53-
for i in 0..L {
54-
let dst = unsafe { base_dst.sub(1 + i) };
55-
let src = unsafe { base_src.sub(1 + i) };
56-
let (result, overflow) = unsafe { Int::carrying_add((*dst).from_be(), (*src), carry)};
57-
unsafe { *dst = result.to_be() };
58-
carry = overflow;
72+
if cfg!(debug_assertions) {
73+
debug_assert!(offset + limbs.len() <= quire.len())
74+
} else {
75+
// SAFETY: Precondition.
76+
unsafe { core::hint::assert_unchecked(offset + limbs.len() <= quire.len()) }
5977
}
6078

61-
// We're done with the limbs. Now the only value that needs to be "added" is an eventual carry
62-
// and/or sign bit, i.e. a value of +1, 0, or -1. This needs to be propagated until the end.
63-
let upper_padding = limbs[0] >> (Int::BITS - 1);
64-
let mut dst = unsafe { base_dst.sub(1 + L) };
65-
while dst as *const u8 >= self.0.as_ptr() {
66-
/*if !carry { return }*/
67-
let (result, overflow) = unsafe { (*dst).from_be().carrying_add(upper_padding, carry) };
68-
unsafe { *dst = result.to_be() }
69-
carry = overflow;
70-
dst = unsafe { dst.sub(1) }
79+
// Part 1: Add `limbs[..]` to `quire[offset .. offset + L]`.
80+
let mut carry = false;
81+
for i in 0 .. limbs.len() {
82+
// SAFETY: This follows from the above precondition, but we re-assert it to help the
83+
// compiler.
84+
unsafe { core::hint::assert_unchecked(offset + i < quire.len()) }
85+
let (r, o) = u64::carrying_add(quire[offset + i], limbs[i], carry);
86+
quire[offset + i] = r;
87+
carry = o;
7188
}
7289

73-
/*// If we reach this, then would still be a carry beyond the limits of the quire. In this
74-
// (very unlikely) case, we set the quire to NaR.
75-
if carry == Int::ZERO { return }
76-
*self = Self::NAR*/
90+
// In principle the compiler is able to optimise this, but just in case...
91+
if const { L == Self::SIZE / 8 } { return }
92+
93+
// Part 2: Add `implicit` to `quire[offset + L ..]`.
94+
let implicit = (limbs[L-1] as i64 >> 63) as u64;
95+
96+
fallthrough!(offset + limbs.len(),
97+
0 => if 0 < len_u64 { let (r,o) = u64::carrying_add(quire[ 0], implicit, carry); quire[ 0] = r; carry = o; },
98+
'l1: 1 => if 1 < len_u64 { let (r,o) = u64::carrying_add(quire[ 1], implicit, carry); quire[ 1] = r; carry = o; },
99+
'l2: 2 => if 2 < len_u64 { let (r,o) = u64::carrying_add(quire[ 2], implicit, carry); quire[ 2] = r; carry = o; },
100+
'l3: 3 => if 3 < len_u64 { let (r,o) = u64::carrying_add(quire[ 3], implicit, carry); quire[ 3] = r; carry = o; },
101+
'l4: 4 => if 4 < len_u64 { let (r,o) = u64::carrying_add(quire[ 4], implicit, carry); quire[ 4] = r; carry = o; },
102+
'l5: 5 => if 5 < len_u64 { let (r,o) = u64::carrying_add(quire[ 5], implicit, carry); quire[ 5] = r; carry = o; },
103+
'l6: 6 => if 6 < len_u64 { let (r,o) = u64::carrying_add(quire[ 6], implicit, carry); quire[ 6] = r; carry = o; },
104+
'l7: 7 => if 7 < len_u64 { let (r,o) = u64::carrying_add(quire[ 7], implicit, carry); quire[ 7] = r; carry = o; },
105+
'l8: 8 => if 8 < len_u64 { let (r,o) = u64::carrying_add(quire[ 8], implicit, carry); quire[ 8] = r; carry = o; },
106+
'l9: 9 => if 9 < len_u64 { let (r,o) = u64::carrying_add(quire[ 9], implicit, carry); quire[ 9] = r; carry = o; },
107+
'l10: 10 => if 10 < len_u64 { let (r,o) = u64::carrying_add(quire[10], implicit, carry); quire[10] = r; carry = o; },
108+
'l11: 11 => if 11 < len_u64 { let (r,o) = u64::carrying_add(quire[11], implicit, carry); quire[11] = r; carry = o; },
109+
'l12: 12 => if 12 < len_u64 { let (r,o) = u64::carrying_add(quire[12], implicit, carry); quire[12] = r; carry = o; },
110+
'l13: 13 => if 13 < len_u64 { let (r,o) = u64::carrying_add(quire[13], implicit, carry); quire[13] = r; carry = o; },
111+
'l14: 14 => if 14 < len_u64 { let (r,o) = u64::carrying_add(quire[14], implicit, carry); quire[14] = r; carry = o; },
112+
'l15: 15 => if 15 < len_u64 { let (r,o) = u64::carrying_add(quire[15], implicit, carry); quire[15] = r; carry = o; },
113+
'z: _ => return,
114+
);
77115

78116
// TODO: overflows of the quire sum limit should go to NaR!
79117
}
@@ -85,6 +123,7 @@ impl<
85123
/// `x` must be the result of a [`Posit::decode_regular`] call, or calling this function
86124
/// is *undefined behaviour*.
87125
pub(crate) unsafe fn accumulate_decoded<Int: crate::Int>(&mut self, x: Decoded<N, ES, Int>) {
126+
const { assert!(Int::BITS <= 64, "Quire operations are currently not supported for N > 64") };
88127
debug_assert!(x.exp.abs() <= Posit::<N, ES, Int>::MAX_EXP + Int::ONE);
89128

90129
// The quire is a fixed-point accumulator wide enough to accomodate the product of any two
@@ -108,18 +147,41 @@ impl<
108147
// `if const` (well, not quite because of limitations in Rust, but yes, `base_shift` is a
109148
// constant and will be folded out). We simply check if `base_shift - MAX_EXP` can ever be
110149
// negative (a right-shift). If not, this branch is not even included in the binary.
111-
if base_shift <= Posit::<N, ES, Int>::MAX_EXP && shift < Int::ZERO {
150+
if base_shift <= Posit::<N, ES, Int>::MAX_EXP
151+
&& shift < Int::ZERO {
112152
// Note: no bits of `frac` are actually lost in the right-shift; this is just because `Int`
113153
// itself is wide enough relative to `N` and `ES`.
114154
let shift_right = (-shift).as_u32();
115155
debug_assert_eq!(x.frac.mask_lsb(shift_right), Int::ZERO);
116156
// SAFETY: `limbs` is only one `Int`, and `offset` is `0`.
117-
unsafe { self.accumulate([x.frac >> shift_right], 0) }
118-
} else {
157+
let lo = const_as::<Int, i64>(x.frac) >> shift_right;
158+
unsafe { self.accumulate(&[lo as u64], 0) }
159+
}
160+
161+
// Normal case: we multiword-shift left by `shift_left` bits.
162+
else {
119163
let shift_left = shift.as_u32();
120-
let (hi, lo, offset) = x.frac.multiword_shl(shift_left);
121-
// SAFETY: `x.exp` is between `MIN_EXP` and `MAX_EXP`, so the `offset` is always `< SIZE / 2`.
122-
unsafe { self.accumulate([hi, lo], offset) }
164+
let (hi, lo, offset) = multiword_shl_64(x.frac, shift_left);
165+
166+
// Another edge case: we check in `Self::MIN_SIZE` that we have enough bits. But if we have
167+
// less than 64 to spare, then the `hi` word might be pushed out of the quire. In this case,
168+
// we must be careful to **not** add `&[lo, hi]`, but just `&[lo]`, or we would write out of
169+
// bounds.
170+
//
171+
// Note that again, like the previous edge case above, this only happens on a small portion
172+
// of types (e.g. among the standard types only on p8 and q8), and for types where it cannot
173+
// happen the branch is not even included in the binary.
174+
if Self::WIDTH + 2 + Posit::<N, ES, Int>::MAX_EXP.as_u32() > Self::BITS - 64
175+
&& offset == Self::SIZE / 8 - 1 {
176+
debug_assert!((lo as i64) >= 0 && hi as i64 == 0 || (lo as i64) < 0 && hi as i64 == -1 );
177+
unsafe { self.accumulate(&[lo], offset) }
178+
}
179+
180+
// Otherwise, business as usual
181+
else {
182+
// SAFETY: `x.exp` is between `MIN_EXP` and `MAX_EXP`, so the `offset` is always `< SIZE / 2`.
183+
unsafe { self.accumulate(&[lo, hi], offset) }
184+
}
123185
}
124186
}
125187
}

‎src/posit/quire/basics.rs‎

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -42,21 +42,31 @@ impl<
4242
Self(bytes)
4343
}
4444

45-
/// Access the storage as an array of **big-endian** `Int`s.
45+
/// Access the storage as an array of `u64`s.
4646
///
47-
/// Limitation: even though the return size is known, we cannot return an `&[Int; N]` due to
47+
/// Limitation: even though the return size is known, we cannot return an `&[u64; N]` due to
4848
/// limitations in the Rust type system. We have to hope that the compiler will inline and fold
4949
/// the slice len :)
5050
#[inline]
51-
pub(crate) const fn as_int_array<Int: crate::Int>(&self) -> &[Int] {
51+
pub(crate) const fn as_u64_array(&self) -> &[u64] {
5252
const { assert!(SIZE % 8 == 0, "Quire SIZE must be a multiple of 64 bits (8 bytes)"); }
53-
let ptr = self.0.as_ptr() as *const Int;
54-
let len = SIZE / (Int::BITS as usize / 8);
53+
let ptr = self.0.as_ptr() as *const u64;
54+
let len = SIZE / (64 / 8);
5555
// SAFETY: ptr and len form a valid slice; the size and alignment is correct, and any bit
5656
// pattern is a valid u64 value.
5757
unsafe { core::slice::from_raw_parts(ptr, len) }
5858
}
5959

60+
#[inline]
61+
pub(crate) const fn as_u64_array_mut(&mut self) -> &mut [u64] {
62+
const { assert!(SIZE % 8 == 0, "Quire SIZE must be a multiple of 64 bits (8 bytes)"); }
63+
let ptr = self.0.as_mut_ptr() as *mut u64;
64+
let len = SIZE / (64 / 8);
65+
// SAFETY: ptr and len form a valid slice; the size and alignment is correct, and any bit
66+
// pattern is a valid u64 value.
67+
unsafe { core::slice::from_raw_parts_mut(ptr, len) }
68+
}
69+
6070
/// Auxiliary const: the maximum (positive) exponent of a `Posit<N, ES, Int>`. The size of the
6171
/// quire is directly related to this (see [`Self::MIN_SIZE`] and [`Self::WIDTH`] below).
6272
const MAX_EXP: u32 = {
@@ -135,7 +145,7 @@ impl<
135145
/// A quire that represents the posit value `NaR`.
136146
pub const NAR: Self = {
137147
let mut nar = Self::ZERO;
138-
nar.0[0] = i8::MIN as u8;
148+
nar.0[Self::SIZE - 1] = i8::MIN as u8;
139149
nar
140150
};
141151

@@ -159,11 +169,11 @@ impl<
159169
// Therefore, for almost all cases where the quire is not NaR, we only need a compare and
160170
// branch. Only on when the quire is NaR, or in the rare cases where it's not NaR but still
161171
// starts with `0b1000…`, will we need to scan through the whole thing.
162-
let quire: &[i64] = self.as_int_array();
163-
if quire[0] != i64::MIN.to_be() { return false } // TODO mark likely?
172+
let quire: &[u64] = self.as_u64_array();
173+
if quire[quire.len() - 1] != i64::MIN as u64 { return false } // TODO mark likely?
164174
// Written in this awkward way because it's a `const fn`...
165-
let mut i = 1;
166-
while i < quire.len() {
175+
let mut i = 0;
176+
while i < quire.len() - 1 {
167177
if quire[i] != 0 { return false }
168178
i += 1
169179
}
@@ -249,15 +259,15 @@ mod tests {
249259

250260
#[test]
251261
fn is_nar_manual() {
252-
let bits = [0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
262+
let bits = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x80];
253263
assert!(crate::q8::from_bits(bits).is_nar());
254-
let bits = [0x81, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
264+
let bits = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x81];
255265
assert!(!crate::q8::from_bits(bits).is_nar());
256-
let bits = [0x80, 0, 0x42, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
266+
let bits = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x42, 0, 0x80];
257267
assert!(!crate::q8::from_bits(bits).is_nar());
258-
let bits = [0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x42, 0, 0, 0];
268+
let bits = [0, 0, 0, 0x42, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x80];
259269
assert!(!crate::q8::from_bits(bits).is_nar());
260-
let bits = [0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff];
270+
let bits = [0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f];
261271
assert!(!crate::q8::from_bits(bits).is_nar());
262272
let bits = [0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff];
263273
assert!(!crate::q8::from_bits(bits).is_nar());

‎src/posit/quire/mod.rs‎

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,11 @@ use super::*;
4141
/// assert_eq!(posit, p16::ZERO);
4242
/// ```
4343
//
44-
// The quire is represented as an array of bytes in big-endian order. This is because (we theorise
45-
// at the moment, with no data) since most operations need to start by checking if the most
46-
// significant byte is NaR (= 0b10000000), placing that byte first is the best layout for cache.
47-
//
48-
// On the other hand, adding with carry is more natural to do in little-endian order, and also
49-
// avoids the work of shuffling bits from little-endian to big-endian in little-endian
50-
// architectures... Need to try and profile.
44+
// The quire is represented as an array of bytes in little-endian order.
5145
//
5246
// It is also aligned to 128 bits, and we restrict `SIZE` to be a multiple of 64-bits (8 bytes).
47+
// This allows many operations to be done in fixed-size increments of 64 bits. For small quires
48+
// especially, this is extremely good for performance!
5349
#[derive(Clone, Hash)]
5450
#[repr(align(16))]
5551
pub struct Quire<

‎src/posit/rational.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ impl<
167167
Err(IsNaR)
168168
} else {
169169
// The quire is just big fix-point number with denominator 2 ^ WIDTH.
170-
let mut bytes = value.0.iter();
170+
let mut bytes = value.0.iter().rev();
171171

172172
let first = bytes.next().unwrap(); // First (most significant) byte is signed i8
173173
let mut numerator = Integer::from(*first as i8);

0 commit comments

Comments
 (0)