87 comments

[ 358 ms ] story [ 2826 ms ] thread
I started a Rust project several months ago and quickly became frustrated with the state of basic terminal libraries. None of them seemed to implement the buffering and minimal-refresh of ncurses, so I was forced to rely on one of the many shifting ncurses binding libraries and all the unsafeness that comes with that (or reimplement my own—way out of scope for the little toy I was building). What's the state of the art for raw terminal libraries (not TUI helpers) in Rust nowadays?
Guh, I feel you on this one. I was shopping around for a buffering wrapper but soon realise that I either had to get basically a terminal Qt clone or raw bindings, nothing inbetween...
Unbuffered I/O can make any language you use much slower. It has nothing to do with Rust / language choice.
"Rust" sets you for the front page of hackernews, though.
Except for the usage patterns where it's faster because you avoid blowing your CPU's caches as much... The trade offs between different ways of performing i/o are decidedly non-trivial.
Everything you mentioned is irrelevant in the context of unbuffered IO. The majority of time is going to be spent on system calls, context switching, and signal handling / checking, and waiting for hardware interrupts to fire and handling them.

Also there is a lot more then mentioning cpu cache as this is very generic term. which part of the cpu cache are you talking about? instruction cache? data cache? And why do you have think they matter in this context?

My comments are perfectly relevant. Using read() and write() still uses buffers, it's just that those buffers happen to be in the kernel. read() and write() still do the kernel's form of memcpy() internally. Truly unbuffered I/O from the kernel's perspective is direct I/O that DMAs data directly into application provided memory.

It is important to note that every time buffered I/O is used, it copies data through buffers, and touching those buffers means the memory used for those buffers being touched clearly has to be pulled into the CPU's L1 data cache. In some cases the extra cache traffic for those buffers degrades performance. Instruction caches are relevant to some workloads, but are clearly not what I was referring to since data doesn't go through the instruction cache.

glibc further complicates the matter by using mmap() in some cases to directly map the kernel's buffers for data (the page cache in the context of Linux) into userspace. This avoids some of the downsides of buffered I/O by avoiding one of the copies at the expense of page fault overhead. Sometimes it's better for the application itself to use mmap() directly.

Direct use of read() and write() makes sense for some applications, especially when dealing with larger chunks of data. Saying unbuffered I/O is bad is a gross oversimplification of the reality when writing high performance applications. Like everything in programming, it's a good rule of thumb, but there is definitely an asterisk next to it.

Unbuffered I/O is slower than buffered I/O regardless of language. It’s not just a Rust phenomenon.

Making syscalls like write() every time you want to send even a character of output down a stream is much more latent than just sticking bytes in a buffer.

In general, the only time anyone should use unbuffered output is when there’s a real person consuming it - for example, when you’re prompting a user for feedback or input.

> In general, the only time anyone should use unbuffered output is when there’s a real person consuming it

I think this might be a little -too- general, as it's pretty workload dependent. I mean, if you just want to write a blob of bytes once, just write the blob IMO.

I don't think using a buffer in those cases ever really hurts, but sometimes makes what should be simple a bit more complicated than necessary.

Even for stuff as simple as handling LOBs, I buffer on both read and writes. Some DB APIs really do “what you tell them to”, even if that means being unbearably slow when unbuffered

The worst I’ve seen was actually streaming the LOB on getLength. Since there’s no JDBC API for lob.getContentAsArray(), you need to call lob.getContent(1, lob.length()) (notice that you’d start at index 1…), which by default would stream the LOB twice under the scene!

Definitely too general of a statement. I previously worked on real-time mapping and the only way to get the necessary write performance was to go unbuffered and manually split off a block at a time to write. The same was true of reading - it was faster and made sure the # of syscalls in any frame was predictable.

As a side note, the real issue here is that unbuffered is the default. Other languages and libraries I’m aware of make it difficult to get in such a mode.

Lots of people are writing Rust code who aren't previously versed in the nuances of optimizing bare-metal code. They're (reasonably) expecting amazing performance, because Rust tends to give you that by default compared with higher-level languages. But repeated calls to println!() is one of the biggest and easiest-to-make performance mistakes for those newcomers, so I think it's fair to call it out.

With that said, the OP could definitely have done a better job making it clear that this problem isn't specific to Rust

