14 ms·
Memory bandwidth
- ant6n 10y ago"Code that runs OK-ish on that CPU averages around 1 instruction per cycle, well-optimized code around 3 instructions per cycle." Is unoptimized code really that bad? I thought between modern compilers and supporting 4 op/cycle with out-of-order execution, a new Kaby Lake cpu would get more than 1 cycle overall. Or are the branch delays just killing the performance? (Code is small relative to data, most of it should be in cache most of the time)
- deleted 10y ago[deleted]
- inetknght 10y agoYes, unoptimized code is really that bad. Moreover, optimized code is able to do more with less (by not repeating itself). Unoptimized code sits and does the same thing over and over. Then there's parallelism (threads and, in particular, SIMD); use right, it works great. Use wrong and it only expands your memory bandwidth requirements (and kills your effective instructions per cycle).
- ant6n 10y agoThe author wrote "OK-ish" code. I think code that is OK is one that was compiled with -o3, like basically all code in release mode. 'optimized' in this context should mean somebody spent time to make it faster. In this discussion, doing something 'over and over', or doing less with more is a consideration at a higher level, and shouldn't play into this. This is about instructions per cycle, so being able to only do one instruction per cycle on average should only be due to continuous streams of dependent instructions, waiting on branches and waiting on memory.
- panic 10y agoIntel CPUs run one micro-op per execution port per cycle (see section 2.3.4 of http://www.intel.com/content/dam/www/public/us/en/documents/manuals/64-ia-32-architectures-optimization-manual.pdf http://www.intel.com/content/dam/www/public/us/en/documents/... for details, though the manual is a bit out of date -- more recent CPUs have more ports: http://www.anandtech.com/show/6355/intels-haswell-architecture/8 http://www.anandtech.com/show/6355/intels-haswell-architectu...). Optimized code has to be written with execution port utilization in mind.
- ant6n 10y agoI'm not disputing that ... I just thought that out-of-order execution was supposed to deal with that to an extend, with tens to even hundreds instruction in flight at a time.
- gpderetta 10y agoThere are so many execution ports compared to the throughput of decoding, renaming and retiring, that they are rarely the bottleneck. As ant6n correctly pointed, memory latency, branch prediction, dependency chain length, decoding and renaming are, in that order, normally the bottleneck even for hand optimized assembler code.
- vardump 10y ago-o3? I'm assuming you mean gcc -O3. Not enough by itself. The code will likely be pretty bad, unless you'll also add -march=native.
- Jweb_Guru 10y ago> Is unoptimized code really that bad? Absolutely. Compilers can't do much about poor locality and most code is not written with locality in mind. And in many cases locality isn't really much of an option anyway; for instance, think about a key-value dictionary much larger than cache, responding to random requests. People have contrived ways of trying to make these cache-friendly but they generally end up slower than a really well-optimized hash lookup. You can improve it further in some cases by using prefetching extensively and trading memory bandwidth for latency, but as the article points out that can actually be a really terrible idea. So without further knowledge of the workload there just isn't a lot you can do.
- fulafel 10y agoTo refine: popular compilers don't do much about poor locality. It's very possible do this but you need the language to permit it and the compiler engineering done. Data optimizations have for some reason received too litlte attention for a long time. For example, most key-value dictionaries could shave off 50-90% of their memory use if the value ranges and key counts were optimized for, along with unboxing.
- deleted 10y ago[deleted]
- dom0 10y ago> Is unoptimized code really that bad? Most application code is far worse and stalls (sadly - almost literally) every few instructions for L2 / memory. > (Code is small relative to data, most of it should be in cache most of the time) Not if your application is dragging two and a half invasive runtimes around.
- socmag 10y ago> Not if your application is dragging two and a half invasive runtimes around. Cheers to that :/
- jandrewrogers 10y agoSaturating the execution ports in a modern CPU is something compilers demonstrably have a difficult time doing automatically because the the code involved in this optimization has poor locality by definition. Compilers tend to have a bit of tunnel vision to a very local context so that operation relationships are easily analyzed. Optimizing this requires detecting patterns in scattered parts of the code and reorganizing the code in a functionally neutral way. Not difficult to do but the human programmer has to do the work because it requires a higher level view of code sequencing relationships. It doesn't produce code that is any less obvious or readable in my experience. Even on older Sandybridge CPUs, I often saw upwards of 2x throughput by mildly reorganizing perfectly correct C++ for better operation concurrency. Writing code that is well-optimized in this regard becomes natural with awareness and experience. Optimizing cache line hit rates has an even higher throughput improvement than execution port saturation but is a similar optimization.
- dom0 10y agoI don't think it's even possible any more. Haswell and later are 8-wide designs, but can only fetch+decode and retire 4 instructions per cycle (peak). So one would need ops that decode to 2+ execution-unit independent µops. Linear data-dependent algorithms are also such a huge pain: "Why does that only shovel 500 MB/s?" Well... five instructions long dependency chain, that's why! (Yes, the last bit may be a bit unrelated to the discussion at hand)
- kbwt 10y ago> 8-wide designs, but can only fetch+decode and retire 4 instructions per cycle That's where the Trace Cache helps in tight loops.
- dom0 10y agoThe µop cache (trace cache went out the window) doesn't help with fetching instructions AIUI, but accelerates decoding them. So the 4 insns/cycle fetch bandwidth is not changed by that.
- 10y ago
- fulafel 10y agoPut another way, he's saying if the dataset doesn't fit in cache, it's bottlenecked on memory rather than the CPU's computational resources. It might still be good code. Transforming an arbitrary computational task to a perfectly balanced "n operations of computation per n bytes of data accessed" suited to hardware du jour is a not a generally solved problem, as much as programmers like to talk about memory/speed tradeoffs.
- ant6n 10y agoI understand the overall point of the article is about memory blocking cpu; but the author uses the idea of instruction through-put instruction-memory/cycle a way to show that the throughput isn't enough just to execute code. So in order to make that argument, you'd have to look at the instruction through-put without the assumption of memory accesses slowing you down. Otherwise the point comparing cpu speed vs memory speed is moot, if you start with the assumption that cpu speed is bound by memory speed to begin with.
- jfoutz 10y ago> Transforming an arbitrary computational task to a perfectly balanced ... I think that's halting problem level difficulty there. We can probably do neat things for special cases, but i seriously doubt it's generally solvable.
- gvb 10y agoI thought between modern compilers and supporting 4 op/cycle with out-of-order execution, a new Kaby Lake cpu would get more than 1 cycle overall. Or are the branch delays just killing the performance? The problem at the level of how many operations per cycle is data dependencies. A lot of code is assignments to variables and subsequent use of those variables in calculations, etc. Simplistic example: a = 1; b = 2; c = a + b; The assignment to variable "c" stalls until "a" and "b" assignments are completed. Obviously, in real life the assignments of "a" and "b" would generally be other complicated assignments that are dependent on other variables, functions, calculations, etc. The x86 is additionally hampered by its relative lack of registers. This is hidden to some extent by using register renaming so that the CPU actually has multiple instances of register e.g. AX and tracks the dependencies. Ref: https://en.wikipedia.org/wiki/Register_allocation https://en.wikipedia.org/wiki/Register_allocation https://en.wikipedia.org/wiki/Register_renaming https://en.wikipedia.org/wiki/Register_renaming
- ant6n 10y agoSkylake has up to 224 instructions in flight [1], apparently on multiple branches. This compares to your 3 instruction example. I think it's a valid question whether this doesn't imply we get more than 1 instruction/cycle, even for code that isn't hand-optimized (i.e. just usual code optimized by the compiler), assuming that the work load isn't data-memory bound. [1] http://www.anandtech.com/show/9582/intel-skylake-mobile-desktop-launch-architecture-analysis/5 http://www.anandtech.com/show/9582/intel-skylake-mobile-desk...
- gvb 10y agoYou will get the number of instructions in parallel that is the minimum of (a) the number of instructions that can be handled by the hardware simultaneously and (b) the number of instructions that don't have inter-dependencies. My example was not a 3 instruction example. It was a minimum example of data dependencies. In the example, until the CPU can resolve what the value of the variable (register) "a" is, it cannot do the calculation for the variable "c". Most programs have a high level of data inter-dependencies. Conditional branches are major class of data inter-dependency - the CPU cannot execute code beyond the branch[1] until it knows which branch it will take. There is a class of programs that are "embarrassingly parallel" that having more execution units will speed them up, ideally linearly (graphics GPUs are a classic example). "Garden variety" programs end up under utilizing execution units because the execution unit assigned to calculate "c" (from my example) will be stalled waiting for execution units assigned to calculate "a" and "b" to complete their calculations. This is related to the long unrealized, and still unrealized, promise of VLIW architectures. https://en.wikipedia.org/wiki/Very_long_instruction_word https://en.wikipedia.org/wiki/Very_long_instruction_word [1] A small-ish lie: many processors nowadays do "speculative execution" where they guess which branch will be taken and execute ahead. If they guess right, they win. If they guess wrong, they throw away the work they did and restart the execution on the other branch.
- PaulHoule 10y agoMemory bandwidth holds back a number of "revolutionary" advances in computing, both in the sense of FPGAs, ASICs, and new processor types, not to mention Indium Phosphide parts that clock at 50 GHz and could go to 200 or more.
- luckydude 10y agoWhat a refreshing article. This guy gets it and put things into perspective in a way that you (or at least I) don't see very often. Worth a read if you are just scanning the comments.
- KaiserPro 10y agoOne Interesting quirk we found with Nvidia cards was the CPU-GPU transfer speed was affected by how many physical CPUs you had. I assumed it was because with newer intel CPUs the PCI bridge moved to the die, away from the motherboard, so there was CPU affinity for the GPU. The difference in bandwidth was around 15%
- dom0 10y agoThere is also a latency penalty for accessing PCIe devices that are attached to a foreign root complex. This is exacerbated if the QPI links are already busy.
- zbjornson 10y agoWhich way was the association, more CPUs -> faster CPU-GPU transfer, or slower? By "physical CPUs" I assume you mean cores in a single socket, but in pre-Haswell multi-socket systems, the uncore clock speed was tied to the core clock speed, so idle CPUs that enter a low power state can actually cause horrible performance because cache coherence snoop signals have to wait for the low-power/slow CPUs to respond. See https://software.intel.com/en-us/forums/intel-moderncode-for-parallel-architectures/topic/379378 https://software.intel.com/en-us/forums/intel-moderncode-for...
- KaiserPro 10y agosorry, yes, that was the most important part! single cpu was faster than dual. This was a xeon x56 vs an e5-26 On the z620 the second proc was on a removable daughter board, so it was simple test. on the e5-26 it was most pronounced, the x56 was less obvious
- qb45 10y ago> I assumed it was because with newer intel CPUs the PCI bridge moved to the die, away from the motherboard, so there was CPU affinity for the GPU. Actually, the PCIe controller doesn't even have to be on-die. The same issue existed in earlier systems where PCIe was in the northbridge, which was attached to the first socket. Sometimes other sockets had their own NBs and own PCI buses too and things were even more interesting. I heard years ago about people pinning their GPU-accelerated applications to the "right" CPUs.
- pqr 10y agoSee also Roofline Model https://en.wikipedia.org/wiki/Roofline_model https://en.wikipedia.org/wiki/Roofline_model and Memory Wall https://www.google.co.in/search?q=memory+wall+computer+architecture https://www.google.co.in/search?q=memory+wall+computer+archi...
- socmag 10y agoOh nice articles
- nortiero 10y agoThe article is very optimistic about memory availability per cycle, reality is way worse. As an example, on my Macbook Air 2011 with ~10 GB/s of maximum ram bandwidth, random access to memory can take 100 time more than a sequential one. This in C, with full optimizations and using a very low overhead read loop. Using the same metrics of the author: best case: ~ 3 bytes per cycle (around 6 Gigabyte per second of available bandwidth) worst case: ~ 0.024 bytes per cycle (every scheduler, prefetch, already open column mostly defied) Note that worst case uses 10 seconds (!) to read and sum in a random way all the cells of an array of 100.000.000 of 4 byte integers, exactly once. Main loop is light enough not to influence the test. That's about 40 megabytes per second out of 6.000 available. What can I say.. CPU designers are truly wizards!
- rayiner 10y agoRight, the article focuses on bandwidth while in many cases it's about latency of dependent memory operations (which has improved by even a lower factor than what's described in the article). Your typical pointer chasing code won't come anywhere near the maximum memory bandwidth mentioned in the article (50 gb/sec).
- logicallee 10y ago>worst case: ~ 0.024 bytes per cycle (every scheduler, prefetch, already open column mostly defied) >That's about 40 megabytes per second Wow, that is an explosive conclusion. It's very hard for me to come to terms with. 40 MB per second is the sustained read of a spinning platter hard drive http://hdd.userbenchmark.com/ http://hdd.userbenchmark.com/ (click any line)† (Did I say 40? I meant 160 MB/sec...) So more like 1/4 of the sequential read speed off of a spinning platter of rust. Ten seconds is insane. I don't care how many times you're bouncing back and forth and invalidating caches and pipelines and prefetches and schedulers, you simply shouldn't be able to ruin things that badly. It is off from what I would expect by (easily) an order of magnitude. I know you say that the main loop is very light - but aren't there other aspects to your build system and operating system that might be affecting this test? To say something very obvious, couldn't the Operating System scheduler not be giving your process the appropriate number of cycles? There is a lot more that I could say in this direction but let's just do something simpler: -> Could you try your experiment without an operating system? For example here are some people who booted a raspberry pi without an operating system - https://www.google.com/search?q=chess+without+an+operating+system+raspberry+pi https://www.google.com/search?q=chess+without+an+operating+s... Perhaps before going that far you could simply boot into a Linux image that was simply not compiled with any hardware support to do anything. (After all you really don't need to do anything except return to the shell.) Or simply see what happens if you boot into Linux and try it. If you get an instantly different result simply booting Linux on the same hardware then you instantly have an explosive blog post: "summing 100k 4-byte integers randomly takes 10 seconds on Mac OS X but only 1 second under Linux". I realize there is a HUGE difference (HUGE) between sequential and random. But I just wanted to get across how insanely slow 40 MB/second straight to RAM is. That should not be possible, no matter how much you defy caches and scheduling and so forth, unless you get the Mac to swap pages out of RAM onto an SSD or something! So not using an operating system would really help here. Could you try it? I'm not saying I don't believe you but - wow, that is insane. † I just noticed you wrote "Macbook air 2011". If you want to look at 2011 hard drive speeds, a quick glance still sees some quoting 140 MB/sec so it still seems correct to me, but I just quoted 2017 figures.
- socmag 10y agoReally great article and comments :) Just as a point of reference, I currently see around 1.25 - 1.5 op/cycle on carefully crafted highly parallel lock-free, stall-free code... say running on 8 threads. Code that has 0.01% branch misprediction. Unfortunately in my case as others mention, access to the I/O ports and memory latency, is a real limiting factor. The CPU is just... waiting Getting to the Holy Grail that Ryg talks about of 3 instructions per cycle is really hard with non-vectorizable workloads - like screwing around with hash tables that have no chance of fitting in L1/L3, and not being able to really make much use of SIMD, even if you are paying attention to cache-lines. Most apps barely scrape by at 0.5 instructions/cycle or worse and spend most of the time bouncing on the kernel for stupid stuff. Not good. Absolutely <3 performance freaks!
- socmag 10y agoOne thing I'd love to see is to have enough L1 data cache per core for a modest amount of stack space. Not so critical on GPU's but would make a huge difference for CPU's and languages that can take benefit. Give me a meg or two to play with. Would make a huge difference for data heavy workloads. You could even go as far as having a separate cache just for stack. I mean, by its very definition it is isolated. It's the "register file" of CISC machines
- nhaehnle 10y agoI don't think that idea makes sense. A lot of stack space is unused at any particular time. There's not a one-size-fits-all partition that makes sense, and it's not something that programs could easily tune either. Some obvious stack-related ideas that are worth exploring IMHO are (1) using the stack pointer to guide the prefetcher, to run ahead of cache misses when you return from subroutines and go up the stack; and (2) having an instruction that helps avoid unneeded cache misses when going down the stack; for example, a dedicated instruction for decrementing the stack pointer which zeros entire cache lines directly in L1 without loading them. Or perhaps, for more generality, an instruction for marking any arbitrary address range in this way. Who knows, at least the first one might already be in use in some microarchitectures!
- socmag 10y agoYes I agree that a lot of stack space goes unused right now, that is for sure, but the nice thing about the stack it is definitely thread/core local, and if you did have fast-stack capability we would definitely use it. What I was thinking about was a separate bus to a per thread stack space with no competitors. That seems interesting, and I don't think it would be that hard to add. I really like your ideas there, especially number 1, that's very smart. Number 2 is clever as well. > Who knows etc.. ORLY? Good to know.
- adrianmonk 10y agoIt's good to see some attention paid to the subject, but it's not exactly a new revelation. Probably 15-20 years ago, my computer architecture professor commented that, while CPUs of the past had relatively anemic number-crunching powers, all the pipelining and high clock speeds and other advancements in more recent CPUs had changed that, but corresponding advancements in memory bandwidth had not been made. Which in turn meant that the way you go about optimizing code needed to change. In the past it had been mostly about finding ways to eliminate instructions or simplify expressions, because the things that were holding you back were the ALU and the ability to plow through instructions. With all those things sped up massively but less improvement in RAM, it became very important to start thinking about memory access patterns and caches. We even had a homework assignment to optimize a matrix multiply, and the lesson learned was that the dominating factor wasn't what the code looks like the innermost loop, it was which direction you proceed through the matrices (row by row vs. column by column) because that determines memory access patterns.
- xchaotic 10y agoShould we design the processing units differently then? There's been talk about hundreds of cores, all with local memory, but this will only speed up a certain subset of computing problems...
- spullara 10y agoI remember when DRAM "wait states" were 0, 1 or 2. They aren't advertised anymore because they are 1-2 orders of magnitude worse than that now. https://en.wikipedia.org/wiki/Wait_state https://en.wikipedia.org/wiki/Wait_state
- pslam 10y ago> Note that we’re 40 years of Moore’s law scaling later and the available memory bandwidth per instruction has gone down substantially. This is unfair, and (this is me being unfair now) this article is missing the woods for the trees. There were engineering pressures which resulted in the current ratios. I think it is fairer to say that the current situation — where there's about (hand-waving) 1 byte per instruction of bandwidth per core — reflects the kinds of tasks we expect our machines to be doing. It is very rare to find a task which is memory speed bound. There's almost always substantial processing to be done with data. It's not even that hard to increase memory bandwidth. You "just" double up memory channels. This is of course expensive, which in turn is a back-pressure which results in architectures designed around the current sweet-spot. I'm also puzzled the author thinks the situation is "worse". Pretty much every desktop class machine I've used from about 1990-2005 was extremely starved of memory bandwidth, and cores did a far worse job of hiding latency (OOO renames etc). What we have today feels fairly comfortable, to me at least, with some outlier tasks where you might want more (and then obtain specialist hardware). This is a long-winded way of saying: the current core vs memory speed ratio is a sweet-spot of cost vs efficiency, and works well given the tasks and algorithms we execute on these machines. What we had in the olden days was just a case of unoptimized architecture, which hadn't converged yet.
- vvanders 10y ago> It is very rare to find a task which is memory speed bound. There's almost always substantial processing to be done with data. One could argue that memory speed doesn't matter because memory latency has remained (relatively) constant since the advent of DDR. Can't process something while you're waiting for that cache miss to complete.
- nialo 10y agoI don't think it's worse. The (possibly missing) context of this post is a Compute Shader running on a GPU spending more time writing out the results of a computation than actually doing the computation. As I read this post, the moral of the story is just to try to write your code in such a way as to always have that substantial work to do with the data. Prefer one big pass over everything to several smaller passes, each of which must write out results, especially on a GPU. Try to actually have 11 instructions per byte of memory access. I don't think the intent is to make any argument in particular about the state of CPU or GPU design or anything of the sort.
- white-flame 10y agoA commonly recurring subject on the 6502 forums is what a "modern" 6502 would look like. It always boils down to not really being able to replicate the 1-cycle access to any byte in RAM. Changes to the memory access model requires changing everything to being unrecognizable as a 6502 derivative, in programming style. Of course, you could put 64KB of SRAM on the CPU die, but the size and power of the RAM would dwarf the processor, and you'd get an old-school 6502, arguably not a modern take on the concept. If you want more memory, you simply can't replicate the 1980s access model at anything approaching today's speeds.
- PaulHoule 10y agoI was wondering the other day how it would work if you made something like a 6502 out of indium phosphide with 64k of SRAM and clocked it at 50 GHz.
- qb45 10y agoThis would have to be a tiny core with tiny RAM indeed. At 50GHz your clock signal is 180° out of phase after traveling just 2mm. I wonder if there are any practical applications that could benefit from such a thing.
- PaulHoule 10y agoCommunications and signal processing, maybe.
- white-flame 10y agoFully asynchronous designs that send a local activity pulse along with their data, perhaps.
- slededit 10y agoYou could probably have Page 0 be a user managed cache though - keep it in SRAM directly on the CPU die.
- 10y ago