This is tangential and somewhat pedantic, but I want to point out an oft-repeated mischaracterization.
C is essentially a refinement of B with some additions [1], and B was written for the PDP-7, which was a very different machine.
For example, the increment and decrement operators in C were inherited from B; they could not have been modelled after the PDP-11. From Dennis Ritchie himself [2]:
> Thompson went a step further by inventing the ++ and -- operators, which increment or decrement; their prefix or postfix position determines whether the alteration occurs before or after noting the value of the operand. They were not in the earliest versions of B, but appeared along the way. People often guess that they were created to use the auto-increment and auto-decrement address modes provided by the DEC PDP-11 on which C and Unix first became popular. This is historically impossible, since there was no PDP-11 when B was developed. The PDP-7, however, did have a few "auto-increment" memory cells, with the property that an indirect memory reference through them incremented the cell. This feature probably suggested such operators to Thompson; the generalization to make them both prefix and postfix was his own.
Another person puts it this way [3]:
> It's a myth to suggest C's design is based on the PDP-11. People often quote, for example, the increment and decrement operators because they have an analogue in the PDP-11 instruction set. This is, however, a coincidence. Those operators were invented before the language [i.e. B] was ported to the PDP-11.
[1] Notably, the char type, since the PDP-11 was byte addressable but the PDP-7 was not.
[2] https://www.bell-labs.com/usr/dmr/www/chist.html
[3] https://retrocomputing.stackexchange.com/questions/8869/have...
"char" is a useful type for the program's problem domain regardless of whether the machine is byte-addressed or not. Pascal was developed on machines that were not byte-addressable but always had a char type. This is easily visible in the PACK and UNPACK standard functions and the PACKED keyword for arrays and records. It is slow to do access to a random char from an array packed several to a word, so there are standard functions to copy everything from a packed array to an array that uses one word per item for easier processing, and back again.
If anything, you need the compiler to support the char type more if you don't have a byte-addressed machine because it's such a pain for the programmer to deal with themselves.
Standard Algol 60 didn't have "char" but some manufacturers added it non-standard. Algol 68 had char. These ran on primarily word-addressed machines too, as was the fashion in those days.
Though a general purpose machine it had instructions specifically designed for low cost Lisp implementation, arbitrary numbers of stacks etc…all inaccessible to C
Algol68 allowed char to be as small as 6 bits, the size on the ICL 1900 it was first developed on (with a 24 bit word size).
Pascal was first implemented on the CDC 6600, with 60 bit words and characters again 6 bits. The first other machine Pascal was ported to was, again, the ICL 1900.
does this mean that because you can’t be sure if two vectors don’t overlap in C you therefore can’t auto-vectorize loops?
Ha! As if C compilers really cared about correctness. This is why type-punning and aliasing memory through different typed pointers is fraught with peril, often UB. It's so C compilers can cheat and use type-based alias analysis, breaking programs that have "UB" but would work absolutely fine if the compiler wasn't so aggressive.
In general, almost all arguments between language design and optimization for C/C++ are settled with "let's not think too hard about how to help users; go ahead and optimize this and make it their fault (UB) when they observe the hard cases". It's so rude.
C is especially awkward here, because compilers are expected to precisely conform the C specification, but at the same time C users don't actually want to program for the abstract machine from the C specification (the one that can't overflow signed integers or shift by a full int width). C users expect their programs to actually behave like programs for a particular hardware they target, without any overhead or discrepancy from "emulating" the imaginary C-spec machine. In face of such conflicting requirements, C compilers will always be forced to balance performance, predictability, conformance, and compatibility.
Once instruction sets got features such as index (or base) registers and register indirect addressing (including stack addressing) instead of absolute addressing (including for indirect jumps/subroutine calls) there was no longer any need to support self-modifying code during execution of the program itself, and Von Neumann architecture is not required except for initial loading of the program code into memory by the OS.
I'd think most modern instruction sets are easily capable of running with code and data in truly different address spaces.
The only real difficulty is in loading constant data and especially constant tables from program space. Microcontroller ISAs such as AVR have special instructions for program space loads. Any ISA where the PC is a GPR (PDP-11, VAX, arm32) or that has an explicit PC-relative addressing mode (preferably indexed) such as 68000 make it easy to detect that a memory reference is PC-relative and do it in the program space instead of the data space.
Arm64 with ADR and ADRP and RISC-V with AUIPC make it tricky because hardware would have to track that a GPR contains a pointer derived from the PC. x86 and PowerPC make it even more difficult, because the only way to get the PC value is to do a fake function call to the next instruction (or to keep return stack prediction happy, to a real function that just saves the return address then returns). On these ISAs you're probably better off assembling all constants using load immediate and shifts, and arrays or tables of constants using computed jumps to load immediate instructions.
In short, using a conventional modern ISA with Harvard architecture is doable if you want to.
Examples of non-pdp-11 things are discussed in various comments in this thread.
When you're in a C straightjacket it's hard to take advantages of other architectures like transputer, connection machine, or cell. Ever run bsd unix on a Cray? It was dog slow because the CPU assumed very deep pipelining and always had to continually resynchronize because of the frequent branches in the C code.
GPGPU is the sole recent exception, and even then it's still at the margins. And we have CUDA which is an attempt to, yes, get back to the C model.
These modern CPUs benchmark pretty well, but in practice little code can really take advantage. For example the M1 is a multicore screamer, yet most apps are still restricted to a single one.
CUDA allows different programming models, it's the point of having the PTX virtual machine as the exposed programming model.
CUDA C++ is popular, but is far from the only option people have.
(and it can go very deep, https://www.ibm.com/docs/en/sdk-java-technology/8?topic=egpu... for example)
FORTRAN has tighter restrictions around what you can do in terms of aliasing/etc, which allows compilers to be more aggressive about optimizations without as much undefined-behavior "this looks correct and will run fine until we add another optimization and it doesn't" shenanigans. So in practice it can be faster than C, and it's actually quite popular among the HPC community and other performance-sensitive segments.
It's not like C has to be this way, with all the "undefined behavior" nonsense, it just is an artifact of a programming language written in the early 70s. C makes a lot of sense when a compiler looks a lot more like an assembler with a bit of optimization sprinkled in, and isn't performing super deep introspection and deciding that this entire function can be optimized away because of some arcane "undefined behavior" rule.
I'd say that the most popular option at the end-user level today is a whole level of abstraction above: using Python w/ PyTorch (or less frequently, TensorFlow).
Combined with CuPy and cuNumeric, which is available at https://developer.nvidia.com/cunumeric and handles scaling to multiple machines with Python-written code, through the Legate runtime (https://nv-legate.github.io/legate.core/README.html). This covers a huge amount of what the CUDA userbase uses.
That's why you have things like memory order constraints on atomic operations in C, and why you need to be extremely careful with how you place things like memory fences when doing anything fine-grained between several threads.
Generally, most programmers shouldn't need to be reasoning about OOO or memory order and be using fences. Rather, they should use libraries that are written by experts that use extra-lingual mechanisms (like compare-swap or fences) to guarantee a higher contract.
I mean, you're basically saying that it's only observable if you care to observe it.
I seem to recall that there was a mainframe back in the 1970s that actually did that. Univac 1108 or 1100, maybe? Maybe only in some particular mode?
The point of designing a new architecture is you have a point to make (generally, "doing X will lead to faster execution"). So by definition you are adding unfamiliar architectural weirdness, else why get involved.
The big problem is that the pervasiveness of the C model has fossilized design decisions of the PDP-11 that still haunt us to this day. For example a lot of transistors are devoted to hiding the effects of the micromachine (superscalar implementation, OOO execution etc). Newer paradigms (even trivial older ones like vector instructions) don't get used enough because of higher level language constraints (e.g. C's memory aliasing model). There's a lot of innovation going on but it's all stick in one small bay of a large lake.