Developers from other languages, like C, C++, or even Python, would not have expected this particular problem, because it wouldn't have occurred to them that the thing they get from doing a file-open call and that the JSON reader library accepts is a raw file descriptor (for C programmers, the result of 'open' rather than 'fopen') that does unbuffered I/O. In other languages libraries are oriented around buffered I/O structures (a FILE or an iostream).
To me the biggest reason for the surprise is the signature for 'write'

I'm used to a raw stream allowing a few specific operations:

- read/write/peek one byte - write a byte array, but you must explicitly specify length and offset

So seeing a call that simply takes an entire byte string and writes it gives me "buffered I/O vibes". It's hard to quantify, but with how ubiquitous that pattern is I'd be surprised if there are a lot of people who expect similar.

This had been upvoted and now it is downvoted, even though it is obviously true, as the article demonstrates.
For what it's worth, I run into the same nuances frequently in Python. For example I often want to run

    some_script.py | tee log.txt
But by default, Python will turn off line buffering when it detects it's not talking to a tty. So unless you know to call flush, or to add the right env var or interpreter flag, you're not going to see any early output from tee. This can be super confusing for newer programmers.
> Python will turn off line buffering when it detects it's not talking to a tty

Okay, I’ve been running into a bug in one of my Rust programs and I think this comment has the answer. My program uses a Command [0] to listen to stdin and stdout. Theoretically that Command should terminate when the parent process dies, but if the Command is a Python script that doesn’t include a `print()` with `flush=True`, it doesn’t die. I think you might have found my problem for me.

[0]: https://docs.rs/tokio/1.15.0/tokio/process/struct.Command.ht...

(comment deleted)
I don't think it's fair to call out since no one is putting a println! in hot loops unless they're debugging
Not remotely true. I learned this lesson the hard way a couple years ago while working on a program that did exactly that.
Why were you calling println in a hot loop instead of using a logging library?
Because it was a simple CLI tool that spit out a bunch of output as part of its core functionality.
It sounds to me like println! and other I/O macros and functions should buffer I/O by default if the file descriptor is not a terminal. This would be consistent with how other languages’ standard libraries behave.
Totally agree. In contrast, for example, FILE objects in C are (mostly) buffered by default. So in that sense, Rust defaults are less sensible for some developers who would be better served by buffering, but aren't aware that they need it. But I also think that Rust's default is totally reasonable and it's just something you have to learn about as a systems programmer.
> In general, the only time anyone should use unbuffered output is when there’s a real person consuming it - for example, when you’re prompting a user for feedback or input.

I'm in the camp that logging should be always unbuffered. If you buffer logs, and your process dies for any reason, you lose your log exactly when you really need it to know what happened.

Buffered I/O is usually not lost when a process exits. Usually buffers are flushed automatically upon exit when the file descriptors are closed. And if a program truly crashes and leaves a core behind, the unflushed buffers should be found in the core file.
If the process exits in an orderly fashion then, yes, it will flush logs, and also do a bunch of other nice things. The problem arises when it dies in a not-so-nice fashion.

(Also, not every environment is running with unlimited core size.)

Alternative title: Poor Interface Design Makes Most Rust Programs Much Slower

That programmers need to consider this stuff on each new project is a failure of the standard library design, it's a problem not even C++ suffers from

Not even C has this problem - stdio handles this just fine, by deciding whether to buffer or not depending on where the data is going to (tty vs a file).
No, it's a design choice, not a 'failure'. Hidden behavior has its own pitfalls, and you can argue this all day, but there are already 100 languages which go the other way and cleverly optimize things behind the scenes every chance they get. I wouldn't mind much if they made IO buffered by default, but I don't mind it's not, either, because it's consistent with the design ethos.
In my experience, the most ergonomic implementation is rarely the most performant one in C++, you usually end up having to use some SFINAE monstrosity or sprinkle constexpr if all over the place. std::optional comes specifically to mind as one of the big offenders here.

You've also gotta be careful with unexpected and implicit assignment/copy/move constructors.

More than half of all the C++ wisdom one needs in their career is when to avoid stdlib and which part of boost to use. Due to C++s religious devition to 80s compatibility, it becomes a minefield of a Don'ts, with a narrow path of Dos.

The fact boost is a billion libraries in one speaks in of itself about the abysmal dependency situation in C++. There's also a buncha dep managers that are at each others throats. Also, C++ people like to poke fun about js devs and npm/leftpad, but forget that they use boost for even more rudimentary string operations. Or they implement the same thing over and over again. Or my favourite: copy+paste the same code in multiple projects, and then have divergent implementations of the same thing with different bugs and/or API surface; bonus points if the thing is part of an RPC implementation.

