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

[Startup performance] Stop getting the local_timezone when it's not required #650

Open
irevoire opened this issue Nov 3, 2024 · 3 comments

Comments

@irevoire
Copy link
Contributor

irevoire commented Nov 3, 2024

Part of #525

The culprit

Hey, I profiled numbat once again while running 1 + 1 and noticed that 23% (~10ms out of 40) of the time was spent running the ffi::get_local_timezone function of numbat.
image

The call comes from this line:

@description("Timezone conversion function targeting the users local timezone (`datetime -> local`).")
let local: Fn[(DateTime) -> DateTime] = tz(get_local_timezone())

And after commenting it and benchmarking the startup time with hyperfine here are the results I got:

% hyperfine './numbat-master -e "1 + 1"' './numbat-without-tz -e "1 + 1"' --warmup 10
Benchmark 1: ./numbat-master -e "1 + 1"
  Time (mean ± σ):      41.4 ms ±   1.0 ms    [User: 30.5 ms, System: 10.2 ms]
  Range (min … max):    39.9 ms …  45.0 ms    69 runs
 
  Warning: Statistical outliers were detected. Consider re-running this benchmark on a quiet system without any interferences from other programs. It might help to use the '--warmup' or '--prepare' options.
 
Benchmark 2: ./numbat-without-tz -e "1 + 1"
  Time (mean ± σ):      32.4 ms ±   1.6 ms    [User: 30.3 ms, System: 1.4 ms]
  Range (min … max):    30.6 ms …  36.1 ms    86 runs
 
Summary
  ./numbat-without-tz -e "1 + 1" ran
    1.28 ± 0.07 times faster than ./numbat-master -e "1 + 1"

So it’s pretty much what I measured with my profiler, 1.28 times or ~10ms faster.

The solution

I’m not sure of what should be the solution here though, maybe make the whole local variable a ffi function?
Not a fan though, maybe you’ll have a better idea

@sharkdp
Copy link
Owner

sharkdp commented Nov 3, 2024

Thank you! Not sure how we missed this in profiles so far. I guess it became slower with #511 because now we're loading the tz database from disk.

Conceptually, this is similar to the problem we had with currency conversion factors. We want to initialize variables/units with a value that is expensive to look up. Lazy loading would be an obvious solution here, but might be a bit tricky to implement? I haven't thought deeply about it but maybe, instead of calling get_local_timezone() at startup time, we could set

let local: Fn[(DateTime) -> DateTime] = _local()

where _local() would be a new FFI function that would immediately return lazy wrapper around the actual function reference (we have a special FunctionReference::TzConversion variant anyway…).

It would be fantastic if we could find a solution that would solve this uniformly, across currencies and timezone objects (and potentially others). Maybe some kind of language feature that allows variables and units to be defined lazily. Like a @lazy or @async decorator on that definition.

If all of that turns out to be completely over-engineered, I'd also be fine with turning local into a function 😄

@irevoire
Copy link
Contributor Author

irevoire commented Nov 3, 2024

If all of that turns out to be completely over-engineered, I'd also be fine with turning local into a function 😄

To me, this looks like the easiest for this specific case, but I’m not sure if it’s a breaking change. 🤔
And also, it means it'll be re-computed for every call instead of once for the whole lifetime of the program, I guess?
Ideally, you’re right. A Lazy would be the best to accommodate both use cases.
But shouldn’t it be the case for every variable by default instead of being a keyword?

@Goju-Ryu
Copy link
Contributor

Goju-Ryu commented Nov 3, 2024

I have actually been thinking about how having lazy loaded values could be useful for function local variables. I have often needed to use an if-then-else expression to ensure that a calculation is valid because it is valid only in some branches. A lazy value would simplify this significantly in addition to the benefits for this and similar issues. I don't know if it is the best solution for this particular issue, but as a feature in general I think it would be beneficial in any case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants