Performance improvments on beginning and end. #190
Closed
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Background
We've been using tick for a very interval intensive application for some years now and it's been working well. Due to some API changes, we haven't upgraded tick since 0.4.32, but now did some refactorings and upgraded to 0.7.2. However, our acceptance tests started to run extremely slow, to a point that we would not be able to use a new tick in production, which would be a pity.
So I started to dig in, and found that the bottleneck was the new implementation of beginning and end that checks if v satisfies p/TimeSpan before calling.
I changed this to this:
Large test benchmarks
With this change, performance went back to almost what it was before. Here are some numbers:
Microbenchmarks
Here are the microbenchmarks for the different versions:
;;
;; With tick 0.4.32
;;
(use 'criterium.core)
(require '[tick.alpha.api :as t])
(def t1 (t/new-interval (t/date "2021-02-01") (t/date "2021-02-05")))
(with-progress-reporting (bench (t/beginning t1) :verbose))
;;
;; Evaluation count : 1146487740 in 60 samples of 19108129 calls.
;; Execution time sample mean : 45.372086 ns
;; Execution time mean : 45.373254 ns
;; Execution time sample std-deviation : 0.220612 ns
;; Execution time std-deviation : 0.234171 ns
;; Execution time lower quantile : 45.063637 ns ( 2.5%)
;; Execution time upper quantile : 45.853972 ns (97.5%)
;; Overhead used : 7.744911 ns
;;
;; Found 3 outliers in 60 samples (5.0000 %)
;; low-severe 1 (1.6667 %)
;; low-mild 2 (3.3333 %)
;; Variance from outliers : 1.6389 % Variance is slightly inflated by outliers
;;
;;
;; With tick 0.7.2 (500x slower)
;;
(use 'criterium.core)
(require '[tick.core :as t])
(require '[tick.alpha.interval :as ti])
(def t1 (ti/new-interval (t/date "2021-02-01") (t/date "2021-02-05")))
(with-progress-reporting (bench (t/beginning t1) :verbose))
;;
;; Evaluation count : 2620980 in 60 samples of 43683 calls.
;; Execution time sample mean : 22.829573 µs
;; Execution time mean : 22.836290 µs
;; Execution time sample std-deviation : 395.071874 ns
;; Execution time std-deviation : 395.633990 ns
;; Execution time lower quantile : 22.682359 µs ( 2.5%)
;; Execution time upper quantile : 22.996234 µs (97.5%)
;; Overhead used : 7.746340 ns
;;
;; Found 4 outliers in 60 samples (6.6667 %)
;; low-severe 2 (3.3333 %)
;; low-mild 2 (3.3333 %)
;; Variance from outliers : 6.2811 % Variance is slightly inflated by outliers
;;
;;
;; With patched tick 0.7.2 (similar to 0.4.32)
;;
(use 'criterium.core)
(require '[tick.core :as t])
(require '[tick.alpha.interval :as ti])
(def t1 (ti/new-interval (t/date "2021-02-01") (t/date "2021-02-05")))
(with-progress-reporting (bench (t/beginning t1) :verbose))
;;
;; Evaluation count : 1153781880 in 60 samples of 19229698 calls.
;; Execution time sample mean : 45.005852 ns
;; Execution time mean : 45.019090 ns
;; Execution time sample std-deviation : 0.943030 ns
;; Execution time std-deviation : 0.948350 ns
;; Execution time lower quantile : 44.036791 ns ( 2.5%)
;; Execution time upper quantile : 46.372050 ns (97.5%)
;; Overhead used : 7.749936 ns
The API for beginning/end in general
On a higher level, I don't see the reason for having
beginning
andend
returningv
ifv
doesn't match the protocol. If I would call(t/beginning 0.5)
with 0.4.32, it would throw an exception (which feels very reasonable), but in 0.7.2 it would instead return0.5
.It's an obvious programming error, and it would make more sense to me to get an exception if it's not an
ITimeSpan
. I don't fully see the use case for silently returning what was sent in when it's not anITimeSpan
?In the spirit of this, I did try to change beginning/end to just call
p/beginning
andp/end
and not returningv
if it fails/won't work, but it broke quite a lot of tests, so thetry/catch
is at least compatible with 0.7.2 behaviour, but I do feel that an exception for a nonITimeSpan
argument would be more appropriate.; with 0.4.32
(t/beginning 0.5)
Execution error (IllegalArgumentException) at tick.protocols/eval20902$fn$G (protocols.cljc:63).
No implementation of method: :time of protocol: #'tick.protocols/IExtraction found for class: java.lang.Double
; with 0.7.2
(t/beginning 0.5)
result: 0.5