As much as the edgy-dev crowd loves to belittle the very concept of dependency management, at least JS/Ruby/Python ecosystems give a shit about DX and made sure the package managers can be used with the same repositories/sources.

But hey, at least C++’s IO is buffered by default. Amazing.

I got distracted by my rant above: I actually PREFER the default is unbuffered, because it doesn't violate the principle of least surprise (if we remember that Rust is a systems programming language). The fact you expect the default to be buffered is probably due to exposure to C/++.

> The fact boost is a billion libraries in one speaks in of itself about the abysmal dependency situation in C++. There's also a buncha dep managers that are at each others throats.

There are no dep managers for C++ that have any kind of universality to them.

To reiterate for the umpteenth time: many C and C++ libraries come with compile-time flags that totally change the nature of the resulting library. It is not possible to refer to a "canonical" version of the library the way you could with something for Java, Python, JS etc.

When I say "this code relies on libfoo", it is necessary to know if there are build/compile flags for libfoo that could change the nature of libfoo.{a,so}. If there are not (common, but not universal), great. If there is just one, probably manageable. If there is more than one, game over.

Also, only C++ iostreams are buffered by default. Just like in C, where you are free do IO with syscalls if that floats your boat. There's nothing more or less obvious about this than in C, where stdio vs. syscalls is an essentially identical choice.

> To reiterate for the umpteenth time: many C and C++ libraries come with compile-time flags that totally change the nature of the resulting library. It is not possible to refer to a "canonical" version of the library the way you could with something for Java, Python, JS etc.

That just iterates GP's point: C/C++ dependency management is abysmal, but it's "hard" because the languages themselves do not have the capability to express the required metadata in any standard way. As an example, the first time I tried to compile Ardour took me several hours finding and installing the right dependencies for my distro.

PL designers have learned from these mistakes, it's not worth defending them or accepting that it is the way the world should be...

> but it's "hard" because the languages themselves do not have the capability to express the required metadata in any standard way.

Of course the languages don't. It's not a part of the language. If I build libfoo with -Dexclude_feature_bar which just drops 2 source modules from the library build, how can the language express that?

On Debian, you get the Ardour dependencies like this: apt-get build-dep ardour6 If your distro makes it hard than that, that's a distro problem.

Of course, the libraries Debian or any other distro will install when you do that are not the ones we use to build Ardour, because we have patches for several libraries that will never be accepted upstream but are necessary/useful/important for Ardour. You can't express that with the language either.

I think you and the previous poster are talking about the same thing. I think they are pointing out that there's no way to express (I guess there is via macros, but it's a workaround) this, whereas it's innate to Cargo and Rust.
It cannot be innate to any language because I may wish to link libraries written in other languages.

You cannot use Cargo or Rust to describe how a C or C++ library was compiled and linked.

I don't see why it can't be a part of the language, since the language implementation requires that the notion of compilation configuration exists and even C/C++ has a specific language construct to check for it (through the preprocessor).

> If I build libfoo with -Dexclude_feature_bar which just drops 2 source modules from the library build, how can the language express that?

By expressing within the language that the library can be configured with an optional `exclude_feature_bar` flag. Then downstream dependencies can express they require importing libfoo configured with exclude_feature_bar.

At least in Rust where the compilation unit is a crate, crates must express what their compile options are, and dependents express that they require specific compile options from their dependencies. You could argue this isn't a property of the language, but I would argue it is since it's so tightly coupled to the language implementation (despite living in a config file, that file is required to build anything)

Since C and C++ eschew this fundamental requirement of packaging software and pass the buck to the library authors to figure out a way to describe available configuration options, there's no standard way to do this, and dependency management becomes quite difficult. That doesn't mean that other language authors can't solve the problem. And it plays into what the original comment said, which is that managing dependencies in C/C++ is abysmal.

So let's take one of my canonical examples. libfftw (Fastest Fourier Transform in the West) can be built with at least the two following critical options:

   1. use double or float for computation
   2. thread-safe or not
The result is 4 possible versions of libfftw:

   double + thread-unsafe
   float  + thread-unsafe
   double + thread-safe
   float  + thread-safe
What aspects of rust's crate design will allow me, developer of an application that uses libfftw, to define which of these I want to use? How many crates will I have installed in order to switch between any of them?
So we're in agreement that the C and C++ languages cannot express compilation requirements. I'm saying this is a reason why dependency management in these languages is hard. Any language depending on C/C++ will not be able to magically fix the lack of module semantics, just like they can't magically fix use-after-frees across FFI.

