Re: LW compiler optimizations

"Yuri Davidovsky (as work at disclosure dot ie)" <[email protected]>
Newsgroups gmane.lisp.lispworks.general
Message-ID <[email protected]>
> On 14 Jan 2025, at 18:47, Tim Bradshaw (as tfb at tfeb dot org) <[email protected]> wrote:
> 
> I added some more variants since yesterday, and with the best of them LW is not slower than SBCL and neither is slower than C (or than my C): both are about 0.94ns/step on my M1 machine

Ok, I did a good bit of testing of the code provided this morning. I concentrated on the native LW performance, but also tested the FLI versions, as well as testing the compiled executable of the C code used for the dynamic lib. I did not test the code in SBCL, but I am not seeing any reasons why it would be producing worse results that LW on my machine.

On M1 I am seeing pretty much exact the same results as Bradshaw demonstrated, both the native lisp versions, as well as the FLI versions. The testing was done allocating two 1M element arrays of double floats and doing 1000 iterations like so:

(defvar u (make-array 1000000 ;1M
                      :element-type 'double-float
                      :allocation :pinnable
                      :initial-element 1.0d0))
(defvar v (make-array 1000000 ;1M
                      :element-type 'double-float
                      :allocation :pinnable
                      :initial-element 2.0d0))

(time (dot-offset u v 0 0 1000000 1000)) ;1B operations



Essentially we are doing 1B pair operations. This number is chosen because the output of the time function will give you the nanosecond values per pair, like so:

Elapsed time =        1.110

Since elapsed time here is 1.1 seconds approx, it means that a single operation will take 1 billionth of that, or 1.1 nanosecond. The results were these:

dot-offset-dotimes:
User time    =        1.121
System time  =        0.004
Elapsed time =        1.110
Allocation   = 160648 bytes
27 Page faults
GC time      =        0.000

dot-offset-avoid-addition
User time    =        3.481
System time  =        0.017
Elapsed time =        3.463
Allocation   = 174160 bytes
37 Page faults
GC time      =        0.000

dot-offset-do
User time    =        0.974
System time  =        0.005
Elapsed time =        0.965
Allocation   = 177776 bytes
53 Page faults
GC time      =        0.000

dot-offset-do-u2
User time    =        0.970
System time  =        0.003
Elapsed time =        0.961
Allocation   = 157544 bytes
1 Page faults
GC time      =        0.000

dot-offset
User time    =        0.971
System time  =        0.004
Elapsed time =        0.957
Allocation   = 158672 bytes
12 Page faults
GC time      =        0.000

dot-offset-smarter
User time    =        0.956
System time  =        0.003
Elapsed time =        0.945
Allocation   = 161200 bytes
20 Page faults
GC time      =        0.000


At 3.2GHz clock of the M1 CPU that means that a single pair operation takes about 3 clock cycles — you probably won’t be able to significantly exceed that without using the SIMD instructions, which should improve it to a single cycle per operation, but that won’t be possible to do in LW (at this time) and foreign code would need to be used.

These tests have solidly confirmed an old claim that in fact C-like performance can be achieved in Common Lisp. It takes some work, but it is doable and apart from some fringe cases (like the confusing results in dot-offset-avoid-addition) it can in fact show comparable performance. Up to this point I have never seen anyone testing this claim and at some stage started to consider it a myth. 

I found this discussion and testing experience rather enlightening, my understanding of how to optimise Lisp code was rather hazy at best before it, and at this stage I think I have a very solid grasp on how it is done.

It also needs to be highlighted that the code that was used while looking much like a synthetic benchmark is nothing of the sort, as dot product calculation is a fundamental operation in neural nets, as well as in a host of DSP stuff. If you can get this one operation run as fast as it can you have probably addressed the biggest potential bottleneck in your algorithm.

Also, I compiled the provided C functions to see what their barebone executable performance would be like and it showed a marginal difference to both the native Lisp and FLI versions, literally hundredths of a nanosecond:

Benchmarking with:
Array size: 1000000
Iterations: 1000
Runs: 5

Original dot_offset:
  Average time: 937410.40 microseconds
  Operations per second: 1066.77 million
  Result checksum: 622056.2

Optimized dot_offset_smarter:
  Average time: 936505.40 microseconds
  Operations per second: 1067.80 million
  Result checksum: 622056.2
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.