Make your program slower with threads (2014)
brooker.co.za
brooker.co.za
"Give me a random number" feels like it shouldn't require all this overhead and hidden complexity under the hood. But unless you've read a lot about implementation details, or have been bitten by that exact problem before, how would you know? You wouldn't, of course.
If you call a function that calls a function that has side effects, or that closes the file descriptor you're using, or could panic, or that needs global shared state, or spins up a thread, or allocates, we don't have a good way to deal with that. There are some partial and underutilised solutions like monadic IO and linear/affine types and, uh, not having panics. I have some (bad) ideas but I think this is a space that's worth playing in.
Imagine if you could look at a function, and know, undeniably - this, and anything it can call, absolutely cannot alter the file system directly or make a network call or spawn a process. Maybe instead of just types, you need to supply capabilities (much like interfaces on classes). Sounds like it could be a real pain. Would make auditing easier though.
Those three cases are excluded in pure functions.
You can make individual functions pure in any language, but do many languages enforce this at a core level? Or we have to behave ourselves and hope nothing we call is changed by another programmer later?
The tech is there. It sounds more like functional programming has been waiting for you for 20 years.
> It just seems fundamentally at odds with how most people want to work.
"Most people", or "you"? If you believe in something, stand up for it.
There are a couple of advantages to doing it through the OS. For one thing, the implementation is shared across programming languages. For another thing, it's potentially more secure against an attacker trying to inject code.
I guess the disadvantage of doing it at the OS level is that you might have to write OS-specific code.
The difficulty is that doing this sensibly requires new OS abstractions. It's one thing to put one in a research paper, but it's really tough to get this kind of thing into an OS nowadays. Another difficulty is OS level abstractions, with a few exceptions, cannot be used without a system call, which is cheaper than it used to be but much more expensive than just a function call.
A third problem is just that the programming language has a lot more semantic information, or at least it _can_ have a lot more semantic information, than can be fluently transmitted to the OS. There are approaches to deal with this (like having a richer OS / user land interface, almost impossible to get into an OS). In general, plugging into and extending the type system of some user land is probably going to be much easier route to take.
If the research world worked differently than it does, I'd have loved to continue previous explorations on OS / userland interfaces.
But I was also thinking of the development and refactoring side of things. Guarantees and assurances. Sometimes I know the generic code I wrote is 'OK' but the C# compiler says no - you're not being specific enough, I can't cast that - so I have to do better. A while ago I was trying to track down programs that loaded data from certain network shares, and it was a bit rough because these calls were all very deep, and very occasional. Traces (or blocks) of system calls only work when the code fires, but my task was to find and replace them all, so we could delete the share (for various reasons string searches only got part of the way). If 'network access' was tagged at a high level, I could follow the trail to see what we actually access and report on that. We had a similar issue identifying jobs that called out to external APIs. We left a log running for a while to catch some, but some only run a few times a year and we didn't have that long. Adding a firewall exception later could take days and these jobs have turnaround times.
I don't know if this is at all feasible. It's just some thoughts I had back then.
It doesn't. The libc is the real problem. The libc and its global state. Even something as simple as errno creates global state problems just like this one. There are buffers all over the place.
Freestanding C is a better language just by virtue of not linking against libc.
The libc was designed with heavy use of global variables in mind. Modules in C are designed as compilation units with static global variables and exposed functions that operate on them.
There's a reason many big projects (and programmers) use their own version of the standard library instead of libc.
None other than John Regehr recommends studying it [1].
My opinion is that the standard libc should be used much more as a fallback than a default.
And this is especially true about random number generation, there are much better generators out there, and some of them are only a few lines of code.
what this will do is download the .c file rather than just show it to you, not sure I understand that enirely, but just so you know.
What does that mean? Do you mean optimize a piece of code to take advantage of multiple cores? Or do you mean describe multiple independent threads of execution. The latter works best when it has it's own memory address and you assume that "if then else" works as you would expect.
(In practice, there is no way for a program to distinguish this from a single RNG that was initialized with a truly random seed that you can't recover, so it doesn't break backwards compatibility.)
Unfortunately, the C standard prescribes that failing to call `srand` must be equivalent to calling `srand(1)`, so I don't think glibc can get away with this trick.
Mentioned here: https://go.dev/blog/randv2
I think it could. Just supply two implementations of rand(): a strictly conforming one without this optimisation, and a less than strictly conforming one with it. The second is implemented in a function with a name like `__gnu_rand_optimized`, and then if you `#define __GNU_RAND_OPTIMIZED` before `#include <stdlib.h>`, then that header does `#define rand __gnu_rand_optimized`.
All the C standard requires is that you provide a mode of operation which strictly conforms to it. A platform is allowed to provide configuration options which cause it to be violated in various ways. Strictly conforming code won't turn on those options and so will work as the standard prescribes. If you choose to turn on such an option, that's a non-portable extension, and how it works is between you and the platform, the C standard isn't involved.
Or add a new function that provides behavior different from the existing one.
The latter is simpler. And safer. And more flexible. And faster, because you don't have a condition to check.
For example:
T1:
If rng() < 0.5 {
Rng()
}
T2:
Rng()
The specific numbers returned by calls to rng() depend on the thread ordering UNLESS the user carefully has higher level synchronization themselves to guarantee the ordering. This shows that the locking within the RNG is insufficient and the better performing option that maintains equivalent invariants should be selected (i.e. initialize the TLS RNG state for the new thread from a call to read entropy from the current thread’s RNG state).Case 1: T1: srand(); rng(); rng() T2: no calls to rng
Case 2: T1: srand(); T2: (spawns after the srand call) rng(); rng()
How would T2 know that it should produce the seeded sequence as opposed to one based on its own prng?
For a language that talks so much about simplicity, it puts a lot of effort into inventing clever wheels, with mixed results. I recently realized that SQL connections are thread safe and automagically pooled, I would have preferred more manual control. Don't get me started on date/time-formats.
you could pull different set lengths of pseudorandom numbers from the different seeds
Honorable (?) mention goes to (client-side) JavaScript - it's harder to have threading issues if you only have a single thread to work with!
https://hackage.haskell.org/package/MonadRandom-0.1.3/docs/C...
Also, if you feel that strongly, please don't roll your own crypto and use a CSPRNG.
"Linus Torvalds, though, argued that none of this effort was worth it. Speeding up random-number generation, beyond a point, is not the kernel's job, he said; instead, that should be left to the C library."
https://lwn.net/Articles/919008/
I've not been following the mailing lists so I don't know if that ever changed since then. The dev (Jason Donenfeld) seemed to think there was a way to get it working without the issues that blocked the improvement from getting merged last time though.
This is the rule you need to follow, but I don't understand what you're saying about global state.
The broken version has global state.
Non-broken versions can have global state or not have global state, depending on implementation choice. And that choice is separate from whether they have good performance.
You don't need it global
Seed it once per thread from /dev/random and you are done
I'm saying that if you have one PRNG, then you have global state no matter how it's designed. This is true whether you write it so that you get decent statistics, or you write it so you get tons of duplicate values.
And many of the fixes remove global state. Per-thread PRNGs are one option, but so are PRNGs that are used by specific batches of objects.
So, the straightforward broken option has global state, and the non-broken options might or might not have global state.
Which means I have no idea what you're talking about when you use the phrase "need to have global state". What is the PRNG API where that need exists, and what does the version without global state look like?
The only way to make a PRNG that does not share state between its consumers is to shard per-consumer. The PRNG in libc doesn't do that, it just uses a lock internal to the PRNG to keep things safe.
You could attempt to make a concurrent PRNG that has atomic-sized state and use CAS loops when generating numbers to keep the consumers in sync, but that's probably worse than using a lock.
Right, but again why did you say it needs to have global state "if you want to get decent statistics"?
What is the alternative to having global state that doesn't have decent statistics?
That "if" is the part I'm confused about.
The part of your comment directly after that sounds like you're describing the situation where clients are data-racing the PRNG and generating repeat numbers and other problems. But that's a problem specifically because of global state being used incorrectly, not something that happens as an alternative to global state.
I was considering "fairly broken PRNG" as its own significant group, and those need state too if you treat them as separate from "this literally gives the same number every time". But if those all go in the same bucket, then the comparison you made makes sense.
"Programmers waste enormous amounts of time thinking about, or worrying about, the speed of noncritical parts of their programs, and these attempts at efficiency actually have a strong negative impact when debugging and maintenance are considered. We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil. Yet we should not pass up our opportunities in that critical 3%." - Donald Knuth
Many programs today have no inner loop. Compilers were the first important programs that didn't. Most interactive programs have an outer loop processing events, rather than an inner compute loop.
Note that "AI" programs are much more like the scientific problems of Knuth's era. All the compute is in tight loops wrangling matrices.
Programmers now have done the impossible: written code so consistently bad that there are no hotspots, because the whole codebase is non-performant trash.
We’re going to be in a world of hurt when the newest process node doesn’t save us.
So, me, being brand new to OS/2, immediately wanted to try out the new multi-threading features. Without knowing anything about locks and spins or deadlocks or shared state, I plunged into it just using the OS/2 documentation and a programming book I bought. Keep in mind, this was a single CPU machine, probably an Intel 486dx or something like that.
I spent the next couple of weeks debugging all sorts of things that should not have been happening. There were crashes, lock up, slow downs, missing variables, etc... that I couldn't resolve.
After a while, I gave up and did what the OP did: just put everything in a loop and it solved everything.
If the program was properly written there wouldn't be any issues with multithreading.
The title of the article gives the false impression that multithreading is slowing programs in the general case. In fact poorly written programs are slow and that is true for both multithreaded and singlethreaded programs.
If your program runs slow but you don't need it to run fast, don't bother.
If your program runs slow and you need it to run fast, go see what optimizations can be done.
From my experience, in the general case programs are poorly written, so yeah, multithreading makes them 1) more complex, more poorly written, and 2) slower in many cases.
Now try a multi-thread program compute bound program with more threads than CPUs that does Rust vector "push" operations. Push causes an array to grow, locks get set, other threads hit locks, threads go into spinlock mode, threads inside spinlocks context switch, lose control, and performance drops by two orders of magnitude. Most CPU time is going into spinlocks.
I'm not convinced that user-space spinlocks are a good idea. If you're never compute-bound, they can work, but if you run out of CPU time and a context switch occurs inside a spinlock, it's all downhill from there.
Windows itself doesn't do this; it has a different locking strategy.
Make your program slower with threads - https://news.ycombinator.com/item?id=8711162 - Dec 2014 (47 comments)
"The random() function should not be used in multithreaded programs where reproducible behavior is required. Use random_r(3) for that purpose."
Perhaps the notes should be updated to state that the performance of multi-threaded programs is also affected due to the shared global resource?
However, the point of the article still stands.
I remember in the early 2000s doubling the performance of a CPU-bound C++ program by using two threads, and my boss being shocked that I was able to do it. He had written it off as a naive idea. I was too young and inexperienced to understand why he was surprised; to me it just seemed like he didn't understand what I was doing. In retrospect, now I feel like maybe he had the right default expectation, and I got lucky that it worked out so easily.