However, if fftw was wrapped in a Rust crate, the manifest can express those using features in the Cargo.toml file [1] which can be read in a build.rs[2] script which would provide the proper configurations to libfftw at build time for all dependents that specify which options it needs. In this model you won't need more than one crate installed for libfftw, since the crate expresses everything needed. Crates are not static collections of object files like C/C++ libraries.

[1] https://doc.rust-lang.org/cargo/reference/features.html

[2] https://doc.rust-lang.org/cargo/reference/build-scripts.html

Yes, we agree that the actual languages named C and C++ cannot express compilation requirements. As far as I can tell, neither does Rust as a language. It just so happens that Rust has (thus far) settled on a single build system (Cargo) which (like most other build systems) has a way to express them (and a very expressive one at that, since it is written using Rust itself). This doesn't seem that different from projects that use build systems that express all their compilation requirements, with the difference that the build system uses a different language (e.g. Python).

The crate design you've described works as long as all your dependencies are wrapped in a "crate" of the correct type (in the current case of Rust, a Cargo crate).

But is it even close to realistic to imagine that there will be correct "crate-like" encapsulations of every C/C++ library, thus enabling them to be used seamlessly from each language with a "crate-like" system? I don't know, maybe this is genuinely likely, at least for Rust. To me, having to assume that the hundreds of millions of lines of code that already exist must be and will be wrapped in a crate-like system seems like a bad bet, and I'd rather have a build system that doesn't require this to be true for me to use any pre-existing code.

The model you are describing for crates seems to also break down for 3rd party non-open-source libraries, where (re)compilation as part of the build process for my own application is not an option. In such cases, it's hard to see how proprietary developers would be content with anything other than "a static collection of object files" for their licensable libraries. But maybe I'm wrong about that too.

> since the language implementation requires that the notion of compilation configuration exists

This is not true for most languages. I can use a build system that has its own options, and in the build specification I can say:

   if (defined (build_option1)):
        sources += source_file1
   else
        sources += source_file2
The compiler and linker have no idea that there is any choice between the two source files, or that the build time option exists.

The two files can each provide the same functionality (e.g. hand-coded SIMD asm, but for different target systems); nothing in the language knows anything about this.

This is a rather outdated model for languages, for example the .NET CLR, Rust, Go, NodeJS, Python (and I believe the JVM?) all have module semantics or language specifications that describe how compilation units find each other, and the compiler () is responsible for mapping source files to import statements. The compiler is what is responsible for deciding which file is compiled and how it is configured to do so.

() or de facto standard build tool like Cargo

But that said the example is a little contrived since you wouldn't use a compile configuration to exclude a source file in most languages that have more flushed out module structures than C

I am not sure what interpreted languages would have to do with "compilation units", but perhaps I'm missing something.

The example comes specifically from the build system for Ardour, where we have hand-written assembler and will incorporate different files into build depending on the target (SIMD) architecture and expected runtime environment. I would not call that contrived.

I have no doubt at all that Cargo can handle many, even the majority, of projects of a certain scale. I have very strong doubts that a build system so centered on a particular language and with so many assumptions about how things are written and built can work for all projects. It's a problem when the build system falls down on the most demanding stuff, because it leads to people building "meta-build systems" around it, and we've seen how that turned out in the past.

Interpreted languages have compilers too, they just emit bytecode or an AST instead of object code.

I think you're misunderstanding a lot of what I'm describing, it has nothing to do with Cargo or Rust but the fundamental limitations of C/C++ that explain why dependency management that involves either of them is extremely painful compared to any modern language.

No. But is very surprising when you see Rust how high-level is as language yet it target system programming.

One major philosophy aspect of Rust is that (almost)everything is explicit and you build abstraction from the bottom.

(comment deleted)
In C/++ land the implicit buffering causes all kinds of confusion among beginners and veterans alike. From “why does this not print” to “why is the performance so bad in multithreaded programs”. In practice people avoid C and C++ I/O and use platform-specific functions to have more control over the buffering. Good job!

There are also other reasons for designing it this way such as composability. In general it’s bad idea to try to undo an abstraction, and instead one should build on top of primitives instead.

