Welcome! Please see the About page for a little more info on how this works.

+9 votes
in Java Interop by
Clojure's random functions currently use {{Math.random}} and related features, which makes them impossible to seed. This seems like an appropriate use of a dynamic var (compared to extra arguments), since library code that wants to behave randomly could transparently support seeding without any extra effort.

I propose {{(def ^:dynamic \*rand* (java.util.Random.))}} in {{clojure.core}}, and that {{rand}}, {{rand-int}}, {{rand-nth}}, and {{shuffle}} be updated to use {{\*rand*}}.

I think semantically this will not be a breaking change.

h2. Criterium Benchmarks

I did some benchmarking to try to get an idea of the performance implications of using a dynamic var, as well as to measure the changes to concurrent access.

The code used is at [https://github.com/gfredericks/clj-1452-tests]; the raw output is in a comment.

{{rand}} is slightly slower, while {{shuffle}} is insignificantly faster. Using {{shuffle}} from 8 threads is insignificantly slower, but switching to a {{ThreadLocalRandom}} manually in the patched version results in a 2.5x speedup.

Running on my 8 core Linode VM:

||Benchmark||Clojure||Runtime mean||Runtime std dev||
|{{rand}}|1.6.0|61.3ns|7.06ns|
|{{rand}}|1.6.0 + {{\*rand\*}}|63.7ns|1.80ns|
|{{shuffle}}|1.6.0|12.9µs|251ns|
|{{shuffle}}|1.6.0 + {{\*rand\*}}|12.8µs|241ns|
|{{threaded-shuffling}}|1.6.0|151ms|2.31ms|
|{{threaded-shuffling}}|1.6.0 + {{\*rand\*}}|152ms|8.77ms|
|{{threaded-local-shuffling}}|1.6.0|N/A|N/A|
|{{threaded-local-shuffling}}|1.6.0 + {{\*rand\*}}|64.5ms|1.41ms|


*Approach:* create a dynamic var \*rand* and update {{rand}}, {{rand-int}}, {{rand-nth}}, and {{shuffle}} to use {{\*rand*}}

*Patch:* CLJ-1452.patch

*Screened by:*

11 Answers

0 votes
by

Comment made by: gfredericks

Attached CLJ-1452.patch, with the same code used in the benchmarks.

0 votes
by

Comment made by: gfredericks

Should we be trying to make Clojure's random functions thread-local by default while we're mucking with this stuff? We could have a custom subclass of {{Random}} that has {{ThreadLocal}} logic in it (avoiding {{ThreadLocalRandom}} because Java 6), and make that the default value of * }.

0 votes
by

Comment made by: alexmiller

I think the ThreadLocal question is interesting, not sure re answer.

It would be nice if the description summarized the results of the tests in a table and the criterium output was in the comments instead.

0 votes
by
_Comment made by: gfredericks_

Full output from the test repo (which is summarized in the table in the description):


$ echo "Clojure 1.6.0"; lein with-profile +clj-1.6 run; echo "Clojure 1.6.0 with *rand*"; lein with-profile +clj-1452 run
Clojure 1.6.0

;;;;;;;;;;;;;;;;;;
;; Testing rand ;;
;;;;;;;;;;;;;;;;;;
WARNING: Final GC required 1.261632096547911 % of runtime
Evaluation count : 644646900 in 60 samples of 10744115 calls.
             Execution time mean : 61.297605 ns
    Execution time std-deviation : 7.057249 ns
   Execution time lower quantile : 56.872437 ns ( 2.5%)
   Execution time upper quantile : 84.483045 ns (97.5%)
                   Overhead used : 16.319772 ns

Found 6 outliers in 60 samples (10.0000 %)
    low-severe   1 (1.6667 %)
    low-mild     5 (8.3333 %)
 Variance from outliers : 75.5119 % Variance is severely inflated by outliers

;;;;;;;;;;;;;;;;;;;;;
;; Testing shuffle ;;
;;;;;;;;;;;;;;;;;;;;;
Evaluation count : 4780800 in 60 samples of 79680 calls.
             Execution time mean : 12.873832 µs
    Execution time std-deviation : 251.388257 ns
   Execution time lower quantile : 12.526871 µs ( 2.5%)
   Execution time upper quantile : 13.417559 µs (97.5%)
                   Overhead used : 16.319772 ns

