LFortran: Modern interactive LLVM-based Fortran compiler
lfortran.org
lfortran.org
(L.transpose() * P).diagonal().array().square().mean();
which computes the squared average of column by column dot products between matrices L and P. I assume Fortran, since matrices are native types, does this kind of thing even better.Aside: Fortran 77 was the first programming language I learned in 1985 and I hated programming for awhile after that. We called it the "F" language. Now that I am a bit older and do a lot of linear algebra coding... I wonder if I would like it now.
If you want to try external BLAS/LAPACK with Eigen, I'd look at: https://eigen.tuxfamily.org/dox/TopicUsingBlasLapack.html
I have `A * B`, `A * B'`, `A' * B` and `A' * B'` small-single-threaded-matmul benchmarks here: https://chriselrod.github.io/LoopVectorization.jl/latest/exa... I compared triple nested loops with Clang, icc, ifort, gfortran, Julia, and LoopVectorization.jl with matmul routines from ifort, gfortran, OpenBLAS, MKL, and Eigen.
While gfortran's builtin hit over 40 GFLOPS with `A * B` and `A' * B'`, it failed to get half that if only one argument was transposed. I'm supposed awkward fusing at the start of this post because if it had done one after the other, it should have still hit >40 GFLOPS when only 1 matrix was transposed.
Certainly, gfortran’s default implementation works well as a quick and easy solution for small vectors and matrices outside of hot paths. Also, I remember discussions about improving matmul(transpose(A),B) somewhat recently. I don’t remember for which version it was, but it’s the sort of things that is improved regularly.
I would love an option to align arrays in gfortran. It’s very important to take advantage of automatic vectorisation but I haven’t found a reliable way to do it.
I'm on gfortran 10.1.1, so I'd only be missing out on very recent improvements (i.e., to trunk).
Note that these benchmarks were dynamically sized. If you write a fortran program like
real(kind=8), dimension(10,10) :: A, B, C
! fill A and B somehow
C = matmul(A, B)
the compiler will take advantage of the known dimensions and inline the call, making it much faster.> I would love an option to align arrays in gfortran. It’s very important to take advantage of automatic vectorisation but I haven’t found a reliable way to do it.
I wish more compilers could also use masks to vectorize without padding. If multiplying 7x7 matrices with AVX512, the obvious solution is to just mask the loads/stores of columns, without the need for padding. Of course, padding would also ensure all your loads/stores are aligned, which can be nice.
LoopVectorization can't do many of these yet, so its performance will fall off a cliff shortly after the largest size on the plots (and at much smaller sizes for CPUs with a smaller L2 cache). I had to add code to perform packing/tiling in my actual matmul code on top of what it did. So that MLIR can generate that sort of code already looks promising.
Still, the work of telling it what to do isn't easy.
I'm not involved in any of those projects, so everything I say here is pure speculation. But I imagine pragmas and the like would be important whenever the compiler doesn't know the sizes at compile time. Otherwise, you probably don't want it to generate massive amounts of code through multiple extra blocking loops, massive unrolling in a main kernel, and multiple clean up kernels for every random loop nest.
Just writing three loops and letting the compiler optimize it was much faster for `A * B'`, so it must be a pretty naive implementation getting called.
However, when I last researched this for my previous work, OpenBLAS was the fastest open source implementation and is written in a mix of C and assembly, not Fortran. The repository contains quite a lot of Fortran, but it is mostly for the LAPACK implementation, which is not part of BLAS.
In our testing we got a nice speedup of our signal processing pipeline when enabling EIGEN_USE_BLAS, but this was on ARM64, so your experience may differ.
[0] https://eigen.tuxfamily.org/dox/TopicUsingBlasLapack.html
The last time optimisation came up, I tried the author's problem - multiplying two 4096x4096 matrices - in numpy, FORTRAN and Rust, on a standard laptop. Supposedly it took 9 hours in naive Python (I didn't try). Results were:
Python3 (numpy 1.18.4) 1.3s FORTRAN (gfortran 9.3.0) 6.0s Rust (1.43.1, debug) >60s Rust (1.43.1, release) 4.0s
What surprised me is that python and fortran solutions were written in minutes. The Rust solution took hours and multiple forum posts for help. There's no obvious way, and every single way I tried had "gotchas" and utterly astonishing behaviour in it, particularly with simple, statically-allocated arrays.
Fortran could do with proper namespaces, and properly dropping some of its older conventions, though. Reading it feels like archaeology.
> Python3 (numpy 1.18.4) 1.3s FORTRAN (gfortran 9.3.0) 6.0s Rust (1.43.1, debug) >60s Rust (1.43.1, release) 4.0s
I suspect that you could get the Fortran and numpy results to converge by turning on the compiler option to use the local BLAS library for MATMUL, which will normally use an OpenMP-threaded solution.
Upfront note: these days "Fortran" is preferred to "FORTRAN."
> Fortran could do with proper namespaces, and properly dropping some of its older conventions, though. Reading it feels like archaeology.
The newer Fortran standards support pretty decent namespaces (USE module x, ONLY : y_ and a limited OO with classes and class methods. The problem, at least when I was doing Fortran work everyday, was that the compilers did not fully support the newer Fortran standards. We found the Intel compiler to be the best of the bunch, but we also found a non-trivial number of compiler bugs and standard options that weren't supported. The Intel Math Kernel Library offers pretty good linear algebra performance, too.
Re: older conventions, we had a lot of modern Fortran code that was easy to read and avoided all of the old Fortran 77 nonsense that really confusing to read if you weren't from that era (e.g., the infamous arithmetic GOTO).
However, it was nice being able to still incorporate "just works" code from the 80s that did complicated calculational things that nobody really wanted to touch too much. You know, the kind of subroutine that starts with the comment, "This code _seems to_ ..." I suspect we're not the only organization that wanted to keep that code around but be able to add modern Fortran around and on top of it.
> every single way I tried had "gotchas" and utterly astonishing behaviour in it
As someone who doesn't know much about Rust (or FORTRAN for that matter), what kinds of gotchas? I'd expect numeric code like this to be quite straightforward, as it presumably doesn't lean heavily on memory-management cleverness in the language, or anything like that.
[0] https://en.wikipedia.org/wiki/Strassen_algorithm
edit For clarity, I made a table of your data. Seems surprising that Rust outperformed FORTRAN.
┌──────────┬─────────────────┬──────┐
│ Language │ Detail │ Time │
├──────────┼─────────────────┼──────┤
│ Python3 │ numpy 1.18.4 │ 1.3s │
│ FORTRAN │ gfortran 9.3.0 │ 6.0s │
│ Rust │ 1.43.1, debug │ >60s │
│ Rust │ 1.43.1, release │ 4.0s │
└──────────┴─────────────────┴──────┘Re Rust gotchas: the standard array container breaks with lengths > 32 - alright, it doesn't "break", but half of the traits, including really basic stuff - more basic than "add" - aren't implemented. So if you write something at small scale, then scale it up slightly - gotcha! And you can't easily reimplement them, either. Basically, the easiest way I've found to do it that way is to fork rustc.
That leaves using dynamic allocation, which I wanted to avoid, or external libraries (nalgebra), which aren't necessarily mature yet. And since type parameters are sometimes namespace separated, and sometimes not, and type specification is sometimes necessary, and sometimes not - examples work, but any tiny modification of them does not, because the example relied on type inference, and what looks like a type, and is declared as a type, is actually a generic.
It also, incidentally, compiled to ninety-three megabytes. To multiply two 4096x4096 f64's.
My point was that in comparing numpy to yodelshady's FORTRAN code, we're probably just comparing two different FORTRAN implementations, one much better than the other (perhaps using a different algorithm, perhaps just better optimised).
(I am, obviously, taking inspiration from "There's plenty of room at the top": https://news.ycombinator.com/item?id=23442123. Naive FORTRAN's slow even compared to C there, but it's not the same machine anymore so not apples to apples. Also, as a small point - I initialised all matrices with random numbers. Below half a second and that might start to be a significant time demand.)
I commented on this thread, because it definitely seems to be in the realm where the compiler matters. So there is opportunity.
┌────────────────┬─────────────────┬──────┐
│ Language │ Detail │ Time │
├────────────────┼─────────────────┼──────┤
│ Python3 and C │ numpy 1.18.4 │ 1.3s │
│ FORTRAN │ gfortran 9.3.0 │ 6.0s │
│ Rust │ 1.43.1, debug │ >60s │
│ Rust │ 1.43.1, release │ 4.0s │
└────────────────┴─────────────────┴──────┘I'd assumed numpy used FORTRAN, but you're right, its heavy-lifting is done in C.
But Backwards compatibly is important here as there is a lot of legacy code still in use - yes I know arithmetic if has finally been removed.
LFortran's main advantage is that it is interactive, so the parser and semantics has to be a little different. And also we designed it so that it will be quite approachable for people to write tools on top.
For the really big Fortran systems I worked on in the 1980's (Map Reduce) the edit compile / link process could take several minutes for a single module - we did have a build system written in JCL to help automate this as well.
Here’s a demo of the Jupyter notebook-like interface: https://mybinder.org/v2/gl/lfortran%2Fweb%2Flfortran-binder/...
A fortran without strings would still be a fortran, for many people. In fact, it could arguably be "a better fortran". Without complex numbers or arrays, not at all.
I've written some F# code and for some trivial cases I was benchmarking there was some interesting performance differences between a native multi-dimensional array and an array-of-arrays version.
It wasn't important enough to look into so I don't know if the difference is due to quirks of the CLRs implementation or due to memory access patterns.
(and probably many others)
[1] https://stackoverflow.com/questions/597720/what-are-the-diff...
int a[2][3];
in C. Is that jagged? I wouldn’t have thought so, since AFAIK you could only store 6 elements in that and no fewer. Interestingly you cannot take the value of &&a[0][0], assign it to an int **b;
pointer b, and then access it via b[0][0] because it’s stored as a contiguous block of integers rather than an array of pointers that point to arrays as you might expect.https://stackoverflow.com/questions/1083658/do-jagged-arrays...
> the array will still have the shape [2][3]. If you want a true jagged array, you will have to create it dynamically.
And my first computer lecture many decades ago is really about how he hated fortran and why FORTRAN is still not dead (Mainly negative view due to haveGOTO that time). And it is still not dead.
First cl-jupyter (https://github.com/fredokun/cl-jupyter) which is now in maintance mode and now common-lisp-jupyter (https://github.com/yitzchak/common-lisp-jupyter).
Here is a notebook I did awhile back with cl-jupyter: https://github.com/mmaul/clml.tutorials/blob/master/CLML-Win...
Fortran has been continuously updated, supports OOP, modules, and even generics.