The 8086 used a segmented memory architecture where the linear address was computed from a 16-bit segment number and a 16-bit offset. This greatly complicated things from a programming perspective.
I beg to differ. Using segments doesn't 'complicate' things in any way. Sure, it may require a different style of structuring the data used and there are very few cases, in real world applications, where pointer arithmetic is needed.
The whole issue is a bit like a NYC cabby crawling bumper to bumper on the FDR and complaining about the speed limit on highways and how great the German Autobahn is. While right in theory, there is no practical implication as his job will never let him use this advantage - even if he moves to Europe and becomes a Taxi Driver.
The Motorola MC68000, designed at about the same time, used a flat 32-bit linear address space and was much easier to program.
Not so sure. Mind you, that not only Unix was originally designed segmented (and C still carries this over), but as well the (original) MacOS. Here a user application had to make OS calls for dereferenceation all the time to allow its memory management to work. The flat 68k address space is maybe nice for embedded programs or cases where the simple assumption of one programmer, one program and one machine works out, but not in a more sophisticated environment.
I understand that source-level compatibility with the 8080 was a consideration but surely the CPU could have started up in, say, 8080 mode where the 16 MSBs of the address registers would be forced to zero then switch to full 32-bit (or 20-bit) addressing via a mode-switch instruction.
That would have restricted the machine to an emulation mode, where converted Software could only use a single 64 KiB address space, leaving the rest of memory dormant. And only one such program could run at a time. So effectively removing every reason to switch away for an existing 8080/85 system.
Unless one invents some kind of relocation mechanism that is. Like having a process pointer to give separate 64 KiB address spaces to each program, or the ability to address additional memory beyond 64 KiB with some banking or such... oh, wait, that's exactly what segments do.
Keep in mind that 8085 source compatibility is not a feature in itself, but a request to satisfy customer needs. And one of the biggest needs was to allow more code to be handled (and maybe a bit more data *1). The issue of source code compatibility is often seen as a rather easy thing. Just converting some instructions into equal and that's it. But software also and eventually foremost means data structures. A part that often needs more than a few key presses to be adapted. The instruction mix of the 8086 had for (next to) all 8080 instruction a functional equivalent. While the code representation (binary) may change not only in content but also in size, data structures and their interaction could be replicated without any change.
Adding segment registers offered an easy way to port application with only a few added lines to the 8086 and enabling the use of 64 KiB code plus 64 KiB data (*2). Depending on the applications call structure this could be extended to several hundret KiB of code
As well important is complexity of the CPU itself. The 8086 is a very clean 16 Bit CPU. There are no 32 bit operations (*3) except to optimize far pointer loading. Unlike the 68k with an outright bloated code requirement for handling 16 and 32 bit data types. Not the least reason why 8086 did outperform the 68k in real applications at comparable speed rates (*4).
So, why spending more than double the transistors (*5) on a more complex CPU design that yields less performance? For a feature that is hidden by compilers anyway?
What exactly were the reasons the designers of the 8086 chose a segmented memory architecture instead of a flat, linear one?
It's easy to count several good reasons:
(A non exhaustive list what just came to mind. There might be many more to be found by spending more time to think about)
First and most important, it's a clean 16 bit CPU. There is no need to handle any other data type (beside byte for memory access).
KISS.
Easy port of existing 8080/85 software due a virtual maximum data type of 16 bit for pointers.
Extension of available code space to full 64 KiB without modification
Easy extension of available code space (with a minimal level of modularization) for existing (ported) software.
Extension of available data space to full 64 KiB without modification
Speed. By reducing the majority of pointer operations to 16 Bit instead of 32 on a 68k (*6)
Speed. By reducing code size due a restriction of all code and data pointers to 16 bit. After all, more compact code needs less bus bandwith, leaving more for real work.
Simple memory management for multi tasking
Simple memory management for multi programming
Support existing complex OSes like Unix
and quite important (and to make the dozend full):
- (well behaving) 8086 application can run seamless in a (future) virtual memory environment without any modification.
It might be useful keep in mind that in 1977 mini systems were still build (and bought) with 64 KiB RAM or less. Even a small memory model application (64 KiB for each segment), couldn't be handled by such. Here the 8086 was quite at the height of time. Segmented memory was the way to go. At that time next to all classic 'flat' architectures had reached their EOL. Eventually except the /370, but it might not count as realy flat due its general base+offset addressing scheme.
Long story short, the 8086's segmented scheme was not only on the heights of its time but also a very efficient and foreward looking design.
Addendum: There might be many details to critisize of Intels design decisions in hindsight, like supercat (*7) favouring an 8 bit offset (instead of 4), and some may look quite appealing (*8), still the 8086 did include a remarkable lot of features in a very small and limited design, making it back then a huge success outside the (later) PC market. x86 Unix systems did outsale 68k based ones by a magnitude, especially due its early ability to handle a large physical address space via segmentation.
*1 - The main market for 8080/85 systems were not dektop computers, but embedded systems. Desktop users were in 1977 not only a real minority, but also still happy to have even some memory, with full 64KiB being a dream for the wealthy. Complex embedded systems in contrast did already back then scratch the 64 KiB limit.
*2 - The additional stack segment doesn't really count, as such application would only need a rather meagre amount of stack.
*3 - For nitpicking, there is multiplication and division as 16x16 and 32x16
*4 - At the same clock rate a 80286 outperforms a 68000 by about 20%
*5 - An 8086 got ~29,000 transistor functions, while the 68k is said to have 68,000
*6 - Not to mention that the 68k here always had to shovel a 33% overhead
*7 - Part of this chat. mschaef did make a similar comment as Alex Hajnal reminded. Alex is a serious data archeologist :)
*8 - When thinking about the 8086 and its memory management I'm usually rather satisfied what has been made possible, as the segmented aproach is quite useful for multi programming (and multi tasking as well). I can only blame the designer(s) for not having thought a tiny little step ahead for a by adding support for a software managed MMU supporting memory protection and swaping. All the hardware needed would have been 4 segment size registers(SSR), 4 16-bit-wide OR gates and one comperator active during Effective Address (EA) generation.
Whenever an EA is calculated and the appropriate SSR register is non zero (checked via the OR gate) the resulting EA gets compared to its SSR. When low or equal processing continues, when higher a 'Segment Violation' Exception happens - much like INT 0Dh later on the 286. Now a some (OS) handler can decide what to do.
Similar all instructions loading any segment register (or SSR) would (when segment checking is active) issue a 'segment check' exception. Again like the 'Segment Not Present' (INT 0Bh) on a 286 (or the whole 0Ah/0Bh/0Ch group). As well an OS handler could check if the new segment value is one assigned to the program.
Whenever an exceptions happens the checking is disabled to allow the handler to act as needed (which also would make disabling it quite easy for an OS without memory protection by adding a NOP handler to kill any checking that got activated by 'accident') and must be switched on again before returning.
With this all need for a protected mode OS would be present. Sure, segement switching would be rather slow, compared with a hardware solution like with the 286, still better than noting and at minimal additional cost. Not to mention that if DOS had used this feature, many ugly programs and even less programming styles, would have prevailed :))