I am not all that knowledgeable about rust but I think that if you really want to decrease the numbers of system calls when interacting with files, writev and readv should be considered. I wonder if BuffWriter/Reader could be made to use it, as it would have the same "atomic" behaviour and would forgo the need to concat strings. Still, the char*s could become invalidated once the system call is made.
This solves the “spatial” part, while the actual problem is the “temporal” part: it’s not about many writes at once, it’s about many writes in different parts of code, spaced apart in time.
Typically they are orthogonal concepts. In Rust you could probably do it if you’re accepting ownership of the string but if I recall correctly write takes a slice so the contents are not guaranteed to be valid after the write call (eg the user might reuse the buffer for the next write).

It might be interesting to have a version of a Writer trait that handed ownership over in addition to supporting slices and handled things under the hood for you. The API may be too complex to bother with. Buffered I/O is typically a fine middle ground. When you start needing writev you’re changing the I/O model more drastically.

FWIW, the Read/Write traits have a vectored read/write method, which File implements using readv/writev on supporting Unix platforms:

https://doc.rust-lang.org/src/std/sys/unix/fd.rs.html#77

https://doc.rust-lang.org/src/std/sys/unix/fd.rs.html#144

Buf(Reader|Writer) are vectored I/O aware, bypassing the internal buffer whenever the total length of the provided buffers is larger:

https://doc.rust-lang.org/src/std/io/buffered/bufreader.rs.h...

https://doc.rust-lang.org/src/std/io/buffered/bufwriter.rs.h...

I kind of understand why Rust made buffered IO explicit and on-demand instead of going down the C route and making it the default, but still I think there's a significant risk lots of programs are going to be suboptimal due to developer ignorance or the expectation of having buffered IO by default like with stdio and iostreams.

It would have been IMHO probably less clean but safer to make the standard IO constructs like File buffered by default (which is what users expect in the vast majority of cases) and provide on the side unbuffered variants. Thankfully modern OSes with caches and stuff somewhat mitigate this thing, but it can make IO on less advanced systems way more problematic than it should be.

I think it would go against Rust's philosophy to have hidden (and inconsistent) behavior, like automatically buffering some things but not others. Generally Rust tries to give users explicit and ergonomic control over those sorts of details.
When developing large scale applications ( e.g. any app that needs third-party libraries ). You are going to have to deal with developers who either

1. Didn't know about a nuance. 2. Didn't care about said nuance.

Creating magic incantations to achieve typical expected behavior will result in many libraries doing the unexpected behavior. Which means that the typical code that any end user works with will probably do the unexpected thing.

It's all relative. I'm very glad that JavaScript doesn't try to make you deal with output buffers, but Rust's goals as a language are different.
The problem here is that the uncontested ruler of said system languages, C, has a completely opposite behaviour by default. The C standard clearly states that "When opened, a stream is fully buffered if and only if it can be determined not to refer to an interactive device" (§7.21.5.3.8), which means that generations of developers have learned to always expect buffering when opening files `fopen`. Most higher level languages follow the C library when setting their I/O convention, and this has been the trend for most of the last 40 years.

The fact that Rust goes in the opposite direction makes sense from a logical standpoint, but as the article itself shows will almost certainly end up causing performance issues. You can rest assured that people *will* forget to buffer their files because it is something they almost never had to care about in the past.

It's clearly documented in multiple places that it's unbuffered (fs docs, io docs)

What's magical here? Rust makes it there's no need for magic. Most things like these are designed to compose. Here it's no different, and certainly not any more magical.

Alternative Title: Many Problems That Have Affected Software Development Since Denis Ritchie Roamed the Steppes Continue to Affect Rust Programming
I can't remember the last time I came across a modern language where the "easy default" is unbuffered I/O.

A beginner learning C++ today will be getting familiar with buffered I/O primitives by the time they write a "Hello World"

Java mostly keeps the "useful" access patterns (anything more than reading/writing a single byte or manually passing in byte buffers) in buffered wrappers so even a clueless beginner is inclined to use them.

People are missing the forest for the trees here, it's about ergonomics and encouraging people to do the right thing by default.

Ironic to see this attitude in a Rust comment section of all places, isn't this the same attitude that leads people to dismiss Rust? "Just write correct C code without UB, the major pitfalls haven't changed in the last 4 decades duh?"

> I can't remember the last time I came across a modern language where the "easy default" is unbuffered I/O.

"Hello World" in C from 1986 uses printf() which is buffered.

So yeah, it's been a long, long, long time.

It's because there's this new trend of high-performance, bufferless, copy-less technologies like io_uring getting mindshare. Beginners who don't understand why these technologies work then blindly remove buffering, or forget to add buffering in the first place.
It’s not really new, it’s a return to how things were done before Unix.
What is the forest here (asking genuinely)? Language usage ergonomics?

