Concurrency & performance
Memoize & lazy eval
`memoize function` result-caching and `lazy` deferred-variable evaluation, each proven with real evidence rather than just asserted: a memoized recursive Fibonacci is timed with `tic()`/ `toc()` to show the cached second call is essentially instant, and a
catalog/qu_memoize_lazy.qu
`lazy` variable's initializer is proven to not run until the variable is actually read.
What it exercises
- memoize function name(params) ... end function
- tic() / toc() -- real wall-clock timing
- lazy name = expr -- deferred variable evaluation
How it works
repeat sub-calls with identical arguments to a single cached lookup.
calls = 0
memoize function slow_fib(n)
# The counter lives outside this function, so the function has to
# say it is writing to it -- an assignment binds locally otherwise.
global calls
calls = calls + 1
if n < 2 then
return n
else
return slow_fib(n - 1) + slow_fib(n - 2)
end if
end function
tic()
first_result = slow_fib(28)
first_elapsed = toc()
calls_after_first = calls
tic()
second_result = slow_fib(28)
second_elapsed = toc()
calls_after_second = calls
print("slow_fib(28) first call = {first_result}, elapsed = {first_elapsed:.6f}s, body calls so far = {calls_after_first}")
print("slow_fib(28) second call = {second_result}, elapsed = {second_elapsed:.6f}s, body calls so far = {calls_after_second}")
print("second call re-ran the body: {calls_after_second > calls_after_first} (expected false -- served from cache)")
print("second call was faster: {second_elapsed < first_elapsed}")
# A fresh, previously-uncached argument still runs (and grows the cache).
third_result = slow_fib(29)
calls_after_third = calls
print("slow_fib(29) (new argument) = {third_result}, body calls grew: {calls_after_third > calls_after_second}")
and the result is then cached for every later read.
init_runs = 0
function expensive_init()
# The counter lives outside this function, so the function has to
# say it is writing to it -- an assignment binds locally otherwise.
global init_runs
init_runs = init_runs + 1
return sum(ones(1, 1000000)) # a real, non-trivial computation
end function
lazy total = expensive_init()
runs_before_any_read = init_runs
first_read = total
runs_after_first_read = init_runs
second_read = total
runs_after_second_read = init_runs
print("")
print("lazy init_runs before any read of `total`: {runs_before_any_read} (expected 0)")
print("lazy init_runs after first read: {runs_after_first_read} (expected 1)")
print("lazy init_runs after second read: {runs_after_second_read} (expected 1 -- cached, not re-run)")
print("first_read = {first_read}, second_read = {second_read}")
Results
printed
slow_fib(28) first call = 317811, elapsed = 0.000340s, body calls so far = 29 slow_fib(28) second call = 317811, elapsed = 0.000002s, body calls so far = 29 second call re-ran the body: false (expected false -- served from cache) second call was faster: true slow_fib(29) (new argument) = 514229, body calls grew: true lazy init_runs before any read of `total`: 0 (expected 0) lazy init_runs after first read: 1 (expected 1) lazy init_runs after second read: 1 (expected 1 -- cached, not re-run) first_read = 1000000, second_read = 1000000
The whole program
Runnable as it stands: qu run catalog/qu_memoize_lazy.qu
# qu_memoize_lazy.qu -- `memoize function` result-caching and `lazy`
# deferred-variable evaluation, each proven with real evidence rather than
# just asserted: a memoized recursive Fibonacci is timed with `tic()`/
# `toc()` to show the cached second call is essentially instant, and a
# `lazy` variable's initializer is proven to not run until the variable is
# actually read.
#
# Constructs exercised (all real, verified-working builtins/syntax):
# memoize function name(params) ... end function
# tic() / toc() -- real wall-clock timing
# lazy name = expr -- deferred variable evaluation
backend auto
#%% Part 1: memoize function -- a deliberately slow recursive Fibonacci.
#%% Naive recursive fib(n) is O(2^n) without caching; memoization collapses
#%% repeat sub-calls with identical arguments to a single cached lookup.
calls = 0
memoize function slow_fib(n)
# The counter lives outside this function, so the function has to
# say it is writing to it -- an assignment binds locally otherwise.
global calls
calls = calls + 1
if n < 2 then
return n
else
return slow_fib(n - 1) + slow_fib(n - 2)
end if
end function
tic()
first_result = slow_fib(28)
first_elapsed = toc()
calls_after_first = calls
tic()
second_result = slow_fib(28)
second_elapsed = toc()
calls_after_second = calls
print("slow_fib(28) first call = {first_result}, elapsed = {first_elapsed:.6f}s, body calls so far = {calls_after_first}")
print("slow_fib(28) second call = {second_result}, elapsed = {second_elapsed:.6f}s, body calls so far = {calls_after_second}")
print("second call re-ran the body: {calls_after_second > calls_after_first} (expected false -- served from cache)")
print("second call was faster: {second_elapsed < first_elapsed}")
# A fresh, previously-uncached argument still runs (and grows the cache).
third_result = slow_fib(29)
calls_after_third = calls
print("slow_fib(29) (new argument) = {third_result}, body calls grew: {calls_after_third > calls_after_second}")
#%% Part 2: lazy -- deferred variable evaluation. The initializer expression
#%% does not run at the `lazy` statement; only the FIRST read forces it,
#%% and the result is then cached for every later read.
init_runs = 0
function expensive_init()
# The counter lives outside this function, so the function has to
# say it is writing to it -- an assignment binds locally otherwise.
global init_runs
init_runs = init_runs + 1
return sum(ones(1, 1000000)) # a real, non-trivial computation
end function
lazy total = expensive_init()
runs_before_any_read = init_runs
first_read = total
runs_after_first_read = init_runs
second_read = total
runs_after_second_read = init_runs
print("")
print("lazy init_runs before any read of `total`: {runs_before_any_read} (expected 0)")
print("lazy init_runs after first read: {runs_after_first_read} (expected 1)")
print("lazy init_runs after second read: {runs_after_second_read} (expected 1 -- cached, not re-run)")
print("first_read = {first_read}, second_read = {second_read}")