Zulip Chat Archive
Stream: general
Topic: Why is this memoized fibonacci so slow?
James Sully (May 10 2025 at 04:37):
import Std
partial def fibState (n : Nat) : Nat :=
let rec loop : Nat → StateT (Std.HashMap Nat Nat) Id Nat
| 0 => pure 0
| 1 => pure 1
| n => do
let memo ← StateT.get
match memo[n]? with
| some result => pure result
| none => do
let result : Nat := (← loop (n-1)) + (← loop (n-2))
StateT.set <| memo.insert n result
pure result
(loop n).run' ∅
#eval fibState 22
Once you start getting into the high teens low 20s it starts to take a noticeable amount of time to eval.
I would have assumed FBIP optimisations would work for something like this - am I not using the state linearly here? I can't remember or find it, but I seem to remember there was some kind of incantation for checking this.
Tbc, I'm not surprised it's slower than non-memoized fib for low n, but I am surprised it's this slow.
James Sully (May 10 2025 at 04:41):
It's true that there are two recursive calls, but because we're in a monad and there's a defined evaluation order, shouldn't the first recursive call run, and produce a new state value, which is then passed to the second recursive call? that still seems linear to me
James Sully (May 10 2025 at 04:48):
I just realised I got the order of the calls wrong, switching around
let result : Nat := (← loop (n-2)) + (← loop (n-1))
makes a big improvement. But it's still getting quite slow around n=40.
Robin Arnez (May 10 2025 at 04:57):
Yes, you get the cache, then run loop (n - 1) and loop (n - 2) which adds stuff to the cache and then discard all additions and add n, result to the original cache.
James Sully (May 10 2025 at 04:58):
doh
James Sully (May 10 2025 at 04:58):
thank you
Robin Arnez (May 10 2025 at 04:58):
To make sure you use the state linearly, always use modify for modifying operations:
import Std
partial def fibState (n : Nat) : Nat :=
let rec loop : Nat → StateT (Std.HashMap Nat Nat) Id Nat
| 0 => pure 0
| 1 => pure 1
| n => do
let memo ← StateT.get
match memo[n]? with
| some result => pure result
| none => do
let result : Nat := (← loop (n-2)) + (← loop (n-1))
modify fun memo => memo.insert n result
pure result
(loop n).run' ∅
Robin Arnez (May 10 2025 at 04:59):
set and get usually cause linearity problems because then, after the get, you have two reference to the value, the one from get, and the one which is saved in the state, so linearity gone
James Sully (May 10 2025 at 05:00):
thanks! I could only find https://leanprover-community.github.io/mathlib4_docs/Init/Control/State.html#StateT.modifyGet
James Sully (May 10 2025 at 05:01):
ahh, it's a monadstate method
https://leanprover-community.github.io/mathlib4_docs/Init/Prelude.html#modify
I forgot about that
James Sully (May 10 2025 at 05:02):
this was very helpful, thank you
James Sully (May 10 2025 at 05:05):
docs#dbgTraceIfShared was what I was looking for
James Sully (May 14 2025 at 14:46):
can this idea be made to work somehow?
instance : Nonempty (Thunk Nat) :=
⟨Thunk.mk (λ _ => 0)⟩
partial def fibThunks (n : Nat) : Nat :=
if h : n < 2 then
n
else
let rec arr : Vector (Thunk Nat) n := Vector.ofFn
λ | ⟨0,_⟩ => ↑(0 : Nat)
| ⟨1,_⟩ => ↑(1 : Nat)
| ⟨i+2,_⟩ => Thunk.mk λ () => arr[i].get + arr[i+1].get
(dbgTraceIfShared "oops" arr)[n-2].get + arr[n-1].get
#eval fibThunks 25
Aaron Liu (May 14 2025 at 14:57):
This works
instance : Nonempty (Thunk Nat) :=
⟨Thunk.mk (λ _ => 0)⟩
partial def fibThunks (n : Nat) : Nat :=
if h : n < 2 then
n
else
let rec arr : Vector (Thunk Nat) n := Vector.ofFn
λ | ⟨0,_⟩ => Thunk.pure 0
| ⟨1,_⟩ => Thunk.pure 1
| ⟨i+2,_⟩ => Thunk.mk λ () => arr[i].get + arr[i+1].get
let arr := dbgTraceIfShared "oops" arr
arr[n-2].get + arr[n-1].get
#eval fibThunks 25
Ayhon (May 14 2025 at 15:02):
Does it seem to be caching the values for you? When I execute fibThunks 31 it takes a while.
James Sully (May 14 2025 at 15:02):
I think it's not caching the values. I assumed the perf problem was due to sharing but I must have been mistaken
Last updated: Dec 20 2025 at 21:32 UTC