Subject: Re: bug#73431: Add `setf` support for `stream.el` in ELPA

From: Okamsn <[email-masked]>
Date: Thu, 03 Oct 2024 00:19:28 +0000

Philip Kaludercic wrote:
> Okamsn <[email-masked]> writes:
>> Michael Heerdegen wrote:
>>> Does changing the internal representation of streams have an effect
>>> on the speed of the run code?
>>
>> I think that it does make it slower. I am trying to test it, and I think
>> that making records is slower than making cons cells. I think that
>> accessing the rest of the stream takes longer because the accessors
>> created by `cl-defstruct` always perform type checking. It seems to take
>> about twice as long when compared to naively using `car` and `cdr`.
>>
>> Do you think that it would be better to disable the type checking in the
>> accessors? If so, would you please share how to do that? The manual
>> talks about using `(optimize (safety 0))` in a declare form, but it also
>> seems to say that it cannot be done locally for just the `cl-defstruct`
>> usage. If it cannot be done, do think it makes sense to use
>> `make-record` directly, along with custom function to replace the
>> generated accessors?
> 
> It the overhead noticeable, or just measurable?

I’m not sure what counts as “noticeable”.

Using the benchmark macros given at 
https://github.com/alphapapa/emacs-package-dev-handbook#benchmarking, I 
tested getting the "first" and “rest” of streams, both as fresh streams 
and as already evaluated streams.

These are the results I get for a stream from a list using the current 
implementation:

| Form                     | Tot. runtime | # of GCs | Tot. GC runtime |
|--------------------------+--------------+----------+-----------------|
| stream 10 evald: rest    |     0.015259 |        0 |               0 |
| stream 10: rest          |     0.044525 |        0 |               0 |
| stream 10 evald: first   |     0.059650 |        0 |               0 |
| stream 10: first         |     0.074379 |        0 |               0 |
| stream 100: rest         |     0.132317 |        0 |               0 |
| stream 100 evald: rest   |     0.132821 |        0 |               0 |
| stream 100 evald: first  |     0.198041 |        0 |               0 |
| stream 100: first        |     0.205684 |        0 |               0 |
| stream 1000 evald: rest  |     1.249168 |        0 |               0 |
| stream 1000: rest        |     1.250730 |        0 |               0 |
| stream 1000 evald: first |     1.835921 |        0 |               0 |
| stream 1000: first       |     1.857300 |        0 |               0 |

These are the results I get for a stream from a list using the 
struct-based implementation:

| Form                     | Tot. runtime | # of GCs | Tot. GC runtime |
|--------------------------+--------------+----------+-----------------|
| stream 10 evald: rest    |     0.036241 |        0 |               0 |
| stream 10 evald: first   |     0.048213 |        0 |               0 |
| stream 10: rest          |     0.048221 |        0 |               0 |
| stream 10: first         |     0.048285 |        0 |               0 |
| stream 100 evald: rest   |     0.312544 |        0 |               0 |
| stream 100: rest         |     0.321046 |        0 |               0 |
| stream 100 evald: first  |     0.439694 |        0 |               0 |
| stream 100: first        |     0.441674 |        0 |               0 |
| stream 1000: rest        |     3.032329 |        0 |               0 |
| stream 1000 evald: rest  |     3.142683 |        0 |               0 |
| stream 1000: first       |     4.113174 |        0 |               0 |
| stream 1000 evald: first |     4.132561 |        0 |               0 |

You can see that the struct-based run times are about twice as long.  I 
think this is from the extra work done by the accessors.  For example, 
the type-checking is run multiple times when accessing the “first” and 
“rest” slots, because the accessors are also used in `stream--force`.