I think it's fine like this, because most of the time the language makes it easy to wrap/convert a writer into a buffered writer and use that instead.

Also, it preserves the principle of least surprise. If I call write on a Writer, I expect it to write immediately. When I want it to be buffered, I would either look for it in the stdlib (and in Rust's case expect to find it) or look for a lib (sans implementing it)

> If I call write on a Writer, I expect it to write immediately.

But you can't know what "to write" actually means.

When you call write(2) on a POSIX system, what is it that you think happens immediately? Patterns of NVRAM storage are changed? Fluctuating voltage on a cat-5 cable wobbles a bit in response?

OK, maybe you can get away with saying "If I call write on a Writer I expect it to hand over the data to the kernel immediately", and that's somewhat reasonable. The problem is that such a design encourages beginner programmers to use unbuffered IO, which is generally the wrong thing to do.

They're not encouraged to use unbuffered io. The documentation for FS and IO both point you in the direction of BufRead and explain why you might want it (in the intro section of each).
I mentioned what least surprise is to me:

I'm used to a raw stream allowing a few specific operations, if it only allows:

- read/write/peek one single byte

- write a byte array, but you must explicitly specify length and offset

I assume it's unbuffered. Usually those are the building blocks of a buffered stream API after all. It also matches write(2) closely (offset replaces pointer math)

What surprises me is that it takes a full byte string and writes it with no hints about offset or length, usually that's only provided with buffered interfaces.

-

Again, there's no written rules about it, I'm just sharing my experience from writing code in a lot of different languages.

Someone else might have an experience with a different set and have no such expectation, so I'm not saying Rust is wrong, I'm instead pointing out why this isn't a "well duh" moment

I think it doesn't ask for length because it would increase the chance of mistakes happening. You don't really need to pass the length "manually" because its encoded in the value already. Not sure what you mean by offset, but you could just index with a range probably (ie `bytes_buff[n..]`), but that won't work for Strings (because they're internally encoded as UTF-8, but you could use `.chars()` and then the iterator methods on it to select which part of the string you want to write).
This comment is kind of making my point.

Unbuffered APIs are meant to be tricky: They ask for length (even in languages where arrays have a fixed length like Java) because the idea is:

- you're either building out a buffered wrapper and can't rely on the encoded length value (because your array length will be the entire buffer while you might only want to write part of it)

- or you want very precise control over the writes and are willing to risk those mistakes. So they need to open up that risk of a mistake.

It's an API that mirrors to the user how the underlying syscall operates, so the underlying complexity is being very plainly exposed.

-

Usually it's the buffered wrapper that will have a convenience method that takes a String or a full array without additional information, or there'll be an explicit call to force a buffered wrapper to use a 0-sized buffer.

C++: Without explicit call to setbuf, system defined default buffer used (matches POSIX behavior)

Java: FileWriter vs FileOutputStream, FileOutputStream is unbuffered will not take a string without manually encoding it to bytes first, FileWriter will take a string and is buffered

Python2: need explicit 0 sized buffer

Python3: need to flag as unbuffered and automatically converts file to binary mode, so calling write("SomeString") will not be allowed with unbuffered output.

C#: FileStream buffered by default, must explicitly ask for 0-size buffer

Obviously this won't hold across every language. But you can see that languages consistently make it easier to do the "right" thing in this case, while Rust isn't.

I'm not necessarily holding it against Rust either to be clear, but I'm holding it against the droves of comments here that are acting like this is such natural behavior you'd be a rookie to not catch it... it's been a loooong time since most people have used a language where that was "easy" behavior to uncover accidentally. Like I said even C++ went out of it's way to improve things in this regard.

Right, I see. I guess I understand where you're coming from (basically your expectations are shaped by other languages). That's fair enough, and I've had some of my expectations broken fore the same reasons in Rust, and it was around ergonomics too.

Rust's solution to the problem, even if a departure from traditional solutions, is more ergonomic, and I like that.

There's no reason buffered or buffered IO needs to be trickier than the other! Why sacrifice one? Sure you could have Write buffered by default and have UnbufferedWrite instead, but I personally find this "reversed API" idea really weird. It seems like a subtractive API rather than an additive one, and it also doesn't compose as nicely.

