What's New in CPUs Since the 80s and How Does It Affect Programmers?
danluu.com
danluu.com
GPU memory (GDDR5) has also many times more bandwidth at expense of 10x higher latency compared to CPU.
Currently you put the workload that requires low latency on a CPU and embarrassingly parallel "non-branchy" workload on a GPU.
Both CPUs and GPUs gain about 20% performance per year per core. I'm no expert on CPU and GPU memory architectures, but both seem to be headed towards integrated 3D-stacked eDRAM memory. This can provide up to several TBps reasonably low latency bandwidth.
Even for ray tracing, I think you'll get better results on a modern GPU. Each individual processing element may not be anywhere near as good at dealing with the constant branch prediction failures inherent in ray tracing algorithms as modern CPU cores are, but there are thousands of them for every CPU core you have, and millions of completely independent samples to compute in a raytraced image. But I suspect that by the time that realtime raytracting becomes viable, the HSA approach I outlined above will be viable as well, and that will be the way to go.
This makes SIMT easy. SIMT is "Single Intstruction Multiple Threads" which is sort of like SIMD but better. You have an instruction come in which is distributed to multiple execution lanes each of which has a hopper of instructions it can draw from. Each lane then executes those instructions independently and if the lane notices that the instruction has been predicated out it just ignores it. Or maybe if one lane is behind it can give the instruction to its neighbor which isn't. THe fact that you don't have to be able to drop everything in a single cycle and pretend you were executing perfectly in order gives the hardware a lot of flexiblity, and all of this complexity is only O(n) with the number of executions instead of O(n^2) as with a typical OoO setup. The need to have precise exceptions involving SIMD instructions means that this isn't a simple thing for a regular CPU core to add to its SIMD units.
The fact that each lane is making decisions about when to execute the instructions it has been issued are why some people refer to the lanes as "cores". I don't, because what they're doing isn't any more complicated than the 8 reservation stations in a typical Tomasulo algorithm OoO CPU core would be doing even if they are smarter than a SIMD lane. With GPUs it makes more sense to break down "cores" by instruction issue, in which case a high end GPU would have dozens rather than thousands of cores.
> Each lane then executes those instructions independently and if the lane notices that the instruction has been predicated out it just ignores it.
This is exactly what you do on CPUs as well. In SSE/AVX you often mask unwanted results instead of branching. Just like on GPUs. AVX has 8 lanes, 16 with Skylake.
It's certainly true that SIMD instructions in CPUs have predication which saves you a lot of trouble. The difference is that if you have two instructions which are predicated in a disjoint way you can execute them both in the same cycle in a SIMT machine but you would have to spend one cycle for each instruction in a SIMD machine. You can look at Dylan16807's link for all the details.
Batches of execution units all run the same opcode (or skip it in if/else constructs) but each one has its own register set.
As opposed to SIMD's simple, single, massively-parallel vector instructions.
Ray-tracing doesn't really buy much unless you're doing some specific subset of effects. Rasterization covers 99% of cases with higher efficiency.
GPU cores are tiny because the problems they deal with are "embarrassingly parallel", trivially solved by throwing more cores at the problem. You make the cores as simple as possible so you can have thousands of them on a chip. Modern GPUs don't even have SIMD units per core any more; both NVidia and AMD are completely scalar now. You'd think that graphics would be the perfect scenario for SIMD, since shaders spend so much time dealing with 3 and 4D vectors, transformation matrices, colors, and so on, but it worked out that the gain in throughput and instruction density per-core was outweighed by the power, heat, and die cost of having parts of those thousands of SIMD units sitting idle while working on data that doesn't take up a whole SIMD register. And because context switches are much rarer on GPUs than CPUs, they can have extremely deep pipelines that push compute efficiency even further, at the expense of context switch latency.
CPU cores are, if I may, "fuckhuge", because by and large they can't solve their problems by throwing more cores at them. They take on problems that are inherently serial and branch heavy, like compiling a program or optimally compressing a large file, and throw bigger cores at them (out-of-order execution, branch prediction, speculative execution, multiple ALUs per core, instruction schedulers that exploit the parallelism hidden in the serial instruction stream, large register files only visible to the microarchitecture, etc) while maintaining a fairly short pipeline so that branch prediction failures and context switches don't take too long to recover from. SIMD fits in well here, because the cost of bigger and bigger ALUs is pretty much insignificant compared to all the other hardware that goes into a high-end CPU core. It can be a pain to optimize for, but it's great for middle-ground tasks that need both heavy parallel and serial/branch-heavy computing resources with little latency between the two, like video compression.
So you can't make one as good at solving the tasks of the other without making it worse at the task it's already intended to do. They tried to do this with Larabee, which was, no exaggeration, a few hundred Pentiums on a chip. Very interesting, but unsuccessful in the market, because it was an expensive niche product that was worse than either a CPU or GPU for the majority of their respectful workloads. What's really interesting, however, is the direction that AMD is taking. Rather than pushing SIMD like Intel, they're trying with their "heterogeneous systems architecture" movement to break down the communication barrier between CPUs and GPUs so that sharing data between the two is as easy as passing a pointer. SIMD is still great on CPUs for dealing with higher level primitives like vectors, matrices, or image macroblocks, but I can easily see the aforementioned middle-ground shifting to CPU threads working in close concert with GPU worker threads, as opposed to the mostly hands-off, one way street common in i.e. 3D rendering today.
[1] On second thought, this didn't turn out to be much of a summary, did it?
The answer is that it mostly makes sense except you may be tricked by #defines for example. Thus you could probably "optimistically parse" small chunks of the source code, all in parallel, with the understanding that you may have to throw out some intermediate results in light of new understandings.
This is quite useful for non regular problems that are otherwise difficult to parallelize.
I think something should be called "core" if it can branch instruction stream. Predication doesn't count. A single core can run multiple instruction streams (GPUs, Intel Hyperthreading).
I agree. In the past I posted a decent (if I do say so myself) introduction to hyper threading and SIMT here: https://news.ycombinator.com/item?id=8245360
I simplified and just said "core" in this post because it was getting a bit long in the tooth already.
This is factually incorrect. Let me quote Vasily Volkov [1]: "Earlier GPUs had a 2-level SIMD architecture — an SIMD array of processors, each operating on 4-component vectors. Modern GPUs have a 1-level SIMD architecture — an SIMD array of scalar processors. Despite this change, it is the overall SIMD architecture that is important to understand."
[1] http://parlab.eecs.berkeley.edu/sites/all/parlab/files/LU,%2...
...and now I read your paper, and I need to eat humble pie, because it apparently predates NVidia's use of the term "SIMT." Apparently, even if it makes more logical sense, SIMT is the buzzword!
Either way, I think it's a stretch to say I'm "factually incorrect" just because I used different terminology. Whether you think of it as an "SIMD array of scalar processors", or (to use NVidia terminology) an "SIMT warp of scalar threads" the important thing here is that the execution units in the bundle with the shared instruction pointer operate on scalar values now, rather than 4-element vectors.
Initially it seemed to me the theoretical minimum should be 10000 (as the practical minimum seems to be). But it does indeed seem possible to get 2:
1) Both threads load 0
2) Thread 1 increments 9999 times and stores
3) Thread 2 increments and stores 1
5) Both threads load 1
5) Thread 2 increments 9999 times and stores
6) Thread 1 increments and stores 2
Is there anything in the x86 coherency setup that would disallow this and make 10000 the actual minimum?
On the other hand, 20000 is unlikely as well, if the threads have any concurrency at all.
If you could manipulate your two cores with enough dexterity, you could force a 2 result, but without enough fine-grained control, the practical limit is going to be determined by the number of increments that can be "wasted" by sandwiching them between the load and store operations of the other thread. You would essentially need to stop and start each CPU at four very specific operations.
The practical results show that "load + add + store + load + add + store" on one thread probably never happens during a single "add" on the other thread. You would need that to happen at least once to get below 10000. Otherwise, each increment can waste no more than one increment for the other thread, and you end up with at least 10000.
The experimental numbers are probably indicative of how long the add portion of INCL takes in relation to the whole thing.
The example that the author gives does not apply to x86.
[1]There are memory types that do not have strong memory ordering, and if you use non-temporal instructions for streaming SIMD, SFENCE/LFENCE/MFENCE are useful.
Do you have a reference for the strong memory ordering on x86? I'd like to read more about it.
http://bartoszmilewski.com/2008/11/05/who-ordered-memory-fen...
http://preshing.com/20120515/memory-reordering-caught-in-the...
This whole area is very touchy and easy to get wrong. Further reading:
https://software.intel.com/en-us/articles/tsx-anti-patterns-...
[1] http://www.ece.cmu.edu/~ece447/s13/lib/exe/fetch.php?media=0...
None of them are exposed on ANSI C.
Hence why even if C looks like a portable high level Assembler, there are many modern CPU features not available.
Even most common extensions just cover part of them.
Not all titles do this, since most titles are cross-platform and you're not going to do tons of optimization for a specific one unless there is a huge payoff.
Instruction set extensions (which I believe might be what you were thinking of) are used via intrinsics and assembly programming by pretty much any performance-oriented code. For example look at the dozens of architecture-specific implementations of glibc functions: x86-64[0], arm[1].
[0] https://sourceware.org/git/?p=glibc.git;a=tree;f=sysdeps/x86... [1] https://sourceware.org/git/?p=glibc.git;a=tree;f=sysdeps/arm...
The headers I linked to here: https://news.ycombinator.com/item?id=8873764 Cover most useful Intel CPU extensions. And you can use them easily in code, it just sucks when you have to do CPU feature detection and have different code paths for different CPUs.
In the Linux kernel they went so far as to actually replacing instructions at boot-time at known locations, based on the capabilities of the CPU the code is executing on. This prevents them from maintaining many paths in the compiled code.
From the Linux kernel:
struct boot_params boot_params __attribute__((aligned(16)));
#if !__GNUC__
#define __attribute__(x)
#endif
I think that's a better way to do it than ASM (which totally hoses your portability).Some of them are no different than just writing plain Assembly.
#include <mmintrin.h> // Intel MMX
#include <xmmintrin.h> // Intel SSE
#include <emmintrin.h> // Intel SSE2
#include <pmmintrin.h> // Intel SSE3
#include <tmmintrin.h> // Intel SSSE3
#include <smmintrin.h> // Intel SSE4.1
#include <nmmintrin.h> // Intel SSE4.2
#include <ammintrin.h> // Intel SSE4A
#include <wmmintrin.h> // Intel AES
#include <immintrin.h> // Intel AVX
#include <zmmintrin.h> // Intel AVX-512
#include <arm_neon.h> // ARM NEON
#include <mmintrin.h> // ARM WMMX
And a full list of what is possible in Microsoft Visual C: http://msdn.microsoft.com/en-us/library/hh977022.aspx
Now the reason why they are not in ANSI C is that low-level CPU features (just like raw instructions) are not portable by nature.
Additionally not all C compilers provide such headers.
http://msdn.microsoft.com/en-us/library/hh977022.aspx
I do not think one has full control over OOO execution on x86-64 processors. Also I do not believe one has control over the execution pipeline even in assembly, although I do not know exactly what you mean by that, so it could just be a misunderstanding.
With NUMA physical memory ordering gets even uglier, usually each physical socket's memory is in a big chunk, but sometimes it's also interleaved every 4 kB.
http://web.stanford.edu/group/comparch/papers/huggahalli05.p...
http://www.anandtech.com/show/8147/the-intel-ssd-dc-p3700-re...
Can someone explain the scenario where this test would result in _foo = 2? The lowest theoretical value I can understand is foo_ = 10000 (all 10000 incls from thread 1 are executed between one pair of load and store in thread 2 and hence lost).
1. Both threads read '0', beginning their first incl instruction (A: 0, B: 0, Store: 0)
2. Thread A completes execution of all BUT ONE incl, writing it's second to last value to the store (A: 9999, B: 0, Store: 9999)
3. Thread B increments it's '0' to '1' (A: 9999, B: 1, Store: 9999)
4. Thread B writes it's '1' to the store (A: 9999, B: 1, Store: 1)
5. Thread A begins it's final incl and reads the '1' that thread B just stored (A: 1, B: 1, Store: 1)
6. Thread B executes all remaining incl instructions, writing its final value to the store (A: 1, B:10000, Store: 10000)
7. Thread A continues it's final incl and increments it's '1' to '2' (A: 2, B:10000, Store: 10000)
8. Thread A stores it's final '2' result (A: 2, B:10000, Store: 2)
9. All instructions have executed, and the result is '2'
It is, of course, completely theory and extremely unlikely under real use as the authors graph shows, but it's one of those threading 'gotchas' that lead to occasionally unpredictable results.
However just a selected few could touch said hardware and you could buy a very nice house with what they used to cost.
On the other hand, programming complexity has gone way up, while predictability of performance has been lost.
#1: Fit in cache.
#2: Try to multithread
all the rest is marginal