At Fri, 18 Jun 2021 05:29:14 -0400, George Neuner wrote:
> My point was that the docs for write-bytes-avail et al specifically 
> mention "flush" of data, and in a way that implies (to me) that there is 
> expected to be something else underlying the buffer to "flush" to, e.g., 
> storage media, network connection, etc., ... something.  But, if I 
> understand correctly, Racket pipes are a library construct and really 
> just a named block of memory.

Agreed, but that named block of memory isn't the "buffer" in the sense
of the port API.

The intent for "buffer" in the Racket API is a thing attached to a port
where data in the buffer is not visible outside the specific port. Data
that's buffered on a file port isn't visible by reading from the file,
for example, and data that's buffered for the write end of a pipe (if
the pipe had a buffer) is not visible to the read end.

I see that I misused the word "buffer" myself in my previous response.
Where I wrote "grow the buffer", I should have written "grow the block
or memory that holds a pipe's data", or something like that.

> So it seems like "flush" is being used in the docs to mean insertion of 
> data into the buffer ...

Into the block of memory, yes. But instead of characterizing a pipe as
all buffer and no device, I'd say that pipe is unbuffered and has only
a device (for lack of a better word). All data written to a pipe is
delivered to the "device" and available to the read end of the pipe.

> which is confusing (to me) and at odds with how 
> the word typically is used in discussions of I/O.

FWIW, I really think this is the same notion of buffer as in the C
library. A `FILE*` object potentially has a buffer, `fflush` moves data
from that buffer to the underlying device, and so on. If you wrap a
Unix pipe in a C-library `FILE*`, then `fflush` pushes data from the C
library buffer (assuming the `FILE*` is made buffered) to the OS ---
even though the OS-level device is just some memory, and that memory
tends to be called as "buffer" at the OS level.

> So the limit value passed to  make-pipe  is only a maximum size for the 
> data buffer, which starts [way too] small and then grows as needed up to 
> the maximum.  Although that does make some sense - I think starting at 
> 16 bytes is a wee bit restrictive: many (most?) real world uses would 
> have to grow it very quickly.

I agree that 16 bytes is too small. Given that a port object is already
on the order of a dozen *words*, an initial size closer to 16 words
makes sense.

> Growing the buffer is expensive in any case and particularly when 
> existing data has to be copied.  With pre-allocated buffer, the I/O call 
> will spend only what time is necessary to copy new data.  That would 
> seem to be the fastest implementation possible.

There is some cost to growing a buffer, but there's also a cost to
allocating a too-large buffer, since that increases GC pressure. For
example, one existing use of pipes is in the HTTP library, where a pipe
is used to communicate incoming POST data to a decoding thread. The
pipe's limit is set to 4096 to promote streaming behavior for large
POST data, bu POST data to be much smaller than 4096 bytes in many
uses.

To get a better handle on relative costs, I tried three implementations
for pipes:

 * the current one, where the the pipe memory always grows by a factor
   of two;

 * initializing the pipe memory to 4096 bytes instead of 16 bytes; and

 * starting with 16 or (more sensibly) 64 bytes for the pipe, but grow
   the memory based on the size of the number of bytes in a write
   request instead of always doubling --- at least up to a new size of
   4096 bytes, after which the doubling algorithm takes back over.

I tried a few benchmarks:

 * a program like the one Shu-Hung posted that allocates and writes
   data to a pipe;

 * a program that alternates writes and reads within a single thread;

 * a program that writes in one thread and reads in another thread,
   with and without a limit on the pipe; and

 * a program that untgzs the Racket v8.0 source repo (because untgz
   uses a pipe to deliver unzipped data from one thread to untarring in
   another thread).

There was a measurable difference only in that first benchmark. For
writes in the range 512 to 32768 bytes, the alternative adaptive
implementations outperformed the current one by 30% on the small end of
that range and 10% for the upper end. I used 1 million iterations, and
the run time for that size range was around 100 ms. (Note that if the
data written to a pipe is later read, that would more or less cut the
difference in half.) The difference between the two new implementations
was in the noise.

Overall, although there's not much difference, the revised adaptive
implementation seems like a win for a small and easily maintained
change to the code, so I expect to push that change.

There's likely still room for improvement. Anyone who is interested
might start at "racket/src/io/port/pipe.rkt".

-- 
You received this message because you are subscribed to the Google Groups 
"Racket Users" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to racket-users+unsubscr...@googlegroups.com.
To view this discussion on the web visit 
https://groups.google.com/d/msgid/racket-users/20210622110419.6e%40sirmail.smtps.cs.utah.edu.

Reply via email to