Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Int - two's complement #695

Open
wants to merge 70 commits into
base: signed-int
Choose a base branch
from

Conversation

erik-3milabs
Copy link

Topic
Adding Int support (see #624)

To decide on sign-and-magnitude (#694) vs. two's complement, I whipped up a quick two's complement implementation of the basic add/sub/mul/div operations. It's not perfect yet; I'm just curious to see how it compares to #694.

@erik-3milabs
Copy link
Author

erik-3milabs commented Oct 16, 2024

FYI: local benchmark results @ b23483d

wrapping ops/split_mul, 
I128xI128:      26.042 ns
I256xI256:      26.104 ns
I512xI512:      68.643 ns
I1024xI1024:   239.23  ns
I2048xI2048:   799.26  ns
I4096xI4096: 2.5102    µs

wrapping ops/div, full size
I256/I128:     351.69 ns
I512/I256:     554.63 ns
I1024/I512:  1.0663   µs
I2048/I1024: 2.5858   µs
I4096/I2048: 7.2846   µs

wrapping ops/add, 
I128+I128:     1.1915 ns
I256+I256:     2.4108 ns
I512+I512:    10.374  ns
I1024+I1024:  20.704  ns
I2048+I2048:  65.318  ns
I4096+I4096: 103.10   ns

wrapping ops/sub,
I128-I128:     1.1967 ns
I256-I256:     2.3008 ns
I512-I512:    10.985  ns
I1024-I1024:  32.041  ns
I2048-I2048:  70.872  ns
I4096-I4096: 128.86   ns

src/int.rs Outdated
///
/// Warning: this operation is unsafe; when `self == Self::MIN`, the negation fails.
#[inline]
fn negate_unsafe(&self) -> (Self, Choice) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it does what is the same as self.0.wrapping_neg() does.

Copy link
Author

@erik-3milabs erik-3milabs Oct 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pointing this out! I adapted wrapping_neg() into wrapping_negc() to include the carry and still get the is_zero for free. In addition to having no duplicate code, this saves anywhere from 8% (I128) to 1% (I4096) in the multiplication benchmark 👌

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

btw. why do you need is_zero in the first place?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, I needed it for mul and div; the fix you pointed out there makes knowing whether something is zero unnecessary (for now).

@tarcieri
Copy link
Member

signed-int has been branched from master

@erik-3milabs erik-3milabs changed the base branch from master to signed-int October 18, 2024 14:28
@lleoha
Copy link

lleoha commented Oct 18, 2024

signed-int has been branched from master

Thanks @tarcieri

Copy link

@lleoha lleoha left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's make it into the feature branch so we have a baseline to work on.
LGTM to initial commit.

@tarcieri
Copy link
Member

Will do another pass tomorrow, but I agree this is already a lot

// TODO(tarcieri): replace with `const impl From<i64>` when stable
#[cfg(target_pointer_width = "32")]
pub const fn from_i64(n: i64) -> Self {
Int::new_from_uint(Uint::<{ I64::LIMBS }>::from_u64(n as u64)).resize()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should work and is a little simpler:
Int::new_from_uint(Uint::from_u64(n as u 64))

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that would work. Let me explain why:

In scaling up a negative integer, we have to make sure that all new limbs are initialized with Limb::MAX instead of Limb::ZERO (as is the default for Uint). So, what I'm doing here is:

  • cast i64 to u64,
  • ingest this u64 into a Uint that is as small as possible,
  • wrap the Uint into an Int, and
  • scale up the int (which I wrote to make sure it uses the right Limb type).

IIUC, Int::new_from_uint(Uint::from_u64(n as u 64)) does

  • cast i64 to u64,
  • ingest this u64 into a Uint<LIMBS>, filling any missing limbs with Limb::ZERO, and
  • wrapping the Uint into an Int.

This would not work for negative values.

Would you agree?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ahh, right,
I forgot that you can call from_i64 to Int with more than two limbs :)

@lleoha
Copy link

lleoha commented Oct 19, 2024

@tarcieri @erik-3milabs
What do you think about introducing Positive, NonPositive, Negative, NonNegative newtype structs similarly to Zero, NonZero, Odd etc.?
Also I was thinking about introducing Signed and Unsigned traits (not sure if these would be a'la marker traits) or should contains any methods.

/// I128::from(2)
/// )
/// ```
pub fn checked_div_floor(&self, rhs: &Self) -> CtOption<Self> {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need this method?
I think one method that rounds towards zero it sufficient (so it works like regular primitive int division).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need it? No, probably not. Do I need it? Yes, yes I do :)

I need this operation as well as div_mod_floor for a project I'm working on (that op is one of the reasons I'm implementing this right now 🙈). IIUC, the definition I am using here aligns with num_bigint::BigInt::div_mod_floor, which I'm trying to replace to make my algorithm constant time.

This function, of course, differs slightly from Int::checked_div in that it rounds down, instead of to zero.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@erik-3milabs out of curiosity, if this is not a secret, what do you need the floor_div for?

@tarcieri
Copy link
Member

@lleoha the wrappers idea sounds possibly good (although I'm unclear on use cases), however something probably best tackled after getting this PR over the finish line

src/int/div.rs Outdated
/// Base checked_div_mod operation.
/// Given `(a, b)` computes the quotient and remainder of the absolute
/// Note: this operation rounds towards zero, truncating any fractional part of the exact result.
fn checked_div_mod(
Copy link

@lleoha lleoha Oct 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we instead have a method that is consistent with primitive div/rem i.e. return quotient and remainder with quotient round towards zero and both q and r being signed integer as they both can be negative e.g. -5 / 2 = (-2, -1).
Also to be consistent with Uint can we rename it to div_rem instead of div_mod?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I was a little chaotic,
What I'm trying to say is that returning two Uints and one bit is not enough to cover all the cases:

-5/-2 = (2, -1)
-5/2 = (-2, -1)
5/-2 = (-2, 1)
5/2 = (2, 1)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh, didn't think of that. Let me fix this later today!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated the code. Let me know what you think!

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@erik-3milabs
Copy link
Author

Failing CI should be fixed by erik-3milabs#1 which is slated to be merged.

@erik-3milabs
Copy link
Author

Ok, this PR has gotten out of hand... Oops...

With all checks passing, I'm proposing to freeze it here; I'll create a new PR for new features I intend to introduce.

@erik-3milabs
Copy link
Author

@tarcieri, could you please re-review this and let me know what still needs to be done to merge this?

@erik-3milabs erik-3milabs mentioned this pull request Oct 22, 2024
@erik-3milabs erik-3milabs changed the title [WIP] Int - two's complement Int - two's complement Oct 22, 2024
@erik-3milabs erik-3milabs mentioned this pull request Oct 22, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants