It really is though. Memory and thus data _and_ instruction encoding were incredibly important. Physical wires on the circuit board were at a premium then as well. It was an incredibly popular platform because it was highly capable while being stupidly cheap compared to other setups.
Engineering is all about tradeoffs. "Purity" almost never makes it on the whiteboard.
x86 could have easily had an IP-relative addressing mode for data from the beginning (jumps and calls already had it), but to get a pointer you can pass around to use someplace else than the current instruction, it has to be either absolute, or relative to some other "base" register which stays constant. Like the segment registers.
The 286 and 386 in protected mode did allow segments with any base address (24 or 32 bits), so your argument about extending the address space doesn't make sense.
386 (not sure how 286 works) did extend segments to a larger address space, by converting them to segment selectors, but it requires a significantly more complex MMU as it is a form of virtual memory.
The 286 did that, though they only extended the address space to 24 bits. The 386 extended it again to 32 bits.
This design allowed for a smaller chip, and keeping backwards compatibility with the 8080.
Yet, it extended CP/M by allowing you to use more than 64 KB vs. 8080/Z80.
Pardon me for not realizing and treating it appropriately.
> with the same inelegant workarounds that's been rediscovered since the late sixties or so
Short of creating instructions that take 64bit immediate operands you're always going to pay the same price. An indirection. This will look different because it will be implemented most efficiently differently on different architectures.
> GOT and PLT versus pools of absolute 64-bit addresses, pick the least worst one.
Or statically define all those addresses within your binary. That seems more "elegant" to you? You'll have the same problem but your loader will now be inside out or you'll have none of the features the loader can provide for you.
At that point just statically link all your dependencies and call it an early day.
There is a difference between indirecting through a register, or through a memory (which in the end also requires a register, in addition to a memory load). On the other hand, I$ is more precious, and the most popular parts of GOT are likely to be in the voluminous D$ anyhow, so it's hard to tell which is more efficient.
> Or statically define all those addresses within your binary. That seems more "elegant" to you?
Of course not. I personally think a directly specifiable 64-bit offset from the base register that holds the start of the data section is more elegant. But dynamic libraries don't mesh too well with this approach although IIRC it has been tried.
> you'll have none of the features the loader can provide for you. At that point just statically link all your dependencies and call it an early day.
This works surprisingly well in practice, actually. Data relocations are still an issue though.