Found 3 outliers in 60 samples (5.0000 %)
    low-severe   3 (5.0000 %)
 Variance from outliers : 7.8591 % Variance is slightly inflated by outliers

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Testing threaded-shuffling ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
Evaluation count : 420 in 60 samples of 7 calls.
             Execution time mean : 150.863290 ms
    Execution time std-deviation : 2.313755 ms
   Execution time lower quantile : 146.621548 ms ( 2.5%)
   Execution time upper quantile : 155.218897 ms (97.5%)
                   Overhead used : 16.319772 ns
Clojure 1.6.0 with *rand*

;;;;;;;;;;;;;;;;;;
;; Testing rand ;;
;;;;;;;;;;;;;;;;;;
Evaluation count : 781707720 in 60 samples of 13028462 calls.
             Execution time mean : 63.679152 ns
    Execution time std-deviation : 1.798245 ns
   Execution time lower quantile : 61.414851 ns ( 2.5%)
   Execution time upper quantile : 67.412204 ns (97.5%)
                   Overhead used : 13.008428 ns

Found 3 outliers in 60 samples (5.0000 %)
    low-severe   3 (5.0000 %)
 Variance from outliers : 15.7596 % Variance is moderately inflated by outliers

;;;;;;;;;;;;;;;;;;;;;
;; Testing shuffle ;;
;;;;;;;;;;;;;;;;;;;;;
Evaluation count : 4757940 in 60 samples of 79299 calls.
             Execution time mean : 12.780391 µs
    Execution time std-deviation : 240.542151 ns
   Execution time lower quantile : 12.450218 µs ( 2.5%)
   Execution time upper quantile : 13.286910 µs (97.5%)
                   Overhead used : 13.008428 ns

Found 1 outliers in 60 samples (1.6667 %)
    low-severe   1 (1.6667 %)
 Variance from outliers : 7.8228 % Variance is slightly inflated by outliers

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Testing threaded-shuffling ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
Evaluation count : 420 in 60 samples of 7 calls.
             Execution time mean : 152.471310 ms
    Execution time std-deviation : 8.769236 ms
   Execution time lower quantile : 147.954789 ms ( 2.5%)
   Execution time upper quantile : 161.277200 ms (97.5%)
                   Overhead used : 13.008428 ns

Found 3 outliers in 60 samples (5.0000 %)
    low-severe   3 (5.0000 %)
 Variance from outliers : 43.4058 % Variance is moderately inflated by outliers

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Testing threaded-local-shuffling ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
Evaluation count : 960 in 60 samples of 16 calls.
             Execution time mean : 64.462853 ms
    Execution time std-deviation : 1.407808 ms
   Execution time lower quantile : 62.353265 ms ( 2.5%)
   Execution time upper quantile : 67.197368 ms (97.5%)
                   Overhead used : 13.008428 ns

Found 1 outliers in 60 samples (1.6667 %)
    low-severe   1 (1.6667 %)
 Variance from outliers : 9.4540 % Variance is slightly inflated by outliers
0 votes
by

Comment made by: gfredericks

I think using a {{ThreadLocal}} is logically independent from adding }, so it could be a separate ticket. I just suggested it here since it would for some uses mitigate any slowdown from } but now that I'm looking at the benchmark results again the slowdown might be insignificant.

0 votes
by

Comment made by: gfredericks

Also worth noting that (as I did in the benchmark code) with just the patch's changes (i.e., no ThreadLocal involved) users still gain the ability to do ThreadLocal manually, which is not currently possible.

0 votes
by

Comment made by: stu

workaround: (link: https://github.com/clojure/data.generators text: data.generators) provides seedable random

0 votes
by

Comment made by: gshayban

Just noting, ThreadLocalRandom is >= JDK 7.

0 votes
by

Comment made by: gfredericks

The >=JDK7 aspect of {{ThreadLocalRandom}} is no longer a problem, correct?

0 votes
by

Comment made by: alexmiller

Right

0 votes
by
Reference: https://clojure.atlassian.net/browse/CLJ-1452 (reported by gfredericks)
...