Is the x86 Architecture Sustainable?
robert.ocallahan.org
robert.ocallahan.org
Even x86's model of handling CPL (privilege level) isn't ideal. I've tried to push RISC-V toward a model where user and kernel code have entirely separate address spaces, which would let privilege handling move to the front end and potentially allow pipelining across privilege changes. Amusingly, x86's privilege mechanism is complicated enough that Intel implemented it wrong in a few CPUs with MPX, resulting in a bizarre erratum.
I would love to see x86 add a "clean mode" in which a lot of legacy features are inaccessible to user code. Then kernels could try to run programs in clean mode and, if they don't fault, context switching between clean mode threads would be much faster.
Because each was retrofitted, each protection ring in x86, from -1 to 3, works differently. When IBM originally invented virtualization (IBM 360/67, late 1960s), they got it right; all the layers work the same, and you can run VM on VM on VM until you run out of memory. That's been continued into today's IBM mainframes.
CS.RPL, CS.DPL, SS.RPL, and SS.DPL all usually match CPL, but I think that all four equalities have nasty corner case exceptions. This results in a bunch of overhead when you change privilege: the CPU needs to update CPL, CS.RPL, CS.DPL, SS.RPL, and SS.DPL. This means that you need to change two of these nasty large legacy registers, the values of which barely matter for practical purposes, every time you enter or exit user mode.
Saying there's a ring -1 is a red herring here. There's SMM, which is a mode of execution that's more privileged than ring 0, but it doesn't have a number -1 associated with it, nor is it part of CPL. Similarly, ring 0 is split into root and non-root when virtualization is in play, but that's also a bit beside the point.
[1] Check out the Linux commit message for ae9fedc793c4d98aa9bb298585b2b9246096ce65 if you want to see a bit of how nasty this is.
Those are only saved/restored when switching between processes not between threads. Context switching when dealing with threads is typically done in software, not in hardware, precisely so that registers that are not used (like the segment registers) don't get needlessly saved & restored.
Saving/restoring registers isn't even the expensive part of a context switch anyway. Switching address spaces, cache flushes/misses, etc... are all more expensive than saving/restoring registers.
No, those registers are per-thread. The only things that are per-process are the page tables and, in special cases, CR4 and LDTR. If you want to see the code on Linux, look at switch_mm_irqs_off().
> Context switching when dealing with threads is typically done in software, not in hardware
Hardware context switches (the x86 "task" mechanism) have been obsolete for a long time and are unavailable in long mode.
> precisely so that registers that are not used (like the segment registers) don't get needlessly saved & restored.
And that's the problem: the OS doesn't know that the registers are unused until it saves them.
> Saving/restoring registers isn't even the expensive part of a context switch anyway. Switching address spaces, cache flushes/misses, etc... are all more expensive than saving/restoring registers.
Go compare WRMSR to MSR_FS_BASE to the cost of a CR3 (page table) write. You might be surprised. And that CR3 write is about to get considerably faster, maybe in Linux 4.13, when my PCID patch set lands. (Yes, Linux will eventually use WRFSBASE instead, but that's a minefield and will take a while.)
But in practice, I guess this legacy architecture adds a nanosecond or two to a thread switch. Is that a big deal? How many switches does a desktop PC do in a second? How about an AWS/Azure/GCP server?
I've never experienced lag in a game or app or web server due to slow thread switch. Server side async is on the rise anyway, minimizing number of threads and switches.
I would guess the total wasted time is more like 500 cycles. This won't make your game lag, but it does mean that extremely low overhead synchronous IPC can't happen on x86.
On Windows, a lot of the windowing system is in the kernel in win32k.sys. Microsoft didn't do this for fun -- they did it because the old way (NT3.5?) was too slow.
> Context switching when dealing with threads is typically done in software, not in hardware
I'm not sure any OS exists that does register saving in HW for switches between processes and in SW for switches between threads. Its especially doubtful for Linux, given it does not really have a very strong concept of what is a process vs. what is a thread.
You are incorrect. Context switching is done for threads in HW. I think you are thinking of cooperative multi-threading (like pre-Windows 95, or nginx or node.js or V8) instead of true context-switching threading.
Preemptive multiprocessing then needs paging and such to make isolated memory a thing as well, but still doesn't need HW context switching.
Uhm, x86-64 can't switch between tasks/contexts in hardware.
AMD should have killed these registers entirely in 64-bit user mode, but they didn't.
I wonder if part of the problem is that AMD was still new to x86 CPU design back when SYSCALL/SYSRET was developed as part of the K6.
Can you elaborate on whats special in terms of CS, SS and the FLAGs registers in terms of what the microcode must do?
>"Even x86's model of handling CPL (privilege level) isn't ideal. I've tried to push RISC-V toward a model where user and kernel code have entirely separate address spaces, which would let privilege handling move to the front end and potentially allow pipelining across privilege changes"
I thought that mapping the kernel into the top of of a userland process address space was supposed to be kind of elegant as it prevents a TLB flush. What is does "move to the front end" in your proposed alternative to this arrangement? This sounds interesting. Might you have any links regarding this?
For SYSCALL, SYSRET, SYSENTER, and SYSEXIT, microcode needs to change the values of the registers (including the hidden parts). For entries through the IDT (interrupts, faults, and traps) and IRET, the microcode needs to go through a whole bunch of complicated logic including looking up the requested CS and SS selectors in the GDT or LDT.
> I thought that mapping the kernel into the top of of a userland process address space was supposed to be kind of elegant as it prevents a TLB flush. What is does "move to the front end" in your proposed alternative to this arrangement? This sounds interesting. Might you have any links regarding this?
Once upon a time, the x86 MMU, and probably most other MMUs, was quite simple. There was a TLB, which is essentially a cache that maps (VA) -> (PA, access rights, cache mode). A memory access looks up the virtual address (VA) in the TLB, then checks the access rights and accesses the physical address. In the event of the TLB miss, the CPU falls back to reading the page tables and fills the TLB. When you change to a different process with different page tables, you flush the TLB. You can either map the whole kernel into every process's address space of you can map a small trampoline that switches to the real kernel's address space and flushes the TLB every time you enter the kernel. From (IIRC) 486 onward, x86 would let you tag a TLB entry as "global", meaning it would survive a normal TLB flush and would only be cleared on a special type of flush. Linux uses global entries for kernel pages so that only user entries get flushed on processes switches.
Modern CPUs have a somewhat different arrangement. The TLB maps (VA, address space ID) -> (PA, access rights, cache mode) [1]. On a TLB miss, the CPU will look up the VA in the current process's page tables and add a TLB entry with the appropriate address space ID. On x86, the address space ID is (VPID, PCID) [2]. This means that you can sometimes switch between processes without a TLB flush -- you can change the page tables and the address space ID and leave the TLB intact.
I would like to see CPUs arrange it just a bit differently: have a separate address space, complete with a separate tag bit, for user mode and kernel mode. Then user accesses effectively look up (VA, "user", ASID) and kernel accesses effectively look (VA, "kernel", ASID). This would add almost no overhead. As an added benefit, when the front end decodes an instruction, it would know the whole TLB key (i.e. VA, user/kernel, and address space ID) and could decode user and kernel code differently. This means that kernel-mode instructions could be rejected in the front end. More importantly, if you imagine the user/kernel bit as being an extension of the VA, a transition between user and kernel mode is functionally the same thing as an indirect jump and could use all the optimizations, including pipelining, that indirect jumps use.
[1] It might be organized internally as (VA) -> (address space ID, PA, access rights, cache mode), but it doesn't really matter.
[2] VPID identifies the current VM guest, if any, and PCID is used at the discretion of the kernel. Linux may soon use PCID to allow a few processes to coexist in the TLB at once. grsecurity uses it for a totally different purpose.
>"You can either map the whole kernel into every process's address space of you can map a small trampoline that switches to the real kernel's address space and flushes the TLB every time you enter the kernel"
Do you know what OS's employed the "trampoline" option instead of the more common mapping the kernel into every process address space? Were these commercial UNIX vendors maybe?
[1] The AMD interactions between CPL, CS, and SS are so messy that there's still an unresolved crash when KVM runs an older Linux guest and exits all the way out to QEMU to handle, say, a virtio interrupt at the wrong time. QEMU tries and fails to read the state and write it back fully.
Take a look at this annotated die shot of an old Pentium 4:
https://i.stack.imgur.com/QK4gm.jpg
Up at the top is the microcode memory and microcode sequencer. That's the hardware that's responsible for translating all those old, large legacy instructions. It's not really much space on the P4, and the P4 Northwood is a 55-million transistor CPU.
Nowadays, depending on if you're talking about a mobile part or a higher-end desktop or server part, the CPUs have between ~1.5 billion to 2 billion transistors.
Again, that microcode ROM just doesn't grow very much as you add instructions, even a ton of instructions. And this is, well, one reason why Intel just keeps adding instructions. It's practically free.
But quite apart from the transistor cost of various legacy feature is the engineering cost. When designing a new x86 chip you have to make sure that every instruction works correctly in every mode that supports it and that's hard. NRE is a large fraction of the cost of each chip and verification is a big part of that. Intel had all the transistors in place to support SMT early on in the Pentium IV development but they didn't turn it on until Northwood because it was hard for them to make sure it worked reliably. And recently look at the problems Intel has been having getting TSX to work right or the problems AMD had with legacy modes in Ryzen.
And also, decode might not take very many transistors but those transistors are in constant use and consume way more power than the huge seas of 8T SRAM cells in the local cache despite being hugely outnumbered. This is far from the majority of the chip's power but every little bit counts.
Not only can today's features limit tomorrow's design, but because those features give rise to mental models and habits of thinking they can limit future design in an almost unconscious way. The more complex your system becomes the more its existing complexity dominates your thinking to the exclusion of innovative ideas.
This process happens so quickly in software that it's obvious there:
It does not matter that adding instructions in that framework is not too costly. The framework itself is costly for some applications...
This market is a lot more price-sensitive than the desktop, laptop and server CPU market.
Further the core issue is the cascade where A limited B and C is limited by B directly and A indirectly, now continue that though 15 generations and things get complicated.
Note: I have zero experience in hardware design
Ever thought of getting back on writing them occasionally?
The same author has also written [2] about Intel's CET (their attempt to add control flow integrity (CFI) support at the CPU level):
> The tail end of Intel's document is rather terrifying; it tries to enumerate the interactions of their CFI feature with all the various execution modes that Intel currently supports, and leaves me with the impression that they're generally heading over the complexity event horizon.
[1] https://news.ycombinator.com/item?id=14560267
[2] http://robert.ocallahan.org/2016/06/is-control-flow-integrit...
See "ISA Overview" here: https://content.riscv.org/wp-content/uploads/2016/06/riscv-s...
x86 processors also have cpuid
> https://en.wikipedia.org/wiki/CPUID
to signal which extensions are supported or not. In theory it would also be possible to drop deadweight extensions this way for x86, too. The practice shows that at least for the tasks that PCs are used for, one can hardly drop anything here. Exceptions are
- 3Dnow! (is not supported in Bulldozer, Bobcat and Zen anymore; cf. https://en.wikipedia.org/w/index.php?title=3DNow!&oldid=7696...)
- A20 gate (not supported anymore since Haswell: cf. https://en.wikipedia.org/w/index.php?title=A20_line&oldid=78...)
The problem with feature detection is that you can't depend on programs to be well-behaved. For example in Web applications you often see accidental or deliberate assumptions "if we detect X, then Y must also be supported". Often there is a !Y path that has never been tested and therefore doesn't work.
This is yet another danger to security.
As far as I can see, if compability with old software is the only concern, then losing 80% performance isn't really a problem since the new CPU is probably 5x faster anyway. Thus making up for the loss, so the old executable runs as fast on a new CPU, using emulation, as it did running in hardware on the old one.
So all we'd really lose is a speedup for legacy software -- and who really needs to play Wolfenstein 3D at 80,000 FPS, as long as it works?
This is probably an important reason why IA-64 failed, but x86-64 succeeded, Itanium CPU's ran x86 software at about half the speed for a much higher price.
The ARM solution seems like the least suboptimal solution in this case, as it at least eventually allows support to be dropped, when emulation is no longer a problem.
The only large-scale successful transition of an ISA migration without backwards compatibility is when Mac Os X transitioned from PPC to x86. It probably helped there that the Apple ecosystem is a bit special and much more closed and controlled, and that the Intel Core Duo's of the time had much better performance than the IBM G5's.
Of course, a lot of ARM use cases also don't have that luxury. Android apps using the NDK are the most obvious example. The Raspberry Pi is another example: the RPi3 is still on 32-bit, held there by binary blob drivers and the massive amount of effort needed to create the fully open source driver stack that's a prerequisite for moving over (Eric Anholt is doing an incredible job; it just takes time).
They had fat binaries and slow emulation as a stopgap solution. But these become mostly unused pretty soon.
That is, there is a possibility to move to a new ISA if most of the software will be recompiled with minor changes, and there's plenty of will to upgrade. This could work for server software.
Not so with Windows desktop software, of course.
On the server side, one can use quite good Power8 servers right now, or ARM-based servers, depending on the workload. An Intel CPU with a non-x64 architecture might have a good chance against them. But the transition would be hard if Itanium or Transmeta are any indication.
If the new thing were and x86_64 CPU with just the oldest, least used and least useful features removed, things that haven't been used intentionally by new software in the past 15 years the problems you describe could largely be avoided. The gains would not be as big as cutting out the all worst features, but there would be some gains for minimal pain.
Potentially this could be paired with a deprecation process that Intel, AMD and other x86 (Are there any other) manufacturers agree on in an open setting with ISVs. If this working group announces the deprecation of some features and the ISVs agree its a good idea, then the chip makers could put out a date some years in the future where that feature would no longer be in new CPUs.
But this is perhaps the first time in history that there is a realistic chance of them being replaced by an alternate ISA. The largest markets for "computers" (and by that I mean the ones that people compile on and develop systems on and run data centers on, have largely adopted open source (if begrudgingly) and that means you can get functionally identical software (but not performance identical) on different ISAs.
So at that point what keeps Intel alive is their volumes make it so much more cost effective than a bespoke architecture. And then there is ARM which is 'open' in the sense that anyone can get a processor architecture license, and 'high volume' as the basis for all of the compute appliances.
But that effort has been slow going[1] and continues to falter on corporate models that want to jump to the level of penetration today x86 has in a heartbeat rather than through measured evolution.
[1] https://www.nextplatform.com/2017/02/01/arm-server-chips-cha...
IIRC that was a notable cause of DOSBox becoming slower when going 64-bit as it had to emulate the 16-bit CPU instead of just running on metal, as well as 64-bit Windows (7?) being unable to run old DOS and Win3x software.
With an increasingly rolling-release culture on software I can't wait for X-year support for hardware instruction sets /s. Wait, this is already happening for Android phones, where ARM SoC manufacturers just flip it at OS and phone vendors (Nexus 4, OnePlus X). I just realised Apple made a damn right call getting that under their control on that front.
† the metal may still be there though, waiting to be jettisoned as the market for 32-bit OSes goes away.
This is still possible, though unsupported by Windows and Linux (the latter one never supported 16 bit code).
This mode was for example used under Windows 9x for implementing the DOS box.
But this is not what I was talking about. I am talking about 16 bit code in protected mode. This is/was used under Win16 and is supported in all 32 bit versions of Windows.
http://i.imgur.com/KtgLeWu.png
(this is an old shot of mine and yes i used a XFCE theme to make it look like Win95 :-P)
Not under Windows: https://news.ycombinator.com/item?id=14246521
Of course, a 64-bit OS could pop in and out of 64-bit mode as needed.
Probably there is a lot of more things that were removed.
Are they really dropped in x86-64? I thought they are still supported also in 64 bit mode.
I didn't know there still was a market for 32-bit x86 OSes. I suppose legacy systems - SCADA and such.
Fewer and fewer systems or people care about x86 vs ARM vs RISC-V. C has been the intermediate language of choice for decades now, and things like WebAssembly drive that care even lower. And if you really have to, you can binary translate.
x86's real problem is Intel's business model.
To compete today, you need processor IP you can integrate with other components on your die or system -- that's why ARM wins outside of the data center. In theory, Intel could sell x86 designs a la ARM, but that would completely break their business.
My bet: x86 gets squeezed between the rise of GPU vector supercomputing and ARM general purpose CPUs. Apple is already doing this in-house.
On the other hand, while it's true that languages and runtimes provide more portability these days, there's still important handwritten assembly, and the amount of investment in compiler implementations still gives popular ISAs an edge.
I can imagine customers that claim they don't care, then some ISV releases some software or game that only runs on one platform, then all of sudden the customer cares. The ISV might not be malicious or in cahoots with anyone, perhaps they just have 2 decades of experience releasing for wintel and haven't reconsidered their approach.
Alternatively, I wonder how feasible it would be, for a multi-core system, to keep one legacy core for incompatible processes and faster new-arch cores for running the main beef.
Ideally we should also be able to decompile and recompile code. But this might be hard for x86 die the complexity.
The approach that immediately occurred to me would be have a layer that translates the legacy instructions into modern equivalents; without as much concern if they are slower to execute in their new form (they're legacy, after all, right?).
Of course, doing something like that is probably nowhere near trivial, the devil's always in the details.
But I bet this is already being done at the microcode level. Stepping things up to having a published agreement about which instructions were globally considered "legacy", and guidelines for what their equivalents were, would go a long was towards allowing a general feeling that an ISA was evolving, rather than just accumulating weight upon weight.
This is what x86 CPUs already do and have been doing for years.
It's also why Intel & AMD don't care about "x86 complexity" and it's also why even though people love to claim that a switch to RISC would improve efficiency/performance, so far there's really no evidence to support that.
Intel just plops their teeny-tiny (in terms of die space) x86-to-internal-microcode transactor on top of their new cores and calls it a day. As long as that translation layer isn't a bottleneck, which it rarely is, then Intel doesn't care.
Linux could eliminate all that complexity tomorrow by just bulk removing x86 support and only running x86-64. All OS complexity eliminated. All context switching complexity eliminated.
Legacy features shouldn't have a large impact on transistor usage, and they should have almost no impact on efficiency (although that may involve design choices). Almost all of the transistors are going to be used to implement the cache, routing between the high level blocks (external IO, cores, shared cache), and routing between the various microarchitecture units.
Using Broadwell as an example, the CPU cores[1] only use a relatively small amount of amount of area. However, even that is misleading, because most of the acre within each core is dedicated to the microarchitecture, which implements a far smaller set of micro-ops. Inside each core[2], the only place affected by legacy instructions is the decode stage (mostly in the "4-Way Decode" block in [2], probably less in the "PreDecode" block"), and a larger MicoCode ROM.
The legacy instructions might not be as efficient as they used to be in older CPUs; as low-priority instructions they may be implemented with less-convenient micro-ops that were designed for other, faster instructions. The software that uses those instructions was probably designed for a far slower CPU anyway, so it's probably no important if those instructions are a "slow" when the CPU overall is much faster.
I agree that the interactions between instructions might increase the difficulty of security, concurrency, or other higher-level concerns, but that will depend on the specific instruction. I'm not sure how large these concerns might be, unfortunately, so I cannot say if it's a serious issue.
> so their complexity ends up being "dead weight"
This sounds suspiciously like the unfortunate recent practice of labeling "old" things as "bad" or "holding back progress" and thus must be removed. In most cases backwards compatibility is a very important feature that adds very little cost. The low cost is possible because you can usually implement older features in terms of the newer features. This is true in CPU microcode, just like it's true in software.
[1] http://i59.tinypic.com/2mrxso9.jpg
[2] https://en.wikichip.org/w/images/thumb/a/a1/broadwell_block_...
Where it really kills you are things that have to analyze x86 binary code (e.g., binary static or dynamic analysis tools). Unless you cover every nook and cranny of x86, chances are you'll eventually encounter some program that uses one of these instructions and your analysis will break. And of course emulators like QEMU face the same problem.
This problem becomes especially severe in the context of malware – any instruction you forget to model becomes a way for a malicious program to evade your analysis.
Turns out that in practice a tiny number of instructions make up the vast majority of all used upcodes in a typical application (although it's not clear in this article how long the tail is, there could be hundreds of instruction in that "others" slice).
My understanding is that there are many legacy instructions in x86 that are basically never used nowadays. They're still a problem for silicon designers because they still have to implement them for compatibility purposes but compiler developers can chose not to use them and instead select a more "risc-y" subset of instructions to do the bulk of the work.
In general Intel on the server side is under pressure by specialized chips. Incremental spend will rather go into specialized silicon, SSDs and RAM than into CPU where it delivers more bang for the buck.
On the PC side people are looking for battery life - something Intel lags. Both Apple and Microsoft are eying the ARM universe causing Intel to recently fire a patent threat in the direction of Redmond and Qualcom.
From a volume perspective Intel is big but ARM with both mobile and IoT behind it has the momentum.
The tick-tock has slowed. New generations are becoming harder to manufacture.
Moore's law ending will assert itself in economic terms. Complexity is not your friend when the volume is increasing elsewhere and adding value becomes harder.
http://wccftech.com/intel-developing-new-x86-uarch-succeed-c...
The other aspect is cloud providers have the data for the next generation of processors.