3 ms·
The apparent lack of responsiveness we see with a lot of modern software is usually a product of three factors. Firstly, architectures today tend to be multita
by Chris_Newton 14y ago
The apparent lack of responsiveness we see with a lot of modern software is usually a product of three factors.
Firstly, architectures today tend to be multitasking and event-driven. A modern GUI application is usually built around an “event loop” that waits for various interesting things to happen, such as the user pushing a key or clicking a mouse, reaching a certain time, or receiving a signal from an external device. When an event of interest takes place, the relevant part of the loop will detect it next time around, and then run the relevant code to react.
However, if that code takes a significant amount of time to run, then the event loop won’t have a chance to react to anything else that is going on until the last event is dealt with. The catch is that things like repainting your application window because the user happened to move another window over the top of it are also handled by the event loop, so if you’ve got an application that is trying to check for new mail every five minutes but your network connection has dropped and it takes 30 seconds to time out, the application might be completely unresponsive to any other kind of user interaction during that time, and might not even redraw properly if you move other windows over it.
The usual solution to this problem is to introduce a degree of concurrency, so that any event-handling code that isn’t guaranteed to complete very quickly is executed in a new thread in parallel with the event loop. The loop itself is therefore immediately free to continue responding to other events, possibly kicking off still more new threads, as necessary. Unfortunately, there are significant overheads to coding this way, so a lot of simple applications don’t bother and just accept that they might become unresponsive now and then. And of course it’s always possible to make a mistake in the design, responding to a certain type of event that is usually dealt with quickly in the main thread without realising that under some circumstances it will take much longer.
The second factor is abstraction. We used to program pretty close to the metal, because you couldn’t get acceptable performance any other way. Today, we build on top of OS kernels, hardware abstraction layers, device drivers, virtual machines, run-time libraries, and who knows how many other tools, all stacked up and each composed of many layers themselves. The benefits of this approach are many: faster development time, more effective testing and more robust software, ease of porting software to run on different hardware or operating systems, and so on.
The downside is that every time you introduce another abstraction, you are potentially introducing an overhead, and those effects multiply. Even if your abstraction is say 90% efficient compared to programming directly to whatever lies beneath it, if you’ve got 10 layers of abstraction between the code you’re writing in your end user application and the hardware you’re ultimately talking to, the effect is to reduce performance by about 2/3. And there are a lot more than 10 layers between your average end user code and the hardware on most systems today, and some of them are far from 90% efficient.
The third and final factor is somewhat related: if development time (or, if you prefer, time to market) is more of a priority than raw performance, it’s easy to skip over the kind of optimization work that used to be essential to produce software that ran acceptably quickly and within the relatively limited RAM and permanent storage capacity available. It’s one thing to skip the fine details and hand-tweaked assembly language, which was often a very expensive activity in terms of time spent vs. resulting speed-up. It’s another thing to just throw in the first data structure or algorithm that comes to mind, often because whatever software development toolkit you’re using has a limited set of options available and maybe it’s not worth the time to code up something more appropriate, or maybe because as the industry has grown a lot of developers aren’t either formally trained or keen hobbyist geeks any more and not everyone knows about all the options. This is how you wind up with abstractions that are a lot worse than 90%, or that don’t scale well as the amount of data they have to deal with increases and assumptions that the hardware is “fast enough to make up for it these days” break down.
So, in summary, and in more of a cause-to-effect order:
1. The emphasis on reducing development time because of commercial pressures or other non-technical factors, combined with a certain complacency about the capabilities of modern hardware, means that not as much optimization tends to be done before code ships today. Sometimes this introduces a modest overhead, but it can be severe if a poor choice leads to scalability problems.
2. Even if the overheads of any one function are modest, we build modern systems from a stack of many abstraction layers, and their overheads multiply. The compound effect can easily be the equivalent of running on hardware from several years ago, even if no individual layer was grossly wasteful.
3. Because we tend to use event-driven architectures for a lot of modern software, including most GUIs, a single slow operation can block the whole event loop and effectively make the whole UI unresponsive indefinitely. More advanced architectures that would prevent this have significant development costs and aren’t always used for the same reasons as above.
[Edit: OK, this is getting a few upvotes and no-one’s replied suggested cutting it down, so I’ll leave the whole post as it is. Sorry if anyone does feel it’s too long.]
- tedunangst 14y agoGreat post. I wish you kept the same order in the summary, but using the numbering at the end, re #1, the raw speed of CPUs means the difference between O(n) and O(n^2) may not be measurable for reasonable test cases, so it looks fast enough. re #3, #2 also contributes to this. when every function you call goes off and calls ten thousand other functions, you lose control of what blocks and what doesn't. Sometimes GUI event loops don't integrate well with other event loops, such as for networking.