I/O library 6x faster than fmt, 10x faster than stdio.h and iostream(github.com)
github.com
I/O library 6x faster than fmt, 10x faster than stdio.h and iostream
https://github.com/expnkx/fast_io
116 comments
One should probably point out that the library/readme is not the only suspicious thing. The author often has huge problems with terminology and/or communication in general. He routinely confuses output with formatting, which might explain some of the "benchmark results".
[1] https://www.reddit.com/r/cpp/comments/dc5t1n/reply_to_herb_s...
[2] https://www.reddit.com/r/cpp_questions/comments/efuemt/c_wit...
[3] https://www.reddit.com/r/cpp_questions/comments/f7zgat/why_d...
[1] https://www.reddit.com/r/cpp/comments/dc5t1n/reply_to_herb_s...
[2] https://www.reddit.com/r/cpp_questions/comments/efuemt/c_wit...
[3] https://www.reddit.com/r/cpp_questions/comments/f7zgat/why_d...
There has been an ongoing trend of publishing “faster” libraries with shaky foundations around the performance claims.
A great many of these libraries are also, unfortunately, “newbie traps” (faster is better, right?)
A great many of these libraries are also, unfortunately, “newbie traps” (faster is better, right?)
Yep. Like not handling the same corner cases or the same operating systems.
same here. All i/o libraries eventually hit OS barrier. I can't really see how it could be optimised to gain 6x speed up. I/O libraries are basically transporters between userland and kernel space.
Not when formatting is involved. stdio and iostreams are notoriously not particularly fast when it comes to formatting. So, while I don't know anything about this particular library, the 6x figure does not sound suspicious to me.
Source: have spent a fair amount of time working on speeding up human-readable log output, the performance of which tends to be dominated by formatting.
Source: have spent a fair amount of time working on speeding up human-readable log output, the performance of which tends to be dominated by formatting.
> All i/o libraries eventually hit OS barrier
Indeed.
> I can't really see how it could be optimised to gain 6x speed up.
Performance claims appear to not even be based on I/O but only on two formatting microbenchmarks and are severely misleading https://news.ycombinator.com/item?id=23311726
Indeed.
> I can't really see how it could be optimised to gain 6x speed up.
Performance claims appear to not even be based on I/O but only on two formatting microbenchmarks and are severely misleading https://news.ycombinator.com/item?id=23311726
Documentation for C/C++ libraries is usually just the header file: https://github.com/expnkx/fast_io/blob/master/include/fast_i...
I was expecting there to be Doxygen comments in there, but there wasn’t. So, no, this header is not documentation. I don’t want to read all the code to understand how to use a library; I want examples.
In many applications I can get 10x faster I/O using stdio.h just by radically increasing the buffer size. It turns out that less you talk to the kernel, faster you are.
/* typically 16 to 32 times
stat.st_blksize or BUFSIZ
is good */
int new_size = 16 * 4096;
setvbuf(fp, NULL, _IOFBF, new_size);I can't remember how many times I've suggested that people use the optional pre-sizing parameter to Java's StringBuffer class constructor in code reviews. Many times the size of the final string is known absolutely, or at least close enough to pre-allocate. IIRC, the default initial size is 16 bytes which is absurdly low.
The default size is in fact optimally sized for "normal" Java applications, which frequently do very little with string buffers, or use them for small tokens or numbers.
Source: mailing list discussion with one of the Sun devs.
Source: mailing list discussion with one of the Sun devs.
I completely agree on the buffer size statement, but at the same time, find it interesting that you would use StringBuffer enough to make that comment. I personally haven’t found any compelling use cases for it since Java 1.5 introduced StringBuilder. Would you like to share some ways you find StringBuffer useful?
You shouldn't use StringBuffer.
The reason it is slow is because it is "thread safe". 99% of the time, you don't really want that (especially in cases where you know how big the string will be at the end of everything).
StringBuilder should be preferred for pretty much everything.
The reason it is slow is because it is "thread safe". 99% of the time, you don't really want that (especially in cases where you know how big the string will be at the end of everything).
StringBuilder should be preferred for pretty much everything.
While I agree with the conclusion to strongly prefer StringBuilder, in most cases the difference is zero: The JVM will prove no other thread can access the object, so it remooves the locks
I'm not sure if I agree about most cases. It is fairly trivial to come up with code that goes down a sad path (you just need to defeat the Escape analysis). That happens all the time.
The JVM's Escape analysis isn't perfect.
The JVM's Escape analysis isn't perfect.
It's a shame there's no way to globally set the default StringBuffer size. I bet if you optimize it just by dumb binary search, on your average project you could save on both runtime and memory usage.
That indeed does work great. Potentially at the expense of trashing the heap.
I once wrote my own JSON parser in Java. Mostly because I couldn't abide by how hard serialization was with the popular libs. I just wanted JSON to HashMap and back.
Then I got nutty trying to hyperoptimize it. Stuff like resizing methods so that JIT inlining could work.
At the time Boon was the benchmark leader. Its trick was preallocating a huge buffer per parse (and as well as taking shortcuts with the spec, accepting invalid JSON). Which I deemed wasteful. I had an HTTP server and I wanted each client connection to be lighter vs heavier. And for some reason I was committed to "stream processing" (incremental pull parser).
I could never match Boon's performance. Close. But never quite catching it. Which offended me in some way. More simple should always win, right?
Some time later I forced myself to crawl out of the rabbit hole. I backed out all my goofy optimizations. Opting for the most simple implementation I could conceive. Given my use case, my parser was more than good enough.
It's fun to tinker now and again. If only to relearn the "don't preoptimize" lesson.
I once wrote my own JSON parser in Java. Mostly because I couldn't abide by how hard serialization was with the popular libs. I just wanted JSON to HashMap and back.
Then I got nutty trying to hyperoptimize it. Stuff like resizing methods so that JIT inlining could work.
At the time Boon was the benchmark leader. Its trick was preallocating a huge buffer per parse (and as well as taking shortcuts with the spec, accepting invalid JSON). Which I deemed wasteful. I had an HTTP server and I wanted each client connection to be lighter vs heavier. And for some reason I was committed to "stream processing" (incremental pull parser).
I could never match Boon's performance. Close. But never quite catching it. Which offended me in some way. More simple should always win, right?
Some time later I forced myself to crawl out of the rabbit hole. I backed out all my goofy optimizations. Opting for the most simple implementation I could conceive. Given my use case, my parser was more than good enough.
It's fun to tinker now and again. If only to relearn the "don't preoptimize" lesson.
Is it 64K that made the difference on your system? From benchmarking on Linux few years ago the gains after 16k buffer were in the noise.
Something like decade ago it did at least in one system.
Nowadays anywhere between 512KB and 1MB on my computers does make huge difference as opposed to going with default buffer sizes which are usually 4KB - 16KB.
Switching to kernel has become much more expensive thanks to recent security bugs, though
Someone else was complaining about this library using 50k of extra space for some operations. I wonder what the benchmark would look like if you used that for buffer.
If much of your I/O is FP, however, that will still be a significant bottleneck. FP <-> ASCII is inherently slow.
Unfortunately performance claims appear to be bogus.
1. ospan, performance claims seem to be based on, doesn't do any bound checks, so you can easily get buffer overflow.
2. fast_io generates a whopping 50kB of static data just to format an integer.
So if these benchmark results are correct (I was not able to verify because the author hasn't provided the benchmark source):
> format_int 7867424 ns 7866027 ns 89 items_per_second=127.129M/s
> fast_io_ospan_res 6871917 ns 6870708 ns 102 items_per_second=145.545M/s
fast_io gives 15% perf improvement by replacing a safe format_int API from https://github.com/fmtlib/fmt with a similar but unsafe one + 50kB of extra data. Adding safety will likely bring perf down which the last line seems to confirm:
> fast_io_concat 7967591 ns 7966162 ns 88 items_per_second=125.531M/s
This shows that fast_io is slightly slower than the equivalent {fmt} code. Again this is from the fast_io's benchmark results that I hasn't been able to reproduce.
50kB may not seem like much but for comparison, after a recent binary size optimization, the whole {fmt} library is around 57kB when compiled with `-Os -flto`: http://www.zverovich.net/2020/05/21/reducing-library-size.ht...
The floating-point benchmark results are even less meaningful. They appear to be based on a benchmark that I wrote to test the worst case Grisu (https://www.cs.tufts.edu/~nr/cs257/archive/florian-loitsch/p...) performance on unrealistic random data with maximum digit count. fast_io compares it to Ryu (https://dl.acm.org/doi/pdf/10.1145/3192366.3192369) where maximum digit count is actually the best case and the performance degrades as the number of digits goes down. A meaningful thing to do would be to use Milo Yip's benchmark instead: https://github.com/miloyip/dtoa-benchmark
1. ospan, performance claims seem to be based on, doesn't do any bound checks, so you can easily get buffer overflow.
2. fast_io generates a whopping 50kB of static data just to format an integer.
So if these benchmark results are correct (I was not able to verify because the author hasn't provided the benchmark source):
> format_int 7867424 ns 7866027 ns 89 items_per_second=127.129M/s
> fast_io_ospan_res 6871917 ns 6870708 ns 102 items_per_second=145.545M/s
fast_io gives 15% perf improvement by replacing a safe format_int API from https://github.com/fmtlib/fmt with a similar but unsafe one + 50kB of extra data. Adding safety will likely bring perf down which the last line seems to confirm:
> fast_io_concat 7967591 ns 7966162 ns 88 items_per_second=125.531M/s
This shows that fast_io is slightly slower than the equivalent {fmt} code. Again this is from the fast_io's benchmark results that I hasn't been able to reproduce.
50kB may not seem like much but for comparison, after a recent binary size optimization, the whole {fmt} library is around 57kB when compiled with `-Os -flto`: http://www.zverovich.net/2020/05/21/reducing-library-size.ht...
The floating-point benchmark results are even less meaningful. They appear to be based on a benchmark that I wrote to test the worst case Grisu (https://www.cs.tufts.edu/~nr/cs257/archive/florian-loitsch/p...) performance on unrealistic random data with maximum digit count. fast_io compares it to Ryu (https://dl.acm.org/doi/pdf/10.1145/3192366.3192369) where maximum digit count is actually the best case and the performance degrades as the number of digits goes down. A meaningful thing to do would be to use Milo Yip's benchmark instead: https://github.com/miloyip/dtoa-benchmark
The author has made a rebuttal to this comment:
https://github.com/expnkx/fast_io/commit/8cf7497593eb185bba1...
The new benchmark is even less meaningful because
1. It now constructs unnecessary `std::string` penalizing `format_int`:
value+=fmt::format_int(i).str().size();
2. The input is consecutive numbers which makes branches well predicted which is not realistic but beneficial for fast_io integer formatter which has a lot of branches.
The precomputed table you can find in include/fast_io_core_impl/integers/jiaendu/table_gen.h
1. It now constructs unnecessary `std::string` penalizing `format_int`:
value+=fmt::format_int(i).str().size();
2. The input is consecutive numbers which makes branches well predicted which is not realistic but beneficial for fast_io integer formatter which has a lot of branches.
The precomputed table you can find in include/fast_io_core_impl/integers/jiaendu/table_gen.h
> 1. It now constructs unnecessary `std::string` penalizing `format_int`:
> value+=fmt::format_int(i).str().size();
Author appears to be doing the equivalent in the other benchmark:
value+=fast_io::to<std::string>(i).size();
Maybe you could argue that in both cases it would be better not to measure time spent creating and destroying strings, but I don't see how the two benchmarks are not comparable.
Author appears to be doing the equivalent in the other benchmark:
value+=fast_io::to<std::string>(i).size();
Maybe you could argue that in both cases it would be better not to measure time spent creating and destroying strings, but I don't see how the two benchmarks are not comparable.
If std::string construction was the goal then sure, but that's not what the original benchmark that I've written was about. The goal was to find the fastest way to format an integer with or without std::string construction: http://www.zverovich.net/2013/09/07/integer-to-string-conver.... Those methods that construct a string but not required to are explicitly marked. The OP made some claims based on the method that doesn't construct std::string (which is one of the reasons it's rather fast) and then when I pointed out that it's unsafe switched to a much slower one but at the same time unnecessarily penalized other methods. It seems like moving a goalpost just to prove that your method is the fastest (which might be true but we don't know because of a poor methodology).
I wonder why not provide source for benchmarks?
Source is here:
https://github.com/expnkx/fast_io/tree/master/benchmarks/001...
Including video: https://www.youtube.com/watch?v=avyIFLuFUQs
Including video: https://www.youtube.com/watch?v=avyIFLuFUQs
Just keep in mind that stdio is really slow, even its fast paths.
For example, here's a trivial sscanf() rewrite for a log parsing case that achieves 300x speed-up - https://gist.github.com/apankrat/20776d68d1d97bca12576a6e204... - and that's with a completely unoptimized code.
For example, here's a trivial sscanf() rewrite for a log parsing case that achieves 300x speed-up - https://gist.github.com/apankrat/20776d68d1d97bca12576a6e204... - and that's with a completely unoptimized code.
> For example, here's a trivial sscanf() rewrite
"sscanf rewrite" implies you wrote a general purpose replacement. This just parses a fixed format. That is not an sscanf rewrite.
The way you phrased it sounds like they could drop it into an existing libc and do better.
"sscanf rewrite" implies you wrote a general purpose replacement. This just parses a fixed format. That is not an sscanf rewrite.
The way you phrased it sounds like they could drop it into an existing libc and do better.
Don't get carried away with misquoting or you end up with a strawman.
It's "sscanf() rewrite for a log parsing case", not "sscanf rewrite".
It's "sscanf() rewrite for a log parsing case", not "sscanf rewrite".
I didn't misquote anything.
There are other word choices than 'rewrite' that describe that implementation without confusing the context.
For example, "log parsing without using sscanf()", or "sscanf()-like parsing using fixed formats".
Saves the need to lookup the actual code in disbelief.
For example, "log parsing without using sscanf()", or "sscanf()-like parsing using fixed formats".
Saves the need to lookup the actual code in disbelief.
It's not misquoting. It's poorly chosen words that were misleading. I too thought you meant you either replaced sscanf with a faster equivalent or used sscanf to beat something else.
Instead write "I replaced sscanf with single use code", but then the speed improvement is not that interesting.
Instead write "I replaced sscanf with single use code", but then the speed improvement is not that interesting.
Disagree that it's not interesting. It's just that the message is "sscanf is not necessarily the best tool for hot paths", not "sscanf is currently poorly written".
A more useful paraphrase would be "sscanf() is slow. Here's some code that doesn't use it and it's much faster."
so the follow-up question (not necessarily to you but to the crowd) is: why is stdio so slow?
Because it's doing something else.
Saying stdio is slow isn't the same thing as saying "they should just do what these other guys are doing" -- they can't do that without breaking everyone who uses stdio, and a lot of people use stdio because being slow isn't a problem they need to solve.
Meanwhile, when being slow is a problem you need to solve, just because there are undoubtedly some quick wins in just about every stdio implementation doesn't mean there would be much value chasing them. Let's say for every 20% improvement in stdio you can find there, you could get a 20x improvement in your application some other way.
And there are a lot of other ways: More formatting and IO libraries than you can shake a stick at, so it's easy to think about the things you're paying for by using stdio that you don't need and simply find something else that doesn't have those things.
Some examples:
- Compatibility: stdio offers a lot of options for no better reason than it's always offered them. This creates a certain amount of bloat and slowdown that can't be removed without breaking some other application that expects this behaviour.
- Edge cases: More on that point, stdio is expected to handle a lot of situations to handle the application might never get into, but they have to test for anyway. Other libraries don't.
- Locking: FILE* needs to be locked for concurrent access to get things like the file position or to update the buffer. Many implementations have an _unlocked() implementation of stdio operations that skip the lock, but the only way to know if they're safe is to know there's no part of the application (or a library) that could be using that stream in another thread. It also nearly doubled the number of API calls stdio offers.
- Tuneables with bad defaults: Most people don't ever use setvbuf because choosing a good value is hard. Pick a value too big, and you'll see massive delays with data coming out, and too small and you waste time on syscalls. If people do use this, they choose something like line buffering (usually for logfiles) which means you're doing a syscall per line.
- Impedance mismatch: fwrite() might look* like write() but it doesn't look anything like writev() or sendmmsg(); Similarly fread() versus recvmmsg() or readv(). stdio would need a bunch of new API calls (like the _unlocked() calls) just to keep up.
- Abstraction in implementation: stdio probably uses malloc which is also slow, but taking an allocator as an argument would require a whole new API. Meanwhile, it is trivial to stack-allocate a buffer for writing your log-line and flush it out with a single write().
There are others.
I like to say that writing fast software is 99% doing only what you need to, and being good at writing fast software requires being brutally narrow about that "need".
Saying stdio is slow isn't the same thing as saying "they should just do what these other guys are doing" -- they can't do that without breaking everyone who uses stdio, and a lot of people use stdio because being slow isn't a problem they need to solve.
Meanwhile, when being slow is a problem you need to solve, just because there are undoubtedly some quick wins in just about every stdio implementation doesn't mean there would be much value chasing them. Let's say for every 20% improvement in stdio you can find there, you could get a 20x improvement in your application some other way.
And there are a lot of other ways: More formatting and IO libraries than you can shake a stick at, so it's easy to think about the things you're paying for by using stdio that you don't need and simply find something else that doesn't have those things.
Some examples:
- Compatibility: stdio offers a lot of options for no better reason than it's always offered them. This creates a certain amount of bloat and slowdown that can't be removed without breaking some other application that expects this behaviour.
- Edge cases: More on that point, stdio is expected to handle a lot of situations to handle the application might never get into, but they have to test for anyway. Other libraries don't.
- Locking: FILE* needs to be locked for concurrent access to get things like the file position or to update the buffer. Many implementations have an _unlocked() implementation of stdio operations that skip the lock, but the only way to know if they're safe is to know there's no part of the application (or a library) that could be using that stream in another thread. It also nearly doubled the number of API calls stdio offers.
- Tuneables with bad defaults: Most people don't ever use setvbuf because choosing a good value is hard. Pick a value too big, and you'll see massive delays with data coming out, and too small and you waste time on syscalls. If people do use this, they choose something like line buffering (usually for logfiles) which means you're doing a syscall per line.
- Impedance mismatch: fwrite() might look* like write() but it doesn't look anything like writev() or sendmmsg(); Similarly fread() versus recvmmsg() or readv(). stdio would need a bunch of new API calls (like the _unlocked() calls) just to keep up.
- Abstraction in implementation: stdio probably uses malloc which is also slow, but taking an allocator as an argument would require a whole new API. Meanwhile, it is trivial to stack-allocate a buffer for writing your log-line and flush it out with a single write().
There are others.
I like to say that writing fast software is 99% doing only what you need to, and being good at writing fast software requires being brutally narrow about that "need".
> Meanwhile, it is trivial to stack-allocate a buffer for writing your log-line and flush it out with a single write().
Indeed! Writing freestanding C while usiing Linux system calls directly was a great experience. I had to rewrite a lot of functionality but everything was explicit and there was no need to worry about old C library stuff like FILE pointers, I/O buffers or thread-local errno.
Even the glibc system call stubs can be a lot more complex than they seem. Many of them have a lot of hidden functionality, something to do with system call cancellation. The kernel's own nolibc.h provides a much better way to make system calls:
https://github.com/torvalds/linux/blob/master/tools/include/...
Indeed! Writing freestanding C while usiing Linux system calls directly was a great experience. I had to rewrite a lot of functionality but everything was explicit and there was no need to worry about old C library stuff like FILE pointers, I/O buffers or thread-local errno.
Even the glibc system call stubs can be a lot more complex than they seem. Many of them have a lot of hidden functionality, something to do with system call cancellation. The kernel's own nolibc.h provides a much better way to make system calls:
https://github.com/torvalds/linux/blob/master/tools/include/...
If somebody finds a 20% improvement in stdio, every program in the world using stdio benefits from that improvement.
You're right in principle, but I think you're overestimating both the cost of discovery and the value of a 20% improvement: A 20% improvement on a microbenchmark (say float to string conversion) when only 0.01% of your application run-time was spent converting floats to strings nets you fuck all. It might also take weeks-to-months to identify some improvement and get it integrated into each libc implementation.
It's so much easier, and the gains are so much bigger to just change the application, and given people's dependency on stdio is usually quite low, that's what most people do.
It's so much easier, and the gains are so much bigger to just change the application, and given people's dependency on stdio is usually quite low, that's what most people do.
My experience is primarily in scientific software which has more string<->number conversions than the norm, so others' mileage may vary, but I can confirm that a lot of widely used C and C++ programs are significantly slowed down by their use of standard library I/O functions. Often written by domain experts with more than a decade of experience, not noobs.
Yes, once a particular programmer knows about the problem, they can solve it pretty quickly by switching to faster and narrower functions. But this is happening one programmer at a time. Practically everyone is still learning the wrong default, and switching away only after they've noticed they've been burned by it, and scientific programmers at least are being burned by enough other things that there's no guarantee that they'll ever notice this detail, even if they're generally very competent. (We're initially trained to not worry much about this sort of constant factor, after all.)
The lowest-hanging fruit I see is in the compiler enhancement domain. They already e.g. inspect printf-family format strings, redirect the containing function call to fputs/memcpy when the string contains no format placeholders, and when there are format placeholders a warning is printed when the number of additional arguments does not match the number of format placeholders. If we could count on snprintf(buf, bufsize, "count: %d", k) being compiled down to little more than a 7- or 8-byte memcpy followed by a call to a dedicated integer-to-string function, I think the associated gain would be far more than "0.01%".
Yes, once a particular programmer knows about the problem, they can solve it pretty quickly by switching to faster and narrower functions. But this is happening one programmer at a time. Practically everyone is still learning the wrong default, and switching away only after they've noticed they've been burned by it, and scientific programmers at least are being burned by enough other things that there's no guarantee that they'll ever notice this detail, even if they're generally very competent. (We're initially trained to not worry much about this sort of constant factor, after all.)
The lowest-hanging fruit I see is in the compiler enhancement domain. They already e.g. inspect printf-family format strings, redirect the containing function call to fputs/memcpy when the string contains no format placeholders, and when there are format placeholders a warning is printed when the number of additional arguments does not match the number of format placeholders. If we could count on snprintf(buf, bufsize, "count: %d", k) being compiled down to little more than a 7- or 8-byte memcpy followed by a call to a dedicated integer-to-string function, I think the associated gain would be far more than "0.01%".
> Practically everyone is still learning the wrong default
I think this is a much deeper problem than stdio.
I think this is a much deeper problem than stdio.
> A 20% improvement on a microbenchmark (say float to string conversion) when only 0.01% of your application run-time was spent converting floats to strings nets you fuck all.
Your logic is incorrect. A typical REST API might make millions of float<->string conversions in one request. Making it twice as fast might mean shaving many milliseconds from your median response time, which is a huge deal.
Your logic is incorrect. A typical REST API might make millions of float<->string conversions in one request. Making it twice as fast might mean shaving many milliseconds from your median response time, which is a huge deal.
If stdio was 100% of all programs in the world, they'd be a 20% improvement. In reality, stdio takes more like 1% of any normal program so even 20% improvement will be barely noticeable.
Amdahl's Law does a nice job of expressing this idea: https://en.wikipedia.org/wiki/Amdahl%27s_law
In many cases stdio takes like 90% overall, because it's so slow. You have to compare IO bound to CPU bound. CPU bound problems are extremely rare, but they do exist.
If stdio takes 1% of all computer time and computers take 1% of the world's electricity to run, a 20% improvement in stdio time saves many tons of CO2.
You're assuming most programs run start to finish then the machine shuts down. That's probably <1% of all machines. Most machines run on a loop. So when they don't do stdio either they do some other useful computation (e.g. if a game engine is not stalled on stdio, it can process a few more frames per second) or they run an idle process which consumes less energy but very far away from 0. So, no even that logic doesn't work here. Saving 20% stdio cycles will probably not even have measurable effect on global enegy consumption.
Because it's an abstraction on top of an abstraction on top of an abstraction. You need to leak parts of the lower layers to higher levels in order to get the speed back (and allow zero-copy). There are probably some non-leaky optimizations that can be done as well, although that's been going on for decades.
Another biggie is floating point. Converting binary floating point to decimal floating point and vice versa is VERY VERY VERY SLOW, difficult to get right, and difficult to predict results. Unfortunately, ieee754 decimal float just isn't getting any adoption, so we're stuck doing these costly conversions every time we deal with text formats or big float implementations.
Actually, scanf and printf are just in general slow because of their general nature. They need to handle a TON of options and get really bloated and complicated as a result.
Another biggie is floating point. Converting binary floating point to decimal floating point and vice versa is VERY VERY VERY SLOW, difficult to get right, and difficult to predict results. Unfortunately, ieee754 decimal float just isn't getting any adoption, so we're stuck doing these costly conversions every time we deal with text formats or big float implementations.
Actually, scanf and printf are just in general slow because of their general nature. They need to handle a TON of options and get really bloated and complicated as a result.
I think you are over counting abstractions. printf copies to a buffer and eventually calls write(2).
On Windows there is an additional layer: CRT implements unix-style integer file descriptors (io.h) before calling WriteFile.
On Windows there is an additional layer: CRT implements unix-style integer file descriptors (io.h) before calling WriteFile.
It's usually printf() -> fwrite() -> write() -> syscall. You need the fwrite (or something equivalent) in there for buffering and concurrency, otherwise multithreaded printfs would stomp on each other.
Usually you go through vfprintf, which is the “most generic” function, and that will grab the file’s lock.
> Another biggie is floating point. Converting binary floating point to decimal floating point and vice versa is VERY VERY VERY SLOW, difficult to get right
Never forget the cluster bug in PHP because the developers decided they needed to reinvent the wheel[1].
[1] https://www.exploringbinary.com/php-hangs-on-numeric-value-2...
Never forget the cluster bug in PHP because the developers decided they needed to reinvent the wheel[1].
[1] https://www.exploringbinary.com/php-hangs-on-numeric-value-2...
Java had a variant of that bug too: https://www.exploringbinary.com/java-hangs-when-converting-2...
It sits in a loop flip-flopping between 2 representations, each time deciding the other one is better.
This turned into a DOS, as some web servers used floating point when decoding time zones from the http request header. So if your server has e.g. 20 http acceptor threads, someone could send 20 requests with an insane time zone, each sending 1 thread in an infinite loop.
It sits in a loop flip-flopping between 2 representations, each time deciding the other one is better.
This turned into a DOS, as some web servers used floating point when decoding time zones from the http request header. So if your server has e.g. 20 http acceptor threads, someone could send 20 requests with an insane time zone, each sending 1 thread in an infinite loop.
[deleted]
Can you explain what you mean?
Because I read the bug report and it seems to be some intricacy about loading variables into a register solved by using volatile.
Meaning it's a complicated implementation detail of the CPU, and does not seem to be because they reinvented the wheel.
Plus is there some standardized library they should have been using?
Because I read the bug report and it seems to be some intricacy about loading variables into a register solved by using volatile.
Meaning it's a complicated implementation detail of the CPU, and does not seem to be because they reinvented the wheel.
Plus is there some standardized library they should have been using?
> Converting binary floating point to decimal floating point and vice versa is VERY VERY VERY SLOW
The fastest dtoa() algorithms are on the order of 50ns or less: https://erthink.github.io/dtoa-benchmark/#results
There are probably a lot of implementations lagging behind the state of the art though...
The fastest dtoa() algorithms are on the order of 50ns or less: https://erthink.github.io/dtoa-benchmark/#results
There are probably a lot of implementations lagging behind the state of the art though...
The problem is if it's still behaving exactly the same for certain edge cases.
I don't know about stdio, but in Java there are a bunch of float operations in the standardlibrary which can be easily implemented much faster and simpler, but then misbehave in that one edge cases most people don't care about.
I don't know about stdio, but in Java there are a bunch of float operations in the standardlibrary which can be easily implemented much faster and simpler, but then misbehave in that one edge cases most people don't care about.
> The problem is if it's still behaving exactly the same for certain edge cases.
It is behaving correctly for all edge cases. That’s the benchmark you compare floating-point to decimal conversion—your algorithm is no good if it doesn’t handle all cases.
The start of the art back in the 1990s the algorithm used in dtoa, written by David M. Gay and included in Netlib, taken from a paper by Steele (I think). Since then, people have chipped away at the problem, making it faster. A few new algorithms have appeared, including Grisu and Ryu.
Something you’ll see in these algorithms is some “fast path” which uses ordinary double arithmetic, and then logic to detect when this gives the wrong answer and fall back to some slower path. The slower path may use arbitrary precision arithmetic or something else (Ryu avoids bignums!). These new algorithms generally hit the slow path very infrequently, which is why you sometimes see benchmarks with numbers picked to trigger the slow path.
The algorithms have associated papers proving their correctness for all inputs. You are free to read them.
https://www.cs.tufts.edu/~nr/cs257/archive/florian-loitsch/p...
https://dl.acm.org/doi/10.1145/3192366.3192369
It is behaving correctly for all edge cases. That’s the benchmark you compare floating-point to decimal conversion—your algorithm is no good if it doesn’t handle all cases.
The start of the art back in the 1990s the algorithm used in dtoa, written by David M. Gay and included in Netlib, taken from a paper by Steele (I think). Since then, people have chipped away at the problem, making it faster. A few new algorithms have appeared, including Grisu and Ryu.
Something you’ll see in these algorithms is some “fast path” which uses ordinary double arithmetic, and then logic to detect when this gives the wrong answer and fall back to some slower path. The slower path may use arbitrary precision arithmetic or something else (Ryu avoids bignums!). These new algorithms generally hit the slow path very infrequently, which is why you sometimes see benchmarks with numbers picked to trigger the slow path.
The algorithms have associated papers proving their correctness for all inputs. You are free to read them.
https://www.cs.tufts.edu/~nr/cs257/archive/florian-loitsch/p...
https://dl.acm.org/doi/10.1145/3192366.3192369
> Another biggie is floating point. Converting binary floating point to decimal floating point and vice versa is VERY VERY VERY SLOW, difficult to get right, and difficult to predict results.
I haven't read deeply into IEEE 754-2008 for decimal floating points, but it seems like it should be pretty fast (relative to system calls) to convert binary to decimal because 10 has a factor of 2.
> Unfortunately, ieee754 decimal float just isn't getting any adoption, so we're stuck doing these costly conversions every time we deal with text formats or big float implementations.
Is there any reason big float implementations should use decimal rather than binary? It seems like it is very straightforward to make a binary big float, and do operations on it. In fact, IEEE 754-2008 specifies interchange formats for all binary floats of bit lengths >= 128 where length is a multiple of 32.
I haven't read deeply into IEEE 754-2008 for decimal floating points, but it seems like it should be pretty fast (relative to system calls) to convert binary to decimal because 10 has a factor of 2.
> Unfortunately, ieee754 decimal float just isn't getting any adoption, so we're stuck doing these costly conversions every time we deal with text formats or big float implementations.
Is there any reason big float implementations should use decimal rather than binary? It seems like it is very straightforward to make a binary big float, and do operations on it. In fact, IEEE 754-2008 specifies interchange formats for all binary floats of bit lengths >= 128 where length is a multiple of 32.
“it seems like it should be pretty fast (relative to system calls) to convert binary to decimal because 10 has a factor of 2“
The problem isn’t to find a decimal representation, it’s to find one that round-trips and, among those, the ‘best’ one, and do that fast.
With best, people typically mean that the IEEE float closest to ⅒ prints as “0.1”, the one closest to 42/100 as “0.42”, etc. In general, you want to produce all “0.x”, “0.xx”, “0.xxx”, etc. until you run out of IEEE numbers in (0,1).
It took surprisingly long for _any_ implementation to get there (I think it was Steele, around 1980, published in 1990)
http://www.ryanjuckett.com/programming/printing-floating-poi... has a good introduction to the problem, insofar as I am qualified to judge that.
The problem isn’t to find a decimal representation, it’s to find one that round-trips and, among those, the ‘best’ one, and do that fast.
With best, people typically mean that the IEEE float closest to ⅒ prints as “0.1”, the one closest to 42/100 as “0.42”, etc. In general, you want to produce all “0.x”, “0.xx”, “0.xxx”, etc. until you run out of IEEE numbers in (0,1).
It took surprisingly long for _any_ implementation to get there (I think it was Steele, around 1980, published in 1990)
http://www.ryanjuckett.com/programming/printing-floating-poi... has a good introduction to the problem, insofar as I am qualified to judge that.
I definitely agree that that is a much harder problem than faithfully converting a binary float to a decimal one.
But doing it correctly shouldn't be too slow. Some rough psuedo-code for what I would do without looking at any references:
But doing it correctly shouldn't be too slow. Some rough psuedo-code for what I would do without looking at any references:
- convert the binary float one higher and lower than the given binary float.
- find the point at which they diverge
- Generate the given value rounded to that digit + 1
That probably is off by a digit in some case, but it should be reasonably fast, and is pretty simple.Recall that the input to your algorithm gives you a binary mantissa and a binary exponent, and you need to convert this to a decimal.
The Ryu paper outlines the process as follows:
* Extract e and m from the float, and normalize the next-smallest and next-largest numbers to handle subnormal and largest/smallest mantissa for a given exponent cases.
* Convert the adjusted mantissas to base-10 mantissas by multiplying by a power of 2, or by multiplying by a power of 1/5.
* Print out all of the common prefix of the smallest and largest values.
Except it turns out that doing the last two steps naively is actually quite slow: you need bignum support to do step 2 correctly, and bignum div/mod to do step 3 correctly.
What Ryu does to speed it up is it uses a lookup table to work out a bound on the prefix size to skip several iterations of "find the common prefix", proves that everything can be done with just 128-bit (for doubles)/64-bit (for floats) math, and then precomputes the necessary multiplications of 2^a/5^b via a lookup table.
The Ryu paper outlines the process as follows:
* Extract e and m from the float, and normalize the next-smallest and next-largest numbers to handle subnormal and largest/smallest mantissa for a given exponent cases.
* Convert the adjusted mantissas to base-10 mantissas by multiplying by a power of 2, or by multiplying by a power of 1/5.
* Print out all of the common prefix of the smallest and largest values.
Except it turns out that doing the last two steps naively is actually quite slow: you need bignum support to do step 2 correctly, and bignum div/mod to do step 3 correctly.
What Ryu does to speed it up is it uses a lookup table to work out a bound on the prefix size to skip several iterations of "find the common prefix", proves that everything can be done with just 128-bit (for doubles)/64-bit (for floats) math, and then precomputes the necessary multiplications of 2^a/5^b via a lookup table.
> Except it turns out that doing the last two steps naively is actually quite slow: you need bignum support to do step 2 correctly, and bignum div/mod to do step 3 correctly.
I don't doubt that my naive solution would be slow relative (probably at least x4) to an ideal one, but I think it would be fast relative to system calls.
My naive implementation would not use a bignum for step two. I would just use a use a integer with size large enough to fit maximum mantissa * 5^log_2(maximum mantissa) which can be rounded up to a number with 4 x the number of bits as the binary mantissa (1 for maximum mantissa and 3 for the multiplication with 5). So a 128 bit number is definitely enough for a 32 bit float.
And with that you can still use a normal modulus and divide for step 3.
> What Ryu does to speed it up is it uses a lookup table to work out a bound on the prefix size to skip several iterations of "find the common prefix", proves that everything can be done with just 128-bit (for doubles)/64-bit (for floats) math, and then precomputes the necessary multiplications of 2^a/5^b via a lookup table.
It definitely seems like there are some optimizations that I had not considered. I will probably give a close look into the algorithm at some point.
It does also seem like the given algorithm roughly follows my psuedo-code though. My psuedo-code lumped your steps 1 and 2 into step 1 and then would include an extra digit rather than stop at the common prefix.
This means my algorithm would show 1.2 for the float closest to 1.2 (0x3F99999A) whereas that algorithm would show 1. Its possible (and probable) I am misunderstanding your simplification of step 3 from the Ryu paper.
I don't doubt that my naive solution would be slow relative (probably at least x4) to an ideal one, but I think it would be fast relative to system calls.
My naive implementation would not use a bignum for step two. I would just use a use a integer with size large enough to fit maximum mantissa * 5^log_2(maximum mantissa) which can be rounded up to a number with 4 x the number of bits as the binary mantissa (1 for maximum mantissa and 3 for the multiplication with 5). So a 128 bit number is definitely enough for a 32 bit float.
And with that you can still use a normal modulus and divide for step 3.
> What Ryu does to speed it up is it uses a lookup table to work out a bound on the prefix size to skip several iterations of "find the common prefix", proves that everything can be done with just 128-bit (for doubles)/64-bit (for floats) math, and then precomputes the necessary multiplications of 2^a/5^b via a lookup table.
It definitely seems like there are some optimizations that I had not considered. I will probably give a close look into the algorithm at some point.
It does also seem like the given algorithm roughly follows my psuedo-code though. My psuedo-code lumped your steps 1 and 2 into step 1 and then would include an extra digit rather than stop at the common prefix.
This means my algorithm would show 1.2 for the float closest to 1.2 (0x3F99999A) whereas that algorithm would show 1. Its possible (and probable) I am misunderstanding your simplification of step 3 from the Ryu paper.
> I would just use a use a integer with size large enough to fit maximum mantissa * 5^log_2(maximum mantissa)
It should be mantissa * 5^O(maximum binary exponent), not O(log mantissa).
It should be mantissa * 5^O(maximum binary exponent), not O(log mantissa).
The algorithms in question are off by a digit in no cases at all, they are correct for all inputs.
If you actually start putting your pseudocode into real code you’ll see how quickly it can go wrong, if you want to correctly convert all inputs. For example, once you get past 10^22, you can no longer do a simple division or multiplication to get the leading digit, because 10^22 cannot be represented as a double. For numbers 10^22 or below, you can just use ordinary division or multiplication and rely on the fact that this will be rounded correctly.
It’s a fun exercise to write an exact converter that works in the range -10^22..+10^22, though. It doesn’t require much code.
If you actually start putting your pseudocode into real code you’ll see how quickly it can go wrong, if you want to correctly convert all inputs. For example, once you get past 10^22, you can no longer do a simple division or multiplication to get the leading digit, because 10^22 cannot be represented as a double. For numbers 10^22 or below, you can just use ordinary division or multiplication and rely on the fact that this will be rounded correctly.
It’s a fun exercise to write an exact converter that works in the range -10^22..+10^22, though. It doesn’t require much code.
> haven't read deeply into IEEE 754-2008 for decimal floating points, but it seems like it should be pretty fast (relative to system calls) to convert binary to decimal because 10 has a factor of 2.
It could be fast, but legacy has doomed us all to a "canonical" conversion of sorts, where any other conversion algorithm will likely yield off-by-a-tiny-amount differences in the binary format (like 1.200000000031 instead of 1.2). There are in fact fairly simple conversions that can be done, but they yield results that are incompatible with printf.
> Is there any reason big float implementations should use decimal rather than binary?
At the end of the day, we work in decimal. So every binary result we calculate has to be converted to its decimal approximation (the meaning of which is subject to convention). Rounding is also an issue, because you want to keep your number of significant digits within reason to avoid false precision errors. Doing all of this in a different base that can't be 1:1 converted adds a whole slew of bug opportunities and corner cases.
It could be fast, but legacy has doomed us all to a "canonical" conversion of sorts, where any other conversion algorithm will likely yield off-by-a-tiny-amount differences in the binary format (like 1.200000000031 instead of 1.2). There are in fact fairly simple conversions that can be done, but they yield results that are incompatible with printf.
> Is there any reason big float implementations should use decimal rather than binary?
At the end of the day, we work in decimal. So every binary result we calculate has to be converted to its decimal approximation (the meaning of which is subject to convention). Rounding is also an issue, because you want to keep your number of significant digits within reason to avoid false precision errors. Doing all of this in a different base that can't be 1:1 converted adds a whole slew of bug opportunities and corner cases.
> At the end of the day, we work in decimal.
What? No.
I have spent much of my life working with floating point numbers and never ever had to resort to anything decimal for serious purposes (that is, except for some occasional printing of a number for debugging).
What? No.
I have spent much of my life working with floating point numbers and never ever had to resort to anything decimal for serious purposes (that is, except for some occasional printing of a number for debugging).
Are you telling me that you do math with a pen and paper in binary? everything is decimal. Computers aren't, but almost everyone tries to paper over that fact as much as possible.
> Computers aren't, but almost everyone tries to paper over that fact as much as possible.
Do you realize that many entire fields of scientific computing (i.e., computer vision, fluid mechanics, solid mechanics, signal processing, numerical partial differential equations, computational chemistry, climate modeling, and dozens of other things) couldn't care less whether the inner representation of floating point numbers is decimal or binary? If binary can be made just a little faster, so be it.
Do you realize that many entire fields of scientific computing (i.e., computer vision, fluid mechanics, solid mechanics, signal processing, numerical partial differential equations, computational chemistry, climate modeling, and dozens of other things) couldn't care less whether the inner representation of floating point numbers is decimal or binary? If binary can be made just a little faster, so be it.
I'm not saying anyone thinks we should have computers process numbers as decimal. I'm just saying that no one thinks (like the brains internal representation) in decimal binary, that's it.
I don't think about real numbers as decimals either (for one, the decimal representation is not unique). For many people real numbers are points on a straight line.
Even if that were true (and I highly doubt it is), when you then need to transcribe those points on a line for future reference you'd do so using decimal. You wouldn't have number lines on a shopping list against items you need multiples of any more than you'd hold up different lengths of string to bar staff when ordering a round of drinks.
The reason being is because decimal is the base system people learn when growing up. It's also widely speculated that one of the origins of decimal is around the number of digits (it's not an accident I've chosen that ambiguous term) on our hands: 10 fingers and thumbs. You might mentally map that to number line when trying to assess the distance between numbers or visualise formula but ultimately these would be mental models you generate on top of decimal rather than in place of.
The reason being is because decimal is the base system people learn when growing up. It's also widely speculated that one of the origins of decimal is around the number of digits (it's not an accident I've chosen that ambiguous term) on our hands: 10 fingers and thumbs. You might mentally map that to number line when trying to assess the distance between numbers or visualise formula but ultimately these would be mental models you generate on top of decimal rather than in place of.
Prices aren’t real numbers. They aren’t even fractional.
They are integers with a fixed point, which is an entirely different domain.
They are integers with a fixed point, which is an entirely different domain.
I'm not sure why I'm replying, but let's say someone asks you to square 1.2 in your head. You think about points on a line?
Sure. The square (or more generally the product) of two segments is the area of the corresponding square (or rectangle). Given a unit measure you can also put the product as the length of a new segment built by triangle similarity.
Totally wrong.
So why do you think languages defined the floating point type "real", not inexact or approximate?
Because they are more real than base 10 rationals. Math is either calculating with exact numbers (without any floating point nor overflow, or bigrat. ie symbolic), or inexact approximations (floating point types). Nothing is decimal, but those folks counting with their fingers, or financial folks who cannot afford to work with exact numbers. Decimal is just to minimize errors on the decimal representation side, nobody else does decimal. It's either binary or exact.
Because they are more real than base 10 rationals. Math is either calculating with exact numbers (without any floating point nor overflow, or bigrat. ie symbolic), or inexact approximations (floating point types). Nothing is decimal, but those folks counting with their fingers, or financial folks who cannot afford to work with exact numbers. Decimal is just to minimize errors on the decimal representation side, nobody else does decimal. It's either binary or exact.
My slide rule isn't even digital, let alone decimal or binary.
> It could be fast, but legacy has doomed us all to a "canonical" conversion of sorts, where any other conversion algorithm will likely yield off-by-a-tiny-amount differences in the binary format (like 1.200000000031 instead of 1.2). There are in fact fairly simple conversions that can be done, but they yield results that are incompatible with printf.
I would argue that the issue here is not the binary -> decimal conversion. People expect when they write "1.2" they get that exact value, but that is not representable with binary floats. So the weird value you get is the closest value that is representable.
I definitely agree that there aren't good options to make a round trip fast and intuitive.
> At the end of the day, we work in decimal. So every binary result we calculate has to be converted to its decimal approximation (the meaning of which is subject to convention). Rounding also is an issue, because you want to keep your number of significant digits within reason to avoid false precision errors. Doing all of this in a different base that can't be 1:1 converted adds a whole slew of bug opportunities and corner cases.
If you are keeping track of significant digits, you can definitely work in binary and render the binary value to the significant decimal digits. In particular for scientific work, if you cannot handle the issues with binary floating point, you probably are not handling uncertainty well enough. The corner cases you would hit would already have been bugs, but you just wouldn't have noticed.
One particular setup I am a fan of is keeping track of an upper and lower bound for all of your numbers which allows your computations to introduce a small amount of error , but you will still have objectively true statements when converting back to decimal to read.
For example "1.2" would be converted to the range "1.001-1.010" in binary with 4 significant figs which when converted back would be the range "1.125-1.25". Its not perfect, but it steps around a lot of typical floating point problems.
I would argue that the issue here is not the binary -> decimal conversion. People expect when they write "1.2" they get that exact value, but that is not representable with binary floats. So the weird value you get is the closest value that is representable.
I definitely agree that there aren't good options to make a round trip fast and intuitive.
> At the end of the day, we work in decimal. So every binary result we calculate has to be converted to its decimal approximation (the meaning of which is subject to convention). Rounding also is an issue, because you want to keep your number of significant digits within reason to avoid false precision errors. Doing all of this in a different base that can't be 1:1 converted adds a whole slew of bug opportunities and corner cases.
If you are keeping track of significant digits, you can definitely work in binary and render the binary value to the significant decimal digits. In particular for scientific work, if you cannot handle the issues with binary floating point, you probably are not handling uncertainty well enough. The corner cases you would hit would already have been bugs, but you just wouldn't have noticed.
One particular setup I am a fan of is keeping track of an upper and lower bound for all of your numbers which allows your computations to introduce a small amount of error , but you will still have objectively true statements when converting back to decimal to read.
For example "1.2" would be converted to the range "1.001-1.010" in binary with 4 significant figs which when converted back would be the range "1.125-1.25". Its not perfect, but it steps around a lot of typical floating point problems.
> it seems like it should be pretty fast (relative to system calls) to convert binary to decimal because 10 has a factor of 2
I don’t see how this would matter?
I don’t see how this would matter?
Any binary float is exactly representable in a decimal float with enough digits. This is not true in the other direction. for example 1/5th or 0.2 is 0.001100110011... in binary.
So a simple solution for binary to decimal is to express it as a larger decimal float than you intend to show and then crop off digits if it doesn't fit in your destination format.
So a simple solution for binary to decimal is to express it as a larger decimal float than you intend to show and then crop off digits if it doesn't fit in your destination format.
They’re exceptionally complicated. No, really, take a look at the source to glibc’s vfprintf-internal.c (http://sourceware.org/git/?p=glibc.git;a=blob;f=stdio-common...) or vfcanf-internal.c (http://sourceware.org/git/?p=glibc.git;a=blob;f=stdio-common...) sometime. all the * (scanf|printf) functions are funneled through that, which means right off the bat you’re converting your string to a FILE * abstraction and marshaling all your arguments through a va_args. Then you get to the actual function itself, which needs to deal with locales and buffering and locking and positional arguments and multiple bases and memory allocation and rounding floating point and a huge amount of complexity that you might not even be aware of. And then there’s the somewhat awkward signature itself, which means it is all but required to do extra work to conform to the standard.
Wonder why... Maybe because of compatibility concerns?
this adds some helpful perspective. thanks!
You know what's even faster... write()
I was writing part of a codebase where I couldn't use the heap, so all standard libraries were not on the cards, and I ended up just using write(2, "hello\n", 6) to send the data direct to the kernel, and found the experience remarkably simple and unpainful.
I was writing part of a codebase where I couldn't use the heap, so all standard libraries were not on the cards, and I ended up just using write(2, "hello\n", 6) to send the data direct to the kernel, and found the experience remarkably simple and unpainful.
No it's not, write is wayyyy slower. I just did a test, I write "hello\n" to a file 10,000,000 times. Using fprintf() and stdio.h, it takes 0.04s on my machine. Using write(), it takes 7.6s, over 2 orders of magnitude slower.
Syscalls are extremely expensive and dominant the execution time unless you are writing large chunks of data all at once. Which is precisely why stdio.h exists, because it only occasionally flushes the buffer with write(). This can be observed when you get a segfault, often it seems like your print statements did not execute. One can use fflush() to force stdio.h to flush the buffer.
write() on the otherhand, always writes the data, well, at least to the file buffer cache. fsync() is required to ensure that the data is on disk, though I've seen systems lie about that too.
Syscalls are extremely expensive and dominant the execution time unless you are writing large chunks of data all at once. Which is precisely why stdio.h exists, because it only occasionally flushes the buffer with write(). This can be observed when you get a segfault, often it seems like your print statements did not execute. One can use fflush() to force stdio.h to flush the buffer.
write() on the otherhand, always writes the data, well, at least to the file buffer cache. fsync() is required to ensure that the data is on disk, though I've seen systems lie about that too.
It depends on what you want to do. If that's sending data into the kernel right now – like procfs or a socket – then write() is what you need. fprintf() will provide more throughput by buffering the data and sending a batched write() later, but sometimes you need to send stuff right now and be sure that it's in the kernel when you get control back.
> fsync() is required to ensure that the data is on disk, though I've seen systems lie about that too.
Virtually all SSDs lie about this to a certain extent. They'll indicate a write is complete when it's in their cache. The cache is RAM; as long as the power stays on, the write will appear to have succeeded to readers, but if the power goes goes off before the drive flushes the cache, the write will disappear. The exception is high end enterprise drives, which come with a capacitor that stores enough power to flush the cache to persistent storage in the event of power loss.
Virtually all SSDs lie about this to a certain extent. They'll indicate a write is complete when it's in their cache. The cache is RAM; as long as the power stays on, the write will appear to have succeeded to readers, but if the power goes goes off before the drive flushes the cache, the write will disappear. The exception is high end enterprise drives, which come with a capacitor that stores enough power to flush the cache to persistent storage in the event of power loss.
I imagine your compiler optimises the fprintf call into a call to fputs or puts.
fwrite, but yeah.
[deleted]
Here is my fast io "library": https://github.com/bjourne/c-examples/blob/master/libraries/... It is very (very!) limited, but also very fast. :) Think I got about 20-50x compared to scanf.
Formatting is a natural demarcation point for deferring to another thread as it can be surprisingly expensive.
You can create a workqueue where each item is a copy of the argument list and a const char* of the format string. Then your program will only take a microsecond or two to print (depending on the argument length) on the critical path.
You can create a workqueue where each item is a copy of the argument list and a const char* of the format string. Then your program will only take a microsecond or two to print (depending on the argument length) on the critical path.
Formatting is generally fast enough in practice that synchronizing with another thread would slow things down. So it is actually not a good place to offload work to other threads, since you spend a similar amount of work just sending the message.
Unless you are formatting a really large message.
Unless you are formatting a really large message.
You should time it and come back. You're wrong.
You might be right, but your response comes across as rather rude. I think you should either soften your message, or provide more details about the timing --- or both! My instinct is that for standard IO involving moderate amounts of string interpolation and number formatting in C running on Linux on modern Intel processors, klodolph is probably closer to right. But maybe you are right for some other setup? Or maybe klodolph and I are both wrong, in which case some more details would be helpful.
I gave a time in my original post: 1-2usec. Printing a handful of doubles with a cold instruction cache using the standard stdio headers takes longer than that due to all the special casing.
klodolph made some handwavey claim about synchronization overhead that are clearly false. It doesn't even make sense to me since the device being printed to will need some sort of synchronization and syscall.
Both you and klodlph are also making handwavey claims about formatting speed in Linux.
You both need to measure before you argue. I find it extremely rude to argue from a position of ignorance as both of you are doing.
"Well gee, I dunno but you're probably wrong". It's so asinine.
klodolph made some handwavey claim about synchronization overhead that are clearly false. It doesn't even make sense to me since the device being printed to will need some sort of synchronization and syscall.
Both you and klodlph are also making handwavey claims about formatting speed in Linux.
You both need to measure before you argue. I find it extremely rude to argue from a position of ignorance as both of you are doing.
"Well gee, I dunno but you're probably wrong". It's so asinine.
Sure, if you have a cold instruction cache. If stdio is not in your instruction cache, or at least in L2, then you’re not using it very much, and you won’t get much benefit from moving it to a different thread. My measurements put stdio formatting at around 100ns for the typical messages I’m printing. So, offloading it to another thread is a fairly clear loss.
Even for floating point numbers, you’ll hit the fast path with fairly high regularity. My measurements put it at around 80ns for a double, on my system, most of the time.
Even for floating point numbers, you’ll hit the fast path with fairly high regularity. My measurements put it at around 80ns for a double, on my system, most of the time.
So is IO faster or 'conversion'?
The formatting part but unfortunately it's just a clickbait: https://news.ycombinator.com/item?id=23311726
Sorry dude. You are horribly wrong.
https://github.com/expnkx/fast_io/commit/9b2b084e680b6cd2809...
Benchmark: https://github.com/expnkx/fast_io/tree/master/benchmarks/001... Binary Size: https://bitbucket.org/ejsvifq_mabmip/fast_io/src/master/benc...
Benchmark: https://github.com/expnkx/fast_io/tree/master/benchmarks/001... Binary Size: https://bitbucket.org/ejsvifq_mabmip/fast_io/src/master/benc...
[deleted]
[deleted]
* What is the core implementation idea behind this library? Why is it supposedly so much faster than stdio and fmt? There doesn't seem to be much explanation in the readme. The only thing I can see is "Locale support optional" and "Zero copy IO" but these are mixing up formatted and unformatted interfaces - it is supposed to be faster at both? Does the "zero copy IO" mean that it's unbuffered (as another comment here mentioned, that usually makes things slower not faster)?
* What does the API look like? There's no documentation whatsoever - the "documentation" heading in the readme just refers to "./doxygen/html/index.html" which doesn't exist in the repo; I can't even see a Doxyfile. Just a brief example in the readme showing reading and writing would be nice!
* What exactly is it faster at? Reading or writing? Formatted or unformatted IO? If formatted, is it just the formatting that's faster (e.g. is there also a comparison against fputs)? The benchmarks section has no detail of what code was compared except a mention of the examples/ directory, but that contains dozens of subdirectories, most of which each have multiple files in them. I find it quite implausible that it's 6x faster at formatting than fmt, and benchmarks are notoriously hard, so combined with the extreme lack of clarity I find it hard to take these claims seriously.