You want to really blow their minds, show them… Um, what’s the name of that weird two-column construction using case?
Which language and processor are you discussing? Rest assured some of what you assert is 100% true in some environments, 100% false in others, and 50/50 true & false in yet another group.
Which is really the big point of the thread: These details matter. Be explicit. Or be buggy. There is no third choice.
Well, C, of course, because all other languages are evil: they are not-C.
switch
In C and its derivatives the rule on evaluation was put in as an explicit language feature to short circuit the need for cascading if statements when you needed to guard an expression:
if ptr != NULL {
if *ptr == thing {
some action
}
}
versus
if prt != NULL && *ptr == thingy {
some action
}
That is really all there is. There is no other deeper reason for having the rule. Not all languages really needed it, many don’t, and some would regard the idea as a hack.
No ordinary switch. Chronos is talking about Duff’s Device:
register short *to, *from;
register count;
{
register n = (count + 7) / 8;
switch (count % 8) {
case 0: do { *to = *from++;
case 7: *to = *from++;
case 6: *to = *from++;
case 5: *to = *from++;
case 4: *to = *from++;
case 3: *to = *from++;
case 2: *to = *from++;
case 1: *to = *from++;
} while (--n > 0);
}
}
With great power comes great (ir)responsibility.
That *is *a bug, not a feature. Yes it works. Yes it’s evil. Yes it’s C.
Many years ago I invented Duff’s device myself whilst trying to accelerate a copy loop. I though I was pretty clever until I looked into the underlying implications of what actually happens. First up it can break branch prediction, so your pipeline stalls and loops go slower. Secondly, unless you can convince your compiler otherwise (and depending upon the compiler there may be ways) it may decide that since there are essentially arbitrary ways that code can reach any of the labels, it needs to add code to save and restore register state between statements, which totally wrecks the performance. Another problem is that it doesn’t sort out exactly well is block boundary effects, where you are vastly better off getting your transfers to use aligned instructions.
No doubt it looks really neat, and I remember a colleague being really impressed with it, but when I worked though it all, I pulled it out of the code again. With any modern language and compiler the single best thing you can do is to express what you mean, not what you think the code should look like.
Tuning block copy code is a bit of an art, and is best left to someone who is intimately familiar with the processor to code up and put in the language libraries.
This. It reminds me of the APL one-liners I was so fond of as an undergrad. Not smart.
The esteemed Raymond Chen has a number of blog posts on the fact that modern optimizing compilers do so much stuff you’ve never thought of that any attempt to get cute with your high level language syntax (and yes, C is a high level language) just risks fooling the compiler, not optimizing the instruction stream.
C is pretty close to assembly language and often used that way. A lot of the C code I work with was converted from old assembler and barely looks like C. When efficiency is paramount you may need some cryptic coding, but there are these things called comments and documentation that can prevent that from being a problem. The practical rules for systems code may be quite different from those for application code.
For the constructs that use && to short circuit and protect a reference, like “(p && *p)” can be coded into a macro to prevent future problems.
Yes, compilers have improved immensely since even the '90s. I remember looking at machine code in the old days and seeing excessive loads and stores and reloads of the same value, but now you can write ridiculously verbose code in C and the compiler will figure out how to scrape it down to the least amount of object code. Sometimes a variable will not even appear in the debugger because its 3 lines of existence got merged into what in the old days you would have written as a bulky expression several layers deep.
Verbosity in source is a good thing.
Before mid 2000’s optimizing compilers I’d agree with 100% of what you (ETA TriPolar) say. And yes, there is C out there which is a thinly warmed over port from earlier assembler.
The big point is that nowadays when efficiency is paramount you choose a good algorithm, pay attention to operand alignment and locality, code it straight forwardly, and let the compiler do magic shit mere humans are nowhere near smart enough to do. Except in very isolated environments with frankly weak toolchains, the compiler is much better at optimizing for hardware performance than even a journeyman of decades’ experience.
C is a mid-level language, in that it forces you to manage your own memory.
That isn’t a knock on C, it’s just a statement of where it’s most useful: Not at the very lowest level, where assembly is required for machine-specific opcodes, nor at the very highest level, where programmer productivity is most important, but in-between, where memory usage is more important than productivity and time is going to be taken to track down memory leaks and buffer overrun bugs.
In our case we have a complex situation where we have to compile our system for multiple platforms and avoid a few cases where the optimizing magic is a problem. But a lot of it is just holdover from the days of yore. Nobody’s going to go through the effort of rewriting the old code converted to C over 20 years ago. It’s not wise IMHO, but only a handful of people ever touch that code.
Application programmers should definitely not be concerned with execution efficiency at the code level, it is a compiler issue. And in the vast majority of modern applications localized execution efficiency is rarely a concern anyway, we are far more bound by communication and disk bandwidth than anything else.
Using a Boolean or operator, I can be explicitly buggy. That is truth-equivalent, after all.
Not necessarily. In Apple’s Objective-C at the very least, you can use an implicit memory management scheme that relies on llvm to track allocated entities and figure out when they need to be disposed of. Which works better in an OOP environment than in a raw-C type environment, I would imagine.
Well, that’s Objective-C, not C. It’s compatible with C, to some extent, but it isn’t C.
From what I’ve seen with C used at the OO level, C++, C#, Objective-C, etc., most of the memory management is already in place or handled automatically. I don’t use those languages much though, I see it mainly in our customer’s applications, and almost all of it is rather pedestrian data processing.
Yeah, Java, C#, C++, and Objective-C aren’t C. The fact they work differently from C is how you know.
Not meaning to trigger a debate on high vs mid-level. For sure C was originally conceived as almost syntactic sugar over assembler on a generic early 1970s CPU design.
But my point is that as we get to superscalar pipelined multicore CPUs, optimizing compilers, and all the rest, the actual flow of micro-ops in the instruction pipelines & the data flow in register banks resembles that generic 1970s TTL CPU very very little.
The language features themselves haven’t moved up from mid to high. What has happened in the underlying silicon has migrated into the next time zone so the original almost one-to-one correspondence between C idioms and hardware has become tenuous to the breaking point.
IOW, the quantity of abstraction between modern standards-compliant C and the transistors in a modern CPU is vastly larger than the corresponding quantity in 1980. Pretending you’re still writing very close to the silicon is pretending.
With the notable exceptional of some legacy situations such as TriPolar points out a few posts ago.
Having been involved with hardware design as well as programming, I can tell you that this is taught in about the second week of logic design 1.
In the real world !(p && q) can be done in one NAND gate. The other form uses two inverters and an OR. However, if the p and q coming from driving gates are inverted anyhow, the second form might be cheaper, Especially if the output is used inverted, in which case you can use a NOR. NANDs and NORs are usually smaller than ANDs and ORs thanks to the way the circuit design is done.
Hardware doesn’t have side effects we see in code sometime, and there are no short circuits - though everything is done naturally in parallel.