Update
On the same computer, using the Rakudo compiler "rakudo-moar-2021.06-01-macos-x86_64-clang.tar" I get 3-10 times speed-ups compared to the timings of my calculations in the original post.
.elems: 100000
.head(3): (id.20444 => 81.95246687507492 id.81745 => 34.859323339828464 id.79973 => 97.33816856420829)
time of .sort({-$_.value}) : 0.764283216
time of .sort(-*.value) : 0.618963783
time of .sort.reverse : 0.584477656
time of .values.sort : 1.68912663
Note, that those timings are close to the R timings. So, on those kinds of computations Raku has the performance I expect.
Original post
I recently watched a FOSDEM presentation by Elizabeth Mattijsen titled
"Raku - Sets without Borders"
and decided to adopt Raku Mix
objects in some of my computational workflows.
I noticed that sorting (the pairs of) a Mix
object is very slow -- I would say 100 to 1000 times slower than what I expect. See the Raku code and output below. (I also provided related R code and output on the same computer.)
Is that slowness expected? Is there a work around for faster computations?
(To be more specific, I am interested in fast reverse-sort and fast retrieval of the top-K largest elements in a Mix
.)
(The timings are on a few years old Mac Book Pro, Mac OS 10.15.7, using latest Rakudo Compiler "rakudo-moar-2021.02.1-01-macos-x86_64-clang.tar.gz" .)
Raku
#!/usr/bin/env perl6
my @words = Array(1 .. 100_000).map({ 'id.' ~ $_.Str });
my $m0 = Mix(@words.map({ $_ => 100.rand() }));
say '.elems: ', $m0.elems;
say '.head(3): ', $m0.head(3);
my $start = now;
my $m1 = $m0.sort({-$_.value});
say 'time of .sort({-$_.value}): ', now - $start;
$start = now;
my $m2 = $m0.sort(-*.value);
say 'time of .sort(-*.value) : ', now - $start;
$start = now;
my $m3 = $m0.sort.reverse;
say 'time of .sort.reverse : ', now - $start;
$start = now;
my $m4 = $m0.values.sort;
say 'time of .values.sort : ', now - $start;
# .elems: 100000
# .head(3): (id.96239 => 87.89629474533156 id.89110 => 11.661698290245525 id.24795 => # 64.80528155838671)
# time of .sort({-$_.value}): 3.64936396
# time of .sort(-*.value) : 4.0388654
# time of .sort.reverse : 4.5783556
# time of .values.sort : 4.3461059
R
Here is a similar data and sorting code in R:
words <- paste0( 'id.', 1:100000)
m <- setNames( runif(n = length(words), min = 0, max = 100), words)
cat( "length(m) : ", length(m), "\n")
cat( "m[1:3]:\n"); print(m[1:3]); cat("\n")
cat( "system.time( sort(m) ) : ", system.time( sort(m) ), "\n")
cat( "system.time( m[order(-m)] ) : ", system.time( m[order(-m)] ), "\n")
cat( "system.time( rev(sort(names(m))) ) : ", system.time( rev(sort(names(m))) ), "\n")
# length(m) : 100000
# m[1:3]:
# id.1 id.2 id.3
# 89.99714 54.31701 11.57415
#
# system.time( sort(m) ) : 0.011 0 0.011 0 0
# system.time( m[order(-m)] ) : 0.011 0 0.011 0 0
# system.time( rev(sort(names(m))) ) : 0.298 0.001 0.3 0 0
Here are answers to questions by @raith:
"Is the m in the R code mutable?"
No, R objects are mostly immutable."Does the sort(m) build a new data structure, or just a new index into the existing m structure?"
A new data structure is created. R is a descendent of LISP, so it mostly follows, in spirit, the functional programming paradigm."Same question for m[order(-m)]?"
order(-m)
gives an integer vector (of indexes.) That vector of indexes is used to retrieve elements ofm
into a new object."And rev(sort(names(m)))?"
names(m)
takes the "keys" ofm
's elements. Those keys are sorted and placed into a character vector, and then that character vector is reversed. (I.e. a new object is created.)"Presumably building just an index could be dramatically faster. Perhaps Raku could have a sort variant for Tuples that produces a Seq that relies on that approach?"
I assume it is not my place to comment on this, but I would like to mention that:- Julia -- which is also a LISP descendant -- does something like that for its data structures. And Julia's creators and developers claim to be, in general, (much) faster than R and Mathematica. (Which are other LISP descendants for mathematical computations.)
- I prefer and expect Raku code in the functional paradigm style to be fast.
Updated R benchmark
A few people requested more detailed R benchmarking:
library(microbenchmark)
set.seed(32)
words <- paste0( 'id.', 1:100000)
m <- setNames( runif(n = length(words), min = 0, max = 100), words)
cat( "length(m): ", length(m), "\n")
cat( "m[1:3]:\n"); print(m[1:3]); cat("\n")
microbenchmark::microbenchmark(
sort(m, decreasing = T),
sort(-m),
m[order(-m)],
rev(sort(m)),
rev(sort(names(m))),
unit = "s", times = 100
)
# length(m) : 100000
#
# m[1:3]:
# id.1 id.2 id.3
# 50.58405 59.48084 80.87471
#
# Unit: seconds
# expr min lq mean median uq max neval cld
# sort(m, decreasing = T) 0.006246853 0.007789205 0.009215613 0.008263348 0.009002414 0.02450786 100 a
# sort(-m) 0.006857755 0.008376058 0.010292145 0.008939605 0.010069702 0.02469324 100 a
# m[order(-m)] 0.006658089 0.008257555 0.009726704 0.008718414 0.009811200 0.02294023 100 a
# rev(sort(m)) 0.008975013 0.010939122 0.014965756 0.011692480 0.012571979 0.22022085 100 b
# rev(sort(names(m))) 0.256036106 0.268526455 0.278385866 0.277794917 0.288586351 0.31160492 100 c
#