Search code examples
idris

Slow type-checking and poor run-time performance with large natural numbers in Idris 1.2.0


I have been playing around with making my own random generator in Idris for learning purpose. In my solution I am aiming for totality for all functions and thus I am using numbers of type Nat and the built-in function modNatNZ which requires proof that the second arg isn't zero.

While making a program to test my function on some large natural numbers as input, I encountered problems with both type checking and execution of the program being incredible slow.

module Main

%default total

getUnixEpoch : IO Int
getUnixEpoch = foreign FFI_C "#(unsigned long)time(NULL)" (IO Int)

isSuccNotZero : IsSucc n -> Not (n = Z)
isSuccNotZero {n = S _} ItIsSucc Refl impossible

congruentialMethod : (seed : Nat) -> (a : Nat) -> (b : Nat) ->
                     (m : Nat) -> {auto prf : IsSucc m} -> Stream Double
congruentialMethod seed a b m {prf} =
  (cast seed / cast m) :: congruentialMethod (safeMod_m (a * seed + b)) a b m
  where
    safeMod_m : Nat -> Nat
    safeMod_m x = modNatNZ x m (isSuccNotZero prf)

randomNumberGenerator : (seed : Nat) -> Stream Double
randomNumberGenerator seed =
  let a : Nat = 16807
      b : Nat = 0
      m : Nat = 2147483647
  in
  case m of
       Z => ?this_will_never_happen_but_it_makes_type_checking_faster
       (S m') => congruentialMethod seed a b (S m')

main : IO ()
main = do seed <- getUnixEpoch
          putStrLn $ show $ take 5 $ randomNumberGenerator (cast seed)

1. It takes forever to type check with large natural numbers.

The type checker seems to take an eternity to verify that a hard-coded value of 2147483647 really is greater than zero. My poor fix to this, is to convince Idris with a case-expression.

        ...
        m = 2147483647
    in
    case m of
         Z => ?this_will_never_happen_but_it_makes_type_checking_faster
         (S m') => congruentialMethod seed a b (S m')

Obviously my work-around with the case-expression isn't very satisfying. So is there a better way of convincing the type checker that m is greater than zero in order to get faster type checking?

If this is something that requires a workaround, then I am wondering if this is something a future implementation of the type checker in theory will be able to handle in reasonable time without workarounds, -or if it is something I should always expect to work around to get fast type checking?

2. Program execution with large natural numbers takes forever.

I've attempted to both execute the program in the repl and execute a compiled version of the program, but had to terminate both manually because they seem to take forever to run.

I've experimented a little with making a program that used integers (Int type) in which I was able to get fast run-time performance, but I don't see any way to make all the functions total when making the same program with integers.

Is there a way to define my program with all functions being total and still get fast performance?

Again, if there currently isn't a good way to get faster run-time performance for this kind of program, then I am wondering if this is something that eventually/theoretically could be improved in future versions of Idris, -or if this is something I'll always have to work around or fall back to partial functions to get fast run-time performance?

I am using Idris version 1.2.0


Solution

  • Type checking perfomance

    Consider

    N : Nat
    N = 10000
    
    nIsS : IsSucc N
    nIsS = ItIsSucc
    

    with

    ItIsSucc : IsSucc (S n)
    

    Right now, as far as I understand it, everything has to be evaluated when type checking, i.e. 10000 must be constructed, before finding n. Neither of them are optimized to integers (in the compile time stage), but a nesting of S (S (S (S (S …)))). You can %freeze N to stop it from being evaluated (which would work in your case):

    N : Nat
    N = 9999
    %freeze N
    
    nIsS : IsSucc (S N)
    nIsS = ItIsSucc
    

    Theoretically that shouldn't be necessary, as you can easily see by the first S (…) that it fulfills IsSucc (S n) (just set n = …). This lazy approach to unifying is called weak head normal form. WHNF is implemented to some extend, but apparently isn't used always when type checking. So this could be improved in the future, and if I'm not mistaken, Blodwen (WIP!) supports this. At least there is a bigsuc-example. :-)

    Run time perfomance

    Idris compiles Nat to GMP bignums and uses corresponding C functions for them (like addition, substraction, multiplication, …). Modulo isn't among them, but uses the actual Idris definition. Of course that's not as fast as Integer, which uses a native C function.

    There isn't really anything clean you can do about it. If you really want totality, best thing would probably asserting it by hand if you're really sure that this function is total:

    modIntNZ : (i, m : Integer) -> {auto prf : m == 0 = False} -> Integer
    modIntNZ i m = assert_total (i `mod` m)