Feel the cache size: a definitive experiment(melikyan.blogspot.com)
melikyan.blogspot.com
Feel the cache size: a definitive experiment
http://melikyan.blogspot.com/2009/07/feel-cache-size-definitive-experiment.html
7 comments
First, on your assembly output: mine was a 64-bit system, and there's one opcode that's different. Ok, doesn't matter.
Second, the fact that your timing is not a lot different for sizes that fit the cache means your system probably lacks some kind of smarter instruction pipelining which is, I presume, present on mine. To be honest, I have no idea why in my case there was such a notable difference, i.e. from 12 to 45 seconds, while in your case it's 30 to 59.
Second, the fact that your timing is not a lot different for sizes that fit the cache means your system probably lacks some kind of smarter instruction pipelining which is, I presume, present on mine. To be honest, I have no idea why in my case there was such a notable difference, i.e. from 12 to 45 seconds, while in your case it's 30 to 59.
Isn't this 'definitive experiment' just showing that code which doesn't take advantage of locality of reference is bound to be really slow if it is larger than the cache size?
I mean, he's just executing trivial code from random locations in memory - not a typical access pattern at all. Because of this, the experiment exaggerates the effect of code size on performance. For example, if you had a 1 million line program that didn't loop, it's performance would not degrade nearly as much as his results imply.
I mean, he's just executing trivial code from random locations in memory - not a typical access pattern at all. Because of this, the experiment exaggerates the effect of code size on performance. For example, if you had a 1 million line program that didn't loop, it's performance would not degrade nearly as much as his results imply.
Exactly. This experiment shows pretty much the opposite of what it aims to prove. It shows that code bloat is completely irrelevant as long as locality is taken into account in performance critical parts of the code.
The "valuable" lession we can learn from this is this: Never access a million random memory locations in your innermost loop :-)
The "valuable" lession we can learn from this is this: Never access a million random memory locations in your innermost loop :-)
The experiment does exaggarate the impact indeed, but I think there are real-world examples that are pretty close to it. For one thing, some bloated virtual machines. Let the effect of optimization of your VM be 20%, not 15 times, then 20% would be a huge gain you might struggle for if this is the runtime environment for your language.
if you want to know more on this topic
"What Every Programmer Should Know About Memory" - http://people.redhat.com/drepper/cpumemory.pdf
"What Every Programmer Should Know About Memory" - http://people.redhat.com/drepper/cpumemory.pdf
A detailed (maybe too detailed) look at the interaction between Hierarchies of Memory and CPUS. Memory Systems: Cache, Memory, Disk [ http://www.amazon.com/Memory-Systems-Cache-DRAM-Disk/dp/0123... ]
Over the top, but fun:
Unix Systems for Modern Architectures
http://www.amazon.com/UNIX-Systems-Modern-Architectures-Mult...
http://www.amazon.com/UNIX-Systems-Modern-Architectures-Mult...
Should this be encrypted? My computer says it is.
The massive difference in random access speeds in comparison to sequential speeds is the thing that changed in the last 15 years. The gap between memory clock speed and CPU clock speed has been increasing for all this time, whereas prior to that they were often similar.
The impact is that random access has suffered and cache has become vital in coping with the majority of those seemingly random jumps. Algorithms like quicksort benefit from locality of reference and hence better utilise the cache than algorithms that theorectically do less work.
Its interesting to see it jump like this, but its not indicative of large programs so much as it is indicative of what happens when you randomly access memory and effectively nullify your cache and your memorys DDR properties. Thankfully most programs don't in practice do this, if they did the programs would run about as well as they did in the mid 90's on an original Pentium.
The impact is that random access has suffered and cache has become vital in coping with the majority of those seemingly random jumps. Algorithms like quicksort benefit from locality of reference and hence better utilise the cache than algorithms that theorectically do less work.
Its interesting to see it jump like this, but its not indicative of large programs so much as it is indicative of what happens when you randomly access memory and effectively nullify your cache and your memorys DDR properties. Thankfully most programs don't in practice do this, if they did the programs would run about as well as they did in the mid 90's on an original Pentium.
This is something that's been bothering me for the last decade (okay, more.). Every time I've articulated it in the past I've been dismissed, but it really is key to good performance.
iirc, the GNOME folks once did some benchmarking and found they could get huge speedups by trimming a few bytes here and there from the gnome libraries.
I hope more people take the time to consider the implications of code bloat. I'm not asking for hand written assembler, but just a little bit of thought here and there can produce huge improvements.
EDIT: This is also why ssd's are so exciting. Yes the're not perfect yet, but they promise to (all but) eliminate disk latency within the next few years.
iirc, the GNOME folks once did some benchmarking and found they could get huge speedups by trimming a few bytes here and there from the gnome libraries.
I hope more people take the time to consider the implications of code bloat. I'm not asking for hand written assembler, but just a little bit of thought here and there can produce huge improvements.
EDIT: This is also why ssd's are so exciting. Yes the're not perfect yet, but they promise to (all but) eliminate disk latency within the next few years.
Too true.
"We should forget about small efficiencies, say about 97% of the time" Donald Knuth
We often forget about the rest 3% too.
"We should forget about small efficiencies, say about 97% of the time" Donald Knuth
We often forget about the rest 3% too.
It's a shame that profiling/performance/cache utilization analysis tools aren't nearly as common/standardized as the rest of a developer's toolbox.
Valgrind/cachegrind are fairly common in the UNIX/C world. Our company uses it as part of the automated build/test run.
Sadly they only support: X86/Linux, AMD64/Linux, PPC32/Linux, PPC64/Linux
Which doesn't help me :-(
Which doesn't help me :-(
Side anecdote to your SSD note:
I have a bit of code that steps through a binary file made up of blocks that are of a known size, in an arbitrary order, and come in N different flavors and save them into N output files, one for each flavor. It actually does much more too, but lets pretend this is the only operation. The simple way to do this is to step sequentially through the file and load everything into memory in N different structures and then save your files. But the problem is that sometimes you don't have enough memory to do that, so you have to loop through the file N times, loading a type, saving it, clearing memory and repeating. Since that seems very inefficient, so I thought I'd be clever and on the first pass through the file, load one of the flavors and also save file pointers to all the other blocks so that each subsequent pass could be done more quickly by seeking to the exact black that I needed to load.
Long story short, on regular hard drives, thanks to optimized sequential reads it can actually be faster to just do the dumb thing and loop through the file N times than seek though it randomly.
I have a bit of code that steps through a binary file made up of blocks that are of a known size, in an arbitrary order, and come in N different flavors and save them into N output files, one for each flavor. It actually does much more too, but lets pretend this is the only operation. The simple way to do this is to step sequentially through the file and load everything into memory in N different structures and then save your files. But the problem is that sometimes you don't have enough memory to do that, so you have to loop through the file N times, loading a type, saving it, clearing memory and repeating. Since that seems very inefficient, so I thought I'd be clever and on the first pass through the file, load one of the flavors and also save file pointers to all the other blocks so that each subsequent pass could be done more quickly by seeking to the exact black that I needed to load.
Long story short, on regular hard drives, thanks to optimized sequential reads it can actually be faster to just do the dumb thing and loop through the file N times than seek though it randomly.
This article is not really about the memory cache hierarchy.
It's an excuse to show off the author's exciting new algorithm for computing the value of the integer "max": (func_table[max] - func_table[0]).
It's an excuse to show off the author's exciting new algorithm for computing the value of the integer "max": (func_table[max] - func_table[0]).
Isn't it really calculating max * 11, since the functions pointed at by func_table[max] are 11 bytes long?
I'm not much of a C coder, is there a more idiomatic way to do this?
I'm not much of a C coder, is there a more idiomatic way to do this?
It could be max * 11, yes, but with optimizations turned on the compiler could produce something that wouldn't be that easy to calculate in a more general case. It's just that a file with one million functions is practically impossible to compile with optimizations.
Right. What I meant was that (func_table[max] - func_table[0]) is equivalent to (in the example assembly given) max * 11.
When I asked if there was more idiomatic way to do this, I meant more idiomatic than the original code, not actually using (max*11) in the code.
When I asked if there was more idiomatic way to do this, I meant more idiomatic than the original code, not actually using (max*11) in the code.
Not sure I understand your question, but all kinds of juggling with pointers is I think idiomatic in C.
Since not every one got it: in C, the difference between two pointers of type (T *) is the difference in (byte) addresses divided by sizeof(T).
In other words, pointer subtraction gives an index (of integral type ptrdiff_t, which should be 64bit if pointers are). For all p2 and p1 pointing to the same type, p2==p1[p2-p1].
In other words, pointer subtraction gives an index (of integral type ptrdiff_t, which should be 64bit if pointers are). For all p2 and p1 pointing to the same type, p2==p1[p2-p1].
Not exactly. The standard only defines pointer subtraction between two pointers that point into the same array object. That's not what's going on here. If he had written
((func_table+max) - (func_table+0))
then yes you would just get max. The difference is that (func_table[max] - func_table[0]) is actually (*(func_table+max) - *(func_table+0))
which is subtraction of two arbitrary function pointers. It's not covered by the standard. In this case GCC is just subtracting the two addresses, with no division. His results (and mine above) bear this out. If you compile with -pedantic you'll actually get a warning about it.I'd noticed that I misread the original code and replied to my top-level post.
But that's an interesting point about the standard - perhaps it's intended to allow a smaller sizeof(ptrdiff_t) than sizeof(void *), where no contiguous allocation would be allowed such that you the larger type.
But that's an interesting point about the standard - perhaps it's intended to allow a smaller sizeof(ptrdiff_t) than sizeof(void *), where no contiguous allocation would be allowed such that you the larger type.
[deleted]
[deleted]
Yeah, I thought the top comment was saying that the way the author did it wasn't idiomatic, in which case I was wondering what is.
You idiot! What you wrote would have been true only if the expression were &func_table[max] - &func_table[0].
However, you make a good point: the difference between two pointers is in multiples of the pointed-to size. It's not 11 bytes per function, but rather 11 * sizeof(pointer to function type) - probably 11 * 4 = 44 bytes. (the author is lucky that the functions are indeed laid out consecutively in memory)
When you want the difference between two pointers a and b to type T in bytes, either cast the pointers to char * before taking the pointer difference, or multiply by sizeof(T).
However, you make a good point: the difference between two pointers is in multiples of the pointed-to size. It's not 11 bytes per function, but rather 11 * sizeof(pointer to function type) - probably 11 * 4 = 44 bytes. (the author is lucky that the functions are indeed laid out consecutively in memory)
When you want the difference between two pointers a and b to type T in bytes, either cast the pointers to char * before taking the pointer difference, or multiply by sizeof(T).
[deleted]
I'm using gcc 4.x which seems to generate a slightly smaller function (10 bytes vs. 11):
I ran it on a 3GHz Xeon with 4MB cache (so 2MB per core I think) and I get roughly the same, but with much reduced compile times.