Last active
August 29, 2015 14:14
-
-
Save danlentz/301a0f056ffd9c6605a9 to your computer and use it in GitHub Desktop.
Thread-safe Monotonic Clock for Clojure
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(ns clock) | |
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |
;; Lock-Free Thread-safe Monotonic Clock | |
;; Dan Lentz <http://github.com/danlentz> | |
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |
;; We use epoch stamp (milliseconds) and zero-pad the low order bits | |
;; of millisecond stamp and provide atomic incremental | |
;; uuids-this- second subcounter on the low order bits, which | |
;; guarantee that two timestamps never collide regardless of clock | |
;; precision. | |
;; | |
;; 113914335216380000 (+ (* (epoch-time) 10000) 100103040000000000) | |
;; 113914335216380001 first contending timestamp | |
;; 113914335216380002 second contending timestamp | |
;; ... and so forth | |
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |
(def ^:const +subcounter-resolution+ 9999) | |
(deftype State [^short seqid ^long millis]) | |
(let [-state- (atom (->State 0 0))] | |
(defn monotonic-time ^long [] | |
(let [^State new-state | |
(swap! -state- | |
(fn [^State current-state] | |
(loop [time-now (System/currentTimeMillis)] | |
(if-not (= (.millis current-state) time-now) | |
(->State 0 time-now) | |
(let [tt (.seqid current-state)] | |
(if (< tt +subcounter-resolution+) | |
(->State (inc tt) time-now) | |
(recur (System/currentTimeMillis))))))))] | |
(+ (.seqid new-state) | |
(+ (* (.millis new-state) 10000) 100103040000000000))))) |
@sbocq this might make it a little more efficient
@danlentz revision 12 runs also about the same speed (250ms) in my simple benchmark.
And without going through any hoops this idiomatic one takes also about 250ms!
(let [max-stamps 9999
state (atom [0 0])]
(defn monotonic-time ^long []
(let [[now stamps] (swap! state
(fn [[last-time stamps]]
(let [now (System/currentTimeMillis)]
(if (= now last-time)
(if (< stamps max-stamps)
[now (inc stamps)]
(recur state))
[now 0]))))]
(+ (* now 10000) 100103040000000000 stamps))))
interesting discussion, @sbocq! Thanks for joining in!
@danlentz Thanks! I'm looking forward for more ;)
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Ah, you're right! I see in the doc that f might be retried multiple times in 'swap! and indeed the implementation uses a CAS operation.
Out of curiosity, I made a Java implementation:
and did a quick dirty benchmark on a lein REPL with "-server" using:
And here are the timings:
The STM implementation is indeed 4 times slower but the Java one is about 1.5 times faster. I don't know if the Clojure code can be improved much beyond what you did. I haven't reached the chapter on Clojure's performance yet but at least I learned a few other things today :)
@danlentz