Your argument about size proves my point too, so we're going in circles I guess. I'll summarise why I prefer Rust's approach (sans ergonomics): it's harder to introduce logic errors if all the information can be inferred from the value itself. If I want to send "hello", and somewhere I fucked up, so I'm thinking 4 bytes instead of 5 and now I sent "hell", and threw 'o' to the RAMs. Why? I can just tell it I wanna send "hello", and it does it. It's more robust. If I wanted to send just "hell" (assuming it's bytes) in I can just create a slice to the necessary data and pass that. The slice explicitly itself signals to other devs I wanna send a subset of the buffer, too.

Doing it this way, buffered or unbuffered doesn't really matter: The API stays the same. Adding buffering is usually a one line change of converting your writer to a buffered writer. Someone called Rust's approach an "arcane incantation", if anything the only magical things here is the special/magic sized buffers or numbers in other languages you highlighted. Furthermore, I think half of the existing implementations probably didn't even think about this design decision at all, they just inherited it.

At the end of the day, this discussion about buffering isn't super important in the grand scheme of things. The stdlib gives you just building blocks (Rust has great ones, IMO) to build a networking library you want, so your (un)buffering is going to be tucked away in a couples of places and you'll be using your own `report` or `send` or `set` methods instead of a sprinklings low-level write/read syscals everywhere anyway.

The `write` function here comes from the `Write` trait[0]. This isn't the unbuffered write API, it's just the write API. What it's writing to could be a `File`, or it could be `StdOut` or a `Vec<u8>`.

It also doesn't take a string; it takes a slice of bytes. In Rust, prefixing a string literal makes it a `&[u8]` instead of a `&str`, which is why you can just pass it straight into `write`.

Adding a length field would only make it more error-prone to use without any added benefit. Slices already know their length, and subslicing is trivial. If you have a larger slice, and you only want to write a subsection of it, you just index it. Forcing the user to pass in two lengths is just silly, and all the API implementations would do is subslice the input to that second length anyway.

You could argue that `File::open` and `File::create` should return a `BufReader<File>` and `BufWriter<File>` respectively, and I would agree that this would be reasonable as the most common use-case probably benefits from buffered IO. However, that ship's already sailed, and those functions can't be changed.

The description for the `File` type should also more explicitly state that IO is unbuffered. I'm surprised it doesn't, the docs are typically better at that sort of thing.

[0] https://doc.rust-lang.org/std/io/trait.Write.html

It was never lost on me that the length is encoded, my very first comment mentions it's the same in Java: the number of bytes you pass in is encoded.

My point isn't that every language should force that syntax ('offset' for example, doesn't make sense in languages with pointer math for example)

Rather the point is that when the language doesn't follow that convention, it should buffer by default. I guess it is a blind spot in the standard library then

> I can't remember the last time I came across a modern language where the "easy default" is unbuffered I/O.

Java. There

In that regard it got turned weirded with multiple buffers which slows down as well, just on as much as totally unbuffered. Java 1.4 added nio where the buffers were part of the framework.

I mentioned Java didn't I? Even in the pre-NIO APIs:

If you try to write a string to a FileOutputStream the way that Rust code did this is what it looks like:

    byte[] byteString = "Hello World".getBytes(StandardCharsets.US_ASCII);
    myStream.write(byteString, 0, byteString.length);
But if you use a FileWriter, which has a built-in buffer and wraps the OutputStream:

    myWriter.write("Hello World");
For me, if the Rust example had required a length and offset, I'd immediately assume it was unbuffered. I keep clarifying: I realize it's not a rule, but I guess that's just a pattern I've seen across a lot of languages. Maybe because it somewhat matches write(2)?
Indeed, Reader/Writer framerwork provides buffering (esp. now).

However, in my experience the writers are not very commonly used which is a stark contrast to BufferedReader that's everywhere (it comes with readLine()). Instead, writing is commonly implemented via encoding/conversion/etc. to a byte[] and then written directly to the output stream.

I'm surprised serde_json takes a std::io::Read rather than a std::io::BufRead. I wonder why they made this choice.

Not only does using plain Read make this mistake possible, but it also means they have to be doing byte-by-byte processing. [1] So not really possible to do SIMD-oriented parsing acceleration like simdjson uses. (I gather simdjson's approach is quite sophisticated, but a simpler version is to scan for the next delimiter via a memchr2 on " and \, then have a routine that validates the characters until then are valid via SIMD methods.)

[1] or do larger reads into their own buffer. But they're clearly not doing that given this strace output. And it'd be redundant when folks do use BufRead.

Taking in a generic T: Read is the recommended way to accept streaming inputs. It is up to the user whether they want to buffer or not. You can pass in a BufReader into a T: Read no problem, and it will buffer any reads. That the reads internally are buffered is an implementation detail serde_json has no business knowing about.

The converse is potentially disastrous - if you read too much from a TCP stream you could hang your program waiting for more input that never comes.

> Taking in a generic T: Read is the recommended way to accept streaming inputs.

Recommended by whom?

When you want to process stuff in bulk, it's better to use BufRead::fill_buf (reusing the BufRead's buffer) rather than copy into your own.

