Experiments ought to be repeatable. Here is a quick bash/perl script to generate the parts he left out. It expects that you have copied his main module to a file called main.c in the CWD.
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.
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.
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 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.
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.
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.
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.
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.
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].
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.
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).