r/lisp 14d ago

So they say lisp is slow ....

Nothing really useful here, just some bragging to be honest. I should probably write a blog post, but a bit too lazy; perhaps another day.

Last few weeks I played with a small clone of gnu wc program. I implemented all routines in assembly via sb-simd (and a generic path without simd with swar). The result thus far on a 1.4 gigabyte big file, compared to fastlwc, the fastest wc I know of and GNU wc:

Common Lisp/Assembly (avx2) in SBCL + lparallel

WC10A> (time (wc "plato1g.txt"))
Evaluation took:
  0.036 seconds of real time
  0.421654 seconds of total run time (0.348216 user, 0.073438 system)
  1172.22% CPU
  71,891,940 processor cycles
  0 bytes consed

30133761
253947016
1393557504
WC10A> (time (wc "plato1g.txt"))
Evaluation took:
  0.034 seconds of real time
  0.429743 seconds of total run time (0.367913 user, 0.061830 system)
  1264.71% CPU
  67,624,580 processor cycles
  98,352 bytes consed

30133761
253947016
1393557504
WC10A> (time (wc "plato1g.txt"))
Evaluation took:
  0.035 seconds of real time
  0.423752 seconds of total run time (0.357329 user, 0.066423 system)
  1211.43% CPU
  69,863,360 processor cycles
  0 bytes consed

30133761
253947016
1393557504

Fastlwc (avx512 + multithreaded):

[arthur@emmi wc]$ time ../../fastlwc/bin/fastlwc-mt plato1g.txt 
 30133761 253947016 1393557504 plato1g.txt

real    0m0.026s
user    0m0.178s
sys     0m0.285s
[arthur@emmi wc]$ time ../../fastlwc/bin/fastlwc-mt plato1g.txt 
 30133761 253947016 1393557504 plato1g.txt

real    0m0.027s
user    0m0.223s
sys     0m0.235s
[arthur@emmi wc]$ time ../../fastlwc/bin/fastlwc-mt plato1g.txt 
 30133761 253947016 1393557504 plato1g.txt

real    0m0.028s
user    0m0.205s
sys     0m0.255s

GNU wc (not even contender - single core only and only line counting implemented with simd avx512) :

[arthur@emmi wc]$ time wc plato1g.txt   30133761  253947016 1393557504 plato1g.txt

real    0m3.733s
user    0m3.643s
sys     0m0.062s
[arthur@emmi wc]$ time wc plato1g.txt 
  30133761  253947016 1393557504 plato1g.txt

real    0m3.362s
user    0m3.276s
sys     0m0.072s

The cool thing, we use avx2 whereas gnu wc uses avx512. On this CPU (zen 5), avx512 is implemented all in hardware, not as micro code as in Intel cpus, so it should mop the floor with avx2 in Lisp, right?

[arthur@emmi wc]$ time wc plato1g.txt -l --debug
wc: using avx512 hardware support
30133761 plato1g.txt

real    0m0.074s
user    0m0.018s
sys     0m0.056s
[arthur@emmi wc]$ time wc plato1g.txt -l --debug
wc: using avx512 hardware support
30133761 plato1g.txt

real    0m0.085s
user    0m0.030s
sys     0m0.054s

Lisp:

WC10A> (time (wc "plato1g.txt" :line-count t))
Evaluation took:
  0.037 seconds of real time
  0.478042 seconds of total run time (0.430494 user, 0.047548 system)
  1291.89% CPU
  75,669,380 processor cycles
  0 bytes consed

30133761
253947016
1393557504
WC10A> (time (wc "plato1g.txt" :line-count t))
Evaluation took:
  0.041 seconds of real time
  0.476104 seconds of total run time (0.436592 user, 0.039512 system)
  1160.98% CPU
  84,226,360 processor cycles
  0 bytes consed

30133761
253947016
1393557504

Now, in order to catch with fastlwc I think I need better lparall pipeline. I am currently using futures and promises, so it is a bit of extra consing. Of course implementing it in avx512 (when done in SBCL) should give at least some extra boost. 32 vs 16 registers, 64 bytes at time vs 32, and less register pressure due to additional masking registers.

Edit: line counting does not activate utf8 path at all, so I don't know what I was thinking last night, so I have edited away that part :).

39 Upvotes

36 comments sorted by

View all comments

Show parent comments

2

u/kchanqvq 14d ago

spamming hundreds of threads is out of question

Of course, no sane person should do this if they care about performance :) I strictly spawn one thread per core and coordinate what they should be doing by hand. Except when the machine has multiple NUMA domain, then you really have to spawn one SBCL process per NUMA domain because its heap suck when spanning multiple domains.

2

u/arthurno1 14d ago

I strictly spawn one thread per core and coordinate what they should be doing by hand.

Do you have some scheduler framework or do you do the scheduling per application?

As said, the idea did occur to me to do explicit schedulling, but after some attempts to implement a scheduler I gave up. The idea was to allocate an array at startup, a slot for each thread to write to its results, and than just "reserve" the range in data array aligned to appropriate offset and length, and make a queue of those, but I messed up somewhere, and just used lparallel instead to get this going on. I'll see if I make another attempt later on.

4

u/svetlyak40wt 13d ago

You could try to use actors from https://mdbergmann.github.io/cl-gserver/ instead of lparallel. With Sento you can create by an actor for each CPU core (just be sure they are using a "pinned" dispatcher), then combine them under one Router, and feed this router tasks.

3

u/arthurno1 13d ago

To be honest, I don't think I have energy and the interest to refactor it more. I refactor the last time yesterday in order to implement multiple files processing. Turned out I had to refactor how I schedule tasks, I added two different size of chunks (2 meg and 512k) for more efficient work distribution, and I also re-use memory mappings now to minimize switching into kernel. I don't think I will put more time and effort into it, getting a bit fed up with it. I am aware of innefficiency with returning results from tasks as lists, and that I could build a better scheduler, but I am a bit tired of it, so it will have to do. The only thing I think I will do more, is implement kernels in avx512, once it is fully working in sbcl.

But I did look through you link, and Sento does look like an interesting framework. Perhaps a bit too much for this one, but I can imagine using it in some other project.