C is Lower Level Than You Think
prog21.dadgum.com
prog21.dadgum.com
That's perfectly rational behaviour. And yes, the string could change mid iteration, in the loop or in another function that has access to the address, or even in another thread that's just made a guess at a valid memory address.
This is the beauty and power of C, everything is a piece of memory and nothing is guaranteed, and no we will not hold your hand or stop you doing something stupid. Someone else has a valid use case for that, even if they don't know it yet.
Just replace C with assembler and we know why we should all go back to it. Or rather not ..? Guarantees provide reliable environments. Reliablity is really helpful.
My nitpick aside ... I still want to (finally, really) learn C some time. Yeah, I can read it more or less, but that's not the same. Maybe on my next vacation.
But it is what it is, a thing of great power and beauty but highly dangerous in the wrong hands.
Really? What made you think he doesn't think at the correct level? The fact that he tried to correct others that invoke the "sufficiently advanced compiler" thing?
If anything that would be proof that he thinks at the RIGHT level.
>And yes, the string could change mid iteration, in the loop or in another function that has access to the address, or even in another thread that's just made a guess at a valid memory address.
He already stated all that -- not to mention he knows it for over 3 decades.
>That's perfectly rational behaviour.
He didn't say it wasn't. He merely said that counter to some people who invoke the "sufficiently advance compiler" idea, that's what is actually happening.
There are many ways you can shoot yourself in the foot with C that are much more obscure.
Thanks for underlining exactly why I love C: no bullshit, get down in the dirt, eat your debugging, live with valgrind for that tiny bit that won't free itself,... Sometimes, I wish ruby could segfault a bit just so I have to debug my code for something else than a missing '='...
1337 / 8;
Becomes:
1337 >> 3;
Compilers ought to figure that one out, shouldn't they? Well if 1337 is a signed int, they can't. Shifting actually leads to different semantics for negative numbers, so this optimisation is only possible for unsigned ints. You can either do the bit shift or cast to unsigned, but it's a leaky abstraction either way.
For the curious who don't want to duplicate my look-up - right shift is by definition division by two except negative signed numbers, for which it's implementation defined. GCC on every architecture I've checked uses arithmetic shift for signed, probably because that extends the base definition to negatives.
for (int i = strlen(str); i--;)
...loopContents...
There's a right way to handle values that are repeatedly used, and a wrong way, ie. relying on the compiler to optimize. Many dynamic languages store the length of a string buffer, as part of a String object. ie str.length, and it's very cheap to query, just an accessor/property/getter.What is misguided in this rant, is the attack against strlen which operates on pointers to zero-terminated strings. The length isn't stored, the only storage is for the string's bytes. If you want to have fast access to strlen, and you find yourself being pained by the extra variable to cache the value, then use std::string.
This blog post needs a refactor.
Also, std::string is C++, not C.
And other languages have nothing to do with it; this is strictly a post about C, not a dynamic language, and not C++.
[0] http://bstring.sourceforge.net/
C is low level, but the same mistake could be said for any other language where a function is repeatedly called in a loop. The compiler can only do so much, garbage code is still garbage code. Cache your function calls less they const restricted, and extremely simple, and even then, the compiler might still fail to optimize.
struct string_t { const char * buffer; int lenght };
And really, I fail to see any attack on C in this article, it only mentions what a naive programmer might do, and why he/she shouldn't. const char[] str = "This is a string";
int len = sizeof(str) / sizeof(str[0]);
Compile-time..still not dynamic immutability though.The array pointer can't change, the buffer size can't change, and the char components are const. Immutable but then again, you can violate that quite easily. And it's only compile-time immutability, which isn't all that great.
I think it's somewhat scary to rely on post-decrement; I tend to favor pre-decrement in for loops as a general pattern, but of course in your code it must be post or it will break on an empty string.
I still think the original article's suggestion to manually pre-compute the length (storing it in a const size_t variable, of course) is the better option, overall.
> I tend to favor pre-decrement in for loops as a general pattern, but of course in your code it must be post or it will break on an empty string.
That makes absolutely no sense. It's post-increment so it runs for i=0, could have easily been:
for (int i = strlen(str); --i >= 0;)
http://codepad.org/kd3oOBAb <-- fiddle around beginner ;-)"It's post-increment so it runs for i=0" is probably exactly what I meant, you needed to use post-decrement (not increment) since otherwise it would break. Of course it can be re-written like you did now, but then you added the >= 0 part which was implicit in your first comment. Its omission made it important to pick post-decrement or it would break on an empty string just as I said. My entire point was that it's beyond most beginners to rely on fairly subtle things like that.
Not sure why you're so condescending, I certainly didn't try to call you a beginner, it was the other way around: your code was not very beginner-esque, but the original article was about how a C beginner might write a simple loop and be surprised.
for (auto i = strlen(str); --i >= 0;)
and you got yourself an infinite loop. One that isn't so easy to see or diagnose either, unless you know that strlen returns size_t. Which a beginner might not.So no beginner programmer will write proper code without further practice. I thought this was reasonably obvious. The reason why both those constructs work the same they do is likely to be found within a single paragraph of a properly-written book on the C programming language.
I'm definitely not claiming this is a nice feature to have in a language. I'm just trying to point out that the quality of the code a beginner programmer can write isn't a very good indicator of how high-level a language is. All beginning programmers will write shit code in any language, as low-level as Assembly and C or as high-level as Lisp or JavaScript.
In 25+ years of C, I've seen plenty of people getting for-loops wrong (making broken assumptions about what's evaluated where, and countless off by one bugs), but everyone gets while-loops.
If you are going to do for-loops in C, stick with the most obvious, simplest variants possible - the moment you try to be smart, you've substantially increased the odds that a maintenance programmer will introduce a bug at one point or another.
// Pure C99 (prints an invalid character at the end)
for(
FILE *f = fopen("/etc/fstab", "r");
!feof(f) || (fclose(f), 0);
putchar(fgetc(f))
) ;
// C99+POSIX
for(
int fd = open("/etc/fstab", O_RDONLY), c = 0;
read(fd, &c, 1) || (close(fd), 0);
write(1, &c, 1)
) ;No attack at strlen at all. Perhaps you didn't read it correctly? Not to mention the author already knows all of this (trivial) information.
The only attack is at the naive assumption of some newbie programmers (which the blog post targets) that "the compiler should take care of that".
For the love of all that is good, please don't actually printf() for every character...
Depending on your platform that may or may not result in a ridiculous number of system calls, but no matter what, it will be slow if you do it a lot. If you insist on calling a function for every character (which is still silly), please at least putchar()/putc()/fputc() (putc()/putchar() can expand inline, but even they have substantial overhead to lock the stream)
I've seen systems spend the majority of their time in IO functions because of stuff like that. Always, always, always batch up IO unless you have real reasons not to (and almost always avoid syscalls when you can)
One approach is to learn to read assembly (being able to write it from scratch is optional), and understand what affects clock cycle count. On modern architectures that also means understanding cache effects, and particularly looking out for memory access patterns. Then look at lots of disassembly and learn to understand what kind of code typically gets generated from your C. Often it will be plain obvious why things are slow when you see the amount of generated code (though here be dragons - sometimes modern compilers will actually do a good job optimising, and as a result produce code that no sane human would ever write manually).
You can quickly get a good feel for patterns that are good and bad.
But in general:
- Profile. Profile some more, to make sure you're actually spending your time on the code that matters.
- A useful rule of thumb is that for any given algorithm, the fewer function calls and memory accesses, the better it is likely to to be. Functions calls are expensive, and often (not always) reduce locality of code, and so increase the amount of time spent accessing memory that is not in cache (== expensive). But don't start hand-unrolling/removing abstractions until you know by profiling that the code in question is performance critical, or you may just end up creating a maintenance nightmare.
- System calls are particularly nasty on any memory protected modern OS. The context switch overhead tends to be absolutely brutal. Which means it's very often worth quickly breaking out strace (on Linux) or equivalents to quickly eyeball the system call behaviour of applications before spending much time on other stuff. E.g. one particular pet peeve of mine is the amount of time you see read() or write() with small byte counts - if that occurs often, you can sometimes get truly massive speedups simply by changing the code to do non-blocking read/writes to/from a buffer and do smaller read/write's in user space from those buffers. (Always be suspicious of code that does read/write/recv/send or similar unless you can see it uses large-ish buffers... but profile). My experience is that odds are shockingly high that people will severely underestimate the cost of system calls.
- Keep data that is accessed together close in memory. E.g. cluster values by access patterns when you can, to avoid thrashing cache when you access the data. This can be tricky, and the solutions can be non-obvious because the actual access patterns may not be that easy to spot.
Your available tools will be different once you start using CUDA or OpenCL; you'll need to look for profiling methods designed for those environments at that point.
[0] http://man.cx/clock_gettime [1] https://duckduckgo.com/?q=gprof+tutorial
char c;
while(c = *(str++))
...
This is, after all, the loop strlen() itself performs. char c;
for (int i = 0; (c = str[i]) != '\0'; i++)
...loopcontents using c...
this should be faster than all the rest of the implementations as you might break early and you save a memory dereference of every character. in all honesty though, these optimizations are highly unlikely to be the constraining factor on any loop so the question really is moot.> Furthermore, the difference between the two really is negligible. > ... > The difference is an increment to a number, i.e. a single instruction which is probably the fastest instruction in the ISA.
Whether or not it matters will depend greatly on how often this loop is executed, and what else it does, and the ISA and available address modes, as well as whether or not the pointer and/or counter can fit in available registers.
If the pointer and counter is not in registers, and you don't have a suitable indirect indexed type load/test instruction, you can easily end up with 3+ extra memory reads. Memory access gets expensive if/when those kind of loops are run often enough.
I've seen speed improvements of 30%-50% from a couple of hours of work from production systems just from trivial rewrites to reduce memory accesses like these. Some fixes like that have paid my salary for months in reduction in server investments.
I wrote this quick-and-dirty code [http://codepad.org/d9ilykxA] to try and test the effect.
To me, it's a little interesting how it various with optimisation level, but I've not picked apart the assembly to see what's going on.
Under gcc 4.8.1, 64bit ubuntu):
-O0 forwards 4.37s, backwards 4.88s (fowards faster)
-O1 forwards 2.67s, backwards 2.66s (same/backwards faster)
-O2 forwards 2.67s, backwards 2.66s (same/backwards faster)
-Os forwards 3.33s, backwards 3.00s (backwards faster)
and the same under clang: -O0 forwards 3.35s, backwards 4.11s
-O1 forwards 2.67s, backwards 2.67s
-O2 forwards 0.69s, backwards 0.77s
-Os forwards 2.67s, backwards 3.00s
So...it's not as simple as "forwards or backwards always faster", unless the test code is simple enough to be defeated by the optimiser in some cases which real code wouldn't.Also - what is going on with clang -O2?
for x in stuff_list:
print other_list_of_stuff.count(x)
This is intuitive to beginning programmers and uses Python's methods. But there's hidden complexity behind it. C is no worse a language for not moving strlen out of the loop than Python is for not optimizing this. Languages work at their level, and it's essential the programmer understand the precise bounds of this level. FOR i := 0 TO Strings.Length(s) - 1 DO
...
END
would be equivalent to lim := Strings.Length(s) - 1;
WHILE i <= lim DO
...
INC(i)
ENDThis is my favourite article of him :)