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

Faster primitive root #127

Merged
merged 9 commits into from
Aug 19, 2018
Merged

Conversation

b-mehta
Copy link
Contributor

@b-mehta b-mehta commented Aug 13, 2018

(As mentioned in #119)

Unfinished so far, but coprimality tests for Int and Word fail seemingly due to overflow: Is it okay to just remove these tests? I think the logic in the new isPrimitiveRoot' is correct, but sanity checks would be appreciated also; the idea is that to check if g is a primitive root mod p^2 it suffices to check that g is a primitive root mod p and that g^(p-1) mod p^2 is not 1. In addition, these same conditions are exactly the conditions for g to be a primitive root mod p^k for any k. I've kept the method for testing primitive roots mod p as wikipedia describes. Similarly to check primitive roots mod 2p^k it suffices to check g is a primitive root mod p^k and that g mod 2p^k is odd, so just that g is odd.

I'd also like to add a test counting the number of primitive roots and possibly laziness tests. Are there any other appropriate tests?

@b-mehta b-mehta changed the title Faster primitive root (WIP) Faster primitive root Aug 13, 2018
@Bodigrim
Copy link
Owner

AFAIU neither tests nor arbitrary instances were changed, so they run through the same range of values as before. And if they succeeded before and fail now, then something went wrong.

A wild guess is to add

{-# SPECIALIZE isPrimitiveRoot' :: CyclicGroup Int -> Int -> Bool #-}
{-# SPECIALIZE isPrimitiveRoot' :: CyclicGroup Word -> Word -> Bool #-}

@b-mehta
Copy link
Contributor Author

b-mehta commented Aug 13, 2018 via email

@b-mehta
Copy link
Contributor Author

b-mehta commented Aug 13, 2018

Confirming my suspicion, the test which fails (isPrimitiveRoot'Property1) tests gcd n (prefValue (cyclicGroupToModulo cg)) == 1 and the function isPrimitiveRoot' asserts gcd n (prefValue m) == 1 where m = cyclicGroupToModulo cg (line 121). The overflow occurs in calculating prefValue m, since this is a prime to a (probably) large power, but the new implementation does not require the large power to be calculated.

@Bodigrim
Copy link
Owner

Yes, you are right: previously both implementation and test were incorrect. Thanks for noticing this. I propose to amend the test, avoiding overflow in arguments of gcd. I often use Int/Word instead of Integer/Natural for the sake of performance, so it is worth to check that primitive root is correct.

import qualified Math.NumberTheory.GCD as GCD

isPrimitiveRoot'Property1
  :: (Eq a, Integral a, UniqueFactorisation a)
  => AnySign a -> CyclicGroup a -> Bool
isPrimitiveRoot'Property1 (AnySign n) cg
  = gcd (toInteger n) (prefValue (castPrefactored (cyclicGroupToModulo cg))) == 1
  || not (isPrimitiveRoot' cg n)

castPrefactored :: Integral a => Prefactored a -> Prefactored Integer
castPrefactored = fromFactors . GCD.splitIntoCoprimes . map (first toInteger) . GCD.toList . prefFactors

castPrefactored is very ugly, of course.


I'd also like to add a test counting the number of primitive roots and possibly laziness tests.

Sounds good. It is also worth to add a benchmark, showing the achieved performance improvement.

@b-mehta b-mehta changed the title (WIP) Faster primitive root Faster primitive root Aug 17, 2018
@b-mehta
Copy link
Contributor Author

b-mehta commented Aug 17, 2018

Should be all completed now.

Benchmarking: I've added benchmarks for cyclicGroupFromModulo and isPrimitiveRoot', as these are the two time-intensive functions in this module. I haven't benchmarked isPrimitiveRoot since it is essentially just a combination of the above two functions.

The new version of isPrimitiveRoot' speeds up primitive root checking for primes to a large power by a huge amount: checking a primitive root modulo 3^20000 went from around 12s to around 1.4μs and similarly modulo 2*3^20000 went from around 11s to around 1.5μs. For large primes, there was essentially no change, and there was a small change for large prime squares: from around 1.3ms to around 850μs.

forceCyclic (Just CG2) = -2
forceCyclic (Just CG4) = -1
forceCyclic (Just (CGOddPrimePower !_p !_k)) = 1
forceCyclic (Just (CGDoubleOddPrimePower !_p !_k)) = 2
Copy link
Owner

Choose a reason for hiding this comment

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

Let us rather derive NFData instance for CyclicGroup, similar to benchmarks of Gaussian integers: one and two.

Copy link
Contributor Author

@b-mehta b-mehta Aug 17, 2018

Choose a reason for hiding this comment

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

Fixed.
EDIT: maybe not. The problem is that this requires NFData PrimeNat (or something analogous). This can't be done as a standalone/orphan instance since PrimeNat is in Math.NumberTheory.Primes.Types, which is a hidden module. I'm hesitant to include the instance in the hidden module since this involves including deepseq in the library dependencies.

, testSmallAndQuick "Int" (isPrimitiveRoot'Property1 :: AnySign Int -> CyclicGroup Int -> Bool)
, testSmallAndQuick "Word" (isPrimitiveRoot'Property1 :: AnySign Word -> CyclicGroup Word -> Bool)
, testSmallAndQuick "Int" (isPrimitiveRoot'Property1 :: AnySign Int -> CyclicGroup Int -> Bool) -- dubious test
, testSmallAndQuick "Word" (isPrimitiveRoot'Property1 :: AnySign Word -> CyclicGroup Word -> Bool) -- dubious test
Copy link
Owner

Choose a reason for hiding this comment

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

They are not dubious anymore, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed.

|| case 0 `modulo` m of
SomeMod (_ :: Mod t) -> genericLength (filter isPrimitiveRoot [(minBound :: Mod t) .. maxBound]) == totient (totient m)
InfMod{} -> False

Copy link
Owner

Choose a reason for hiding this comment

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

If you need to promote m to type level, there is a shorter approach:

case someNatVal m of
  SomeNat (_ :: Proxy t) -> ...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed.

@@ -90,6 +95,13 @@ isPrimitiveRootProperty4 (AnySign n) (Positive m)
SomeMod n' -> not (isPrimitiveRoot n')
InfMod{} -> False

isPrimitiveRootProperty5 :: Positive Natural -> Bool
isPrimitiveRootProperty5 (Positive m)
Copy link
Owner

Choose a reason for hiding this comment

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

Let us use Test.QuickCheck.Small newtype here. Positive does not mean much for Natural and I am worried that if the Arbitrary instance of Natural gets changed, this test may take a really long time.

Copy link
Contributor Author

@b-mehta b-mehta Aug 17, 2018

Choose a reason for hiding this comment

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

I'm not entirely sure what your proposed fix here is, could you elaborate? Positive Natural seems to be used fairly often across the test suites here, including in isPrimitiveRootProperty2.

Copy link
Owner

Choose a reason for hiding this comment

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

My proposal was to write

isPrimitiveRootProperty5 :: Small Natural -> Bool
isPrimitiveRootProperty5 (Smalll m) = ...

But now I realised that there is no Small newtype in smallcheck and my approach actually would not work. Nevermind.

@Bodigrim
Copy link
Owner

checking a primitive root modulo 3^20000 went from around 12s to around 1.4μs and similarly modulo 2*3^20000 went from around 11s to around 1.5μs.

Wow! This is impressive.

@Bodigrim
Copy link
Owner

Overall looks good to me, but Travis build fails with

benchmark/Math/NumberTheory/PrimitiveRootsBench.hs:15:1: error:
    Could not load module ‘Math.NumberTheory.Primes.Types’
    it is a hidden module in the package ‘arithmoi-0.7.0.0’
    Use -v to see a list of the files searched for.
   |
15 | import Math.NumberTheory.Primes.Types
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
cabal: Failed to build bench:criterion from arithmoi-0.7.0.0.

I do not want this module to be publicly exposed.
Is it possible to write instance NFData (Prime Integer)? If no, then let us move NDFata instances from benchmarks to the library, under relevant types.

@b-mehta
Copy link
Contributor Author

b-mehta commented Aug 19, 2018

Everything should be working now!

@Bodigrim Bodigrim merged commit 810070a into Bodigrim:master Aug 19, 2018
@Bodigrim
Copy link
Owner

Thanks for your contribution! Merged.

@b-mehta b-mehta deleted the faster-primitive-root branch August 19, 2018 19:38
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.

2 participants