For example, I wrote some code which basically skips some escape bytes. [1] It wraps a BufRead and is itself a BufRead. Its caller actually can process bytes straight from the buffer of the BufRead you supply, skipping two layers of copying.

> The converse is potentially disastrous - if you read too much from a TCP stream you could hang your program waiting for more input that never comes.

You can implement any idea badly, but that doesn't mean the idea itself is bad. There's a difference between taking advantage of buffering and expecting to operate only on full, fixed-sized buffers.

[1] https://github.com/dholroyd/h264-reader/blob/60ed66dc4dbfe74...

For what it's worth, if you want the best performance out of Serde you have to use from_str or from_slice, regardless of if you pass a BufRead to it.

https://github.com/serde-rs/json/issues/160

In that issue, they're talking about specialization. With that language feature, they'd be able to make this interface optimal for either mode:

* if the caller supplies a BufRead, they can use fill_buf.

* if the caller supplies a Read that isn't BufRead, they can wrap it in a BufReader or the like, and then use fill_buf on that.

Stable Rust doesn't have specialization today, so at most one of those can be handled optimally. (It looks like neither of them is handled well at all right now.)

Maybe they're designing the simplest possible API for post-specialization. They might have been a lot more optimistic when they chose it than I am now about when Rust will get specialization. Or they might be heavily prioritizing API stability for years.

Right now this interface is what I would call an attractive nuisance, leading to the problem in this blog post.

What I would do for today is to have a method that explicitly takes a BufRead and uses it appropriately. And then perhaps a convenience method that explicitly wraps a Read in a BufRead. And the caller chooses. I'd deprecate the attractive nuisance interface for now (maybe reversing that later).

Multiple of your “senior” developers didn’t catch this in a “senior” developer’s PR? Have they ever read any of the IO documentation, since this is pointed out literally everywhere in the docs?

https://doc.rust-lang.org/std/fs/struct.File.html

https://doc.rust-lang.org/std/io/trait.Read.html

You don't even need to be familiar with rust to know this - it's a feature of pretty much language.

That said, I can guarantee rate I have missed such issues in my code both as the writer and the reviewer. Saying "it's in the documentation" doesn't mean it's inexcusable to make 5he mistake.

Look at this guy over here "making mistakes" and "missing things in code review." What a fraud.

Btw, pointing to documentation and saying "see, it's documented!" Every time something has lame or broken or unexpected default behavior doesn't make it any less lame or broken or unexpected. Once you feel like you know how to use something, you aren't always checking the docs to get a refresher on the caveats. So tone it down with the accusations and the "helpful" links, everyone else has access to the same Google you do.

Everybody makes mistakes. Being senior doesn't make you infallible, or otherwise we wouldn't spend huge amounts of resources building tools that are essentially mistake-catchers: smarter type systems, linters, static analyzers and so on.

Also, senior people are more likely to make the kind of mistake explained in the article than less experienced developers. This is because:

- They are more focused on the "big picture" (algorithm correctness, general approach, system architecture, etc.) than on minute details.

- They tend to read less documentation because they don't need to reach for it so often.

- They have learned that a mistake such as this one can be easily corrected, and hence dedicate their attention to more important matters (avoiding data corruption for instance).

>"In this post, we will look at a common source of bad performance on Rust code that can trip up even veteran developers, and what you can do when this happens in your programs."

"Veteran developers", the sort of who would use Rust after having worked in C/C++/etc would know very well that buffering on very granular IO is a must except some specific situations. Otherwise they're anything but. And it has nothing to do with Rust. This is of course when IO libraries used do not already take care of that.

The defensive tone by people in this comment section worried that this is some attack on rust is surprising. Experienced people write bad code sometimes. Rust standard library may or may not encourage people to write poorly performing code sometimes if they aren't paying attention to (or don't know) fundamentals. Chill.
I am not using Rust as I have nothing to gain from it ROI wise. So there is nothing for me to defend. And I am totally cool, no need to chill.