Benchmarking mir.ndslice + lubeck against numpy and Julia

Dennis dkorpel at gmail.com
Sun Jan 12 12:46:44 UTC 2020


On Sunday, 12 January 2020 at 09:43:46 UTC, p.shkadzko wrote:
> Yes, technically we are benchmarking against C numpy and 
> C/Fortran scipy through Python syntax layer. It should have 
> been "C/Fortran".

No, I think the names in your comparison are fair.

You are not comparing the languages themselve, but available 
solutions for scientific computing for each language.

Let me focus on your 5000 * 1000 SVD example. The thing is, all 
these languages / packages are just using LAPACK for that (see 
[1] [2] and [3]), so what you are actually comparing is:

- The default settings
- The LAPACK implementation that is used
- A bit of luck (your matrices are randomly created)
- The wrapping code (this is where the programming language 
actually matters)

The first important default setting is the algorithm. Lapack 
offers 'gesdd', a 'more efficient divide-and-conquer approach', 
and 'gesvd', a 'general rectangular approach' [4]. Python and 
Julia default to gesdd, while D's Lubeck defaults to gesvd.

I factored out the matrix generation code from the benchmark, and 
switched the D algorithm to 'gesdd', and the time went from ~17 
seconds to ~6 on my PC. Comparing that with Python, I got these 
times for some subsequent runs:

Python: 5.7, 6.3, 6.4, 5.3, 6.3
D (LDC release): 5.9, 5.8, 5.8, 5.9, 7.0, 5.7
D (DMD debug): 6.0, 6.5, 6.9, 6.7, 6.0

It can be seen that:

- The times vary between runs (possibly because randomized 
matrices, and other applications running on my PC)
- The times between scipy and Lubeck are comparable
- Compiler settings are not that important, the extra compilation 
time is worse than the gained speed (and when doing scientific 
computing you recompile a lot).

The second important setting is whether to compute full matrix U 
and Vt.

When computing the SVD of matrix A of size 5000x1000, the U*S*Vt 
have sizes 5000x5000, 5000x1000, and 1000x1000 respectively. 
However, since S is just a diagonal matrix with 1000 entries and 
the rest is all 0, you can choose to only compute U as a 
5000x1000 matrix. This obviously saves a lot of work.

Julia does this by default, while Python and D compute the full U 
by default.

When not computing the full matrices in Python and D, I get:

D: roughly 1.7 s
Python: roughly 1.2 s

I don't have Julia, but I bet that it will be in the same order 
of magnitude on my PC as well. The new code can be found below.

So in conclusion, Lubeck has some unfortunate default settings 
for benchmarking, and even with comparable settings D can be a 
little slower. This is either because inferior wrapping code, or 
because I am using a different LAPACK implementation. For D I use 
the LAPACK implementation of OpenBLAS that I compiled myself a 
while ago, I don't know what Python ships with.

In any case, while your benchmark is not representative of the 
actual languages, it is indeed unfortunate when people try 
something D, find it significantly slower than other languages, 
and someone on the forum has to point out all the steps to get D 
performance on par.

---

[1] 
https://github.com/scipy/scipy/blob/adc4f4f7bab120ccfab9383aba272954a0a12fb0/scipy/linalg/decomp_svd.py#L16-L139
[2] 
https://github.com/JuliaLang/julia/blob/aa35ee2d30065803410718378e5673c7f845da62/stdlib/LinearAlgebra/src/svd.jl#L93
[3] 
https://github.com/kaleidicassociates/lubeck/blob/72091ecdd545f9524a1d80e5880cda19845143d0/source/lubeck.d#L356

[4] 
https://docs.scipy.org/doc/scipy/reference/generated/scipy.linalg.svd.html

New Python code:
```
import numpy as np
from scipy.linalg import svd
import timeit
h = 5000
w = 1000
data = np.random.randint(0, 1024, w*h).reshape((h, w))
def svd_fun():
     u, s, v = svd(data, overwrite_a=False, full_matrices=False, 
lapack_driver='gesdd')

print(timeit.timeit(svd_fun, number=1))
```

New D code:
```
import std, mir.ndslice, lubeck;
import std.datetime.stopwatch: benchmark;

enum h = 5000;
enum w = 1000;

auto getMtx() {
     Xorshift rnd;
     rnd.seed(unpredictableSeed);
     auto matrix = generate(() => uniform(0, 1024, rnd))
         .take(w * h).array.sliced(h, w);
     return matrix;
}

auto svdFun(T)(T mtx) {
     auto result = mtx.svd!(No.allowDestroy, "gesdd")(Yes.slim); 
// gesvd gesdd
}

void main() {
     auto svdTime = benchmark!(() => getMtx.svdFun())(1);
     writeln(svdTime);
}
```


More information about the Digitalmars-d mailing list