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.