The "C is Efficient" Language Fallacy
scienceblogs.com
scienceblogs.com
* C programmers have more freedom to arrange data in memory to exploit locality
* C data structures need less bookkeeping
* C programs manage memory manually, and so lack GC overhead
* C programs can easily swap in different allocators for different work sets
* C function calls are usually wired into the code, not indirected through (several layers of) tables.
That's to say nothing of bytecode interpretation overhead, which is probably a straw-man argument.
I buy the instruction scheduling argument for inner-loop numerical and vector scenarios, but even in performant code, that's usually less than 10% of the total, and from what I've seen, both C and HLL code tends to delegate that to machine-specific assembly libraries.
The problem with his argument and yours is that C is NOT faster for The Rest of Us because it it amounts to premature optimization just by virtue of using it. And because it takes so much effort to get C code to just work, the WHOLE program ends up being slower than the corresponding code in Haskell, OCaml, or perhaps even CLisp. Having worked my last job in a company where the programmers preferred C to C++ and C++ to Python, it startles me how utterly slow our system was. And ditto for the previous job.
At my first place, we had a streaming video server that had blindingly fast compression routines, but it was all held up by a UI loop that harked from the prototype days. Thus the argument that C/C++ is/are the best languages for fast code is moot. It is only the fastest if you've all the time in the world to write it.
C programs start up faster than HLL programs. They run faster. They consume less memory. They tend to be more responsive.
From "looking up an object by its string name" to "splitting a string on the comma character" to "running the following functions on a 10hz timer", simple operations that all programs do have less overhead in C than they do in many high level languages, because the C program isn't creating and disposing of hundreds of little objects every time it does one of those things.
Hey --- I write most of my code in Ruby. I'm not saying you should use C. But you shouldn't make up bad reasons not to use it when there are so many good reasons to cite.
When they get bigger, they quickly bloat up to compensate for how hard C makes it to write large-scale software. Oh, and it takes a lot longer to modify those C programs, for the same reasons.
C's malloc() time is also a lot slower than GC'd languages. For them, a memory allocation isn't much more than a subtraction.
Second, fixing malloc slowness is among the easiest and fastest optimizations you can make in a C program (in most cases, a pool and freelist will get you 90% of the way there), and no GC'd allocator is faster than pool and arena allocation (which is usually just a couple ALU operations in both the alloc and free cases).
Third, large C programs tend to start up faster than large HLL programs. For instance, Eclipse is slower to start for me than Firefox --- and if you're thinking of Firefox as your archetypal "big bloated C++ program", remember that Firefox is by design the most complicated application most people ever run.
To make that argument work, you have to find a HLL program of comparable complexity. You didn't do that; you just imply that one exists. I'm calling you out on that.
> Second, fixing malloc slowness is among the easiest and fastest optimizations you can make in a C program (in most cases, a pool and freelist will get you 90% of the way there), and no GC'd allocator is faster than pool and arena allocation (which is usually just a couple ALU operations in both the alloc and free cases).
In a semispace or generational GC, allocating memory is just bumping a pointer.
It is unusual for a lot of references to exist from older generations to the youngest generation, but when this occurs there are data structures for detecting and resolving these references very efficiently.
If you're allocating a lot of short-lived objects, then explicitly keeping the live objects is much more efficient than explicitly culling the dead objects.
If only. You have to check for out-of-memory, and throw an exception if so. (This check can sometimes be optimized away, though). You also have to mark the size of the allocated block, so the garbage collector knows how many bytes to copy during the sweep phase.
Also, garbage collectors usually allocate from a shared memory pool, so every allocation also involves some mutex operations.
Allocation is bumping a pointer in theory only.
You have to check if there is space for the allocation in the current space. This is two native instructions, a compare and a conditional branch. If the allocation fails, you don't throw an exception, you launch a minor collection.
> You also have to mark the size of the allocated block
Yes and no. When an object is initialized, type information is recorded that contains the size. I don't think that the Java VM explicitly stores the size of the allocation somewhere. It certainly doesn't need to since the same information is available by just looking at the object itself.
> Also, garbage collectors usually allocate from a shared memory pool, so every allocation also involves some mutex operations.
In the Java VM this problem is addressed by dividing the Eden space into chunks where each chunk is private to a single thread.
> Allocation is bumping a pointer in theory only.
An object allocation in Java is around 10 assembly instructions. More than just 'bumping a pointer' but not a lot more.
1 and 2 were already commented on, so I'll just add that, there was a proof that a copying gc with enough memory can be faster than manual allocation. http://www.cs.umass.edu/~emery/pubs/04-17.pdf
This is the link I was thinking about: http://www.cs.ucsb.edu/~grze/papers/gc/appel87garbage.pdf
* Garbage collection is faster than manual frees when you provide processes with so much memory that collection happens so rarely that its cost is lost in the noise, and
* Garbage collection is faster than manual frees when you ignore all the constant factors in both collection and manually freeing, for instance the cost of taking page faults?
But it also gives you additional information: how to check if the cost can be ignored and whether it will be lost in the noise. And that's important, because in serious VMs you can often control the GC parameters. That means you cam make sure that those asymptotes are as close to reality as possible for your exact problem.
It's the same story for hashed collections. You can find big O costs for their operations, but they're amortised costs. Sure - once in a while you have to realloc / rehash / copy the table depending on the implementation - but then you know how to judge if that collection is ok for you. You can modify the starting size exactly in order to lose the rare cost in the noise. The trick is knowing the limits.
Yet somehow many people who love hashmaps hate GC...
You're right about what the paper says, it just didn't spell out the reason why that info is useful ;)
Having a running model of your system to profile is one of the best tools you can have for optimization.
I think your idea of "the actual debate at hand" is too narrowly focused. As a reader of HN, I'm generally interested in how to get things done in a reasonably efficient manner. I find that "get it running, profile it, understand it, then optimize" is a good thing to think about when discussing performance. It cuts through a lot of delusions people have about their ability to write fast code. (Especially when they try to do it up-front.)
That's working C programs vs working HLL programs. And, by those metrics, assembly language programs are better still.
> Hey --- I write most of my code in Ruby.
Which suggests that performance of working programs is not the only important factor.
Yes, "premature optimization" is arguably the wrong term for making that suggestion, but it is quite valid.
There is no important difference in performance between C and Java according to http://shootout.alioth.debian.org/
So long as you keep an entire algorithm in one method, and restrict yourself to certain optimized operations, the resulting JIT-ed code will look like it was produced by an unoptimized C compiler. There will be no Smalltalk message sends. (This approach is a bit fragile, though, since you have to have esoteric knowledge to maintain code like that and keep the same level of optimization. Most of the time, you'd just implement a speed-critical operation like this as a DLL in C to be called from Smalltalk. Smalltalk/X has a different approach -- you just inline C code in your Smalltalk method.)
Combine that with naive memory management in the C code, and you have C code that loses a benchmark. Mind you, that was bad C code -- reference code only has to be correct, not fast.
"Smalltalk is Slow" usually a sign of ignorance or trolling. For lots of problem domains, the advanced commercial Smalltalk VMs are pretty darn fast.
I can live with that.
I think just about everyone here is underestimating the impact of this one "feature". Most optimization approaches are grounded in 1970s computer hardware, where CPU speed and memory speed were two aspects of the approach. That's not the case anymore... memory access times absolutely dominate on modern hardware. Just lookup the number of instruction cycles a cache miss takes for your favorite system. Its appalling.
C and C++ have been able to stay ahead of the curve because they allow strict control over memory layout. Ocaml is often brought up as a competitor, but storing a floating point value in ocaml requires a boxed pointer! Nevermind trying to make an aggregate structure that includes floats and other types bundled together... everything will get boxed and the cache never stands a chance.
C and C++ are the only fully current languages that let you bundle your data exactly as you need it in as small space and in appropriately sized chunks such that memory access doesn't grind your program down. Of course you can fail to take advantage of this ability and write slow code in C or C++; in which case you'll match the benchmarks for your other favorite languages and maybe make a blog post about it. That'd be missing the point though...
People don't realize what a power tool C/C++ is because they haven't had it out for a spin at that level. If you've done serious API work, and wondered why you're busy byte-packing, it's because there's some highly optimized code somewhere you're feeding. You can allocate a big honking chunk of memory and create your own world in there. Boxing, for all its goodness, is a performance killer.
C and C++ are quite fast locally, but sometimes that works against being fast overall.
I work in data acquisition and in this domain C/C++ is king. The raw number-crunching is essential (for our applications anyway), and any bonus that can be had from SSE operations, cache locality, loop unrolling, software pipelining, etc. can have a major impact on the overall performance. Assembly is still common on DSP processors and the like (they typically provide a special instruction set). Even in C++, we tend to use the bit-twiddling and pig disgusting optimization hacks that low-level access provides :)
Some people use higher level tools such as LabView but the performance is a huge step down. It's OK for prototyping and learning.
http://www.oonumerics.org/blitz/ This is what Templates are made for. And this is what you can't do in something like Java or Objective C. Java Generics boiling down to type cast at run time to and from Object, it doesn't help performance a bit.
It's tricky to really push on high-level languages because for every argument against one, there are 3 other languages that don't exhibit that problem. If there was a platonic ideal of a high-level language to argue from/with, this would be a more useful debate to have.
There's often a point of diminishing returns in spending time and effort optimizing C, and OCaml can get surprisingly close with much less work.
Ask HN: Does this mean that HN would prefer I wrote comments that do cover all the cases, instead of just sticking to the key point? Or is it just that there seemed to be a gap in my comment, which people naturally wanted to cover?
Note: the replies add more than just "rewrite only hotspots" (like the above one detailing setup, errors, edge cases), and I certainly appreciate extra details being filled in. Is the valuable extra detail the reason for the extra votes? I just feel kind of annoyed that the replies seem to suggest I was stupid in not mentioning the hotspot idea. This has happened to me a few times now. Am I taking it too personally? Is it just a question of different opinions on what is important? Or is it just that I failed to communicate my decision, and then get annoyed when people point out the other branch of the decision?
There is a issue of preference here: I prefer to keep all the code in one language. One factor in this is that my projects are small, and for these, the overhead of switching languages and managing the different source isn't worth it. I'm not running up against efficiency problems either. I'm sure it's different for larger projects, especially multi-person ones, and particularly if the project covers different kinds of activities (for which different languages are suitable), and even more so if there's need to integrate with or reuse existing assets in different languages.
Thanks for any clarifications you may have. :-)
I didn't mean anything personal, and I don't read any of the other comments that way.
The other comments may have just been voted up by people who also like Lua or Mercurial, or something. I thought it was worth noting that Lua was explicitly designed for that style of development. (Lua's also my favorite language.)
Of course. It's the "disproving a generalization from its exceptions" argument fallacy.
At this point, GC is often faster than manual allocation. (Due to being able to allocate or deallocate a bunch of things at once, rather than having to allocate/free memory whenever the programmer says to.)
Many GC schemes are probably faster than malloc. But it's a much less credible argument to say that you have a GC that is faster than a custom allocator tuned to a workset. You probably don't.
GC faster than off-the-shelf allocator? Probably.
GC faster than off-the-shelf strategy? Doubtful.
And "remaining 1% of performance"? How disingenuous is that? Allocator optimizations have sped up commercial software I've shipped by over 200% in the past. Not sped up the allocator; sped up the program.
There's something beyond those figures, though. And it gets worse when a team is involved.
The higher the level of the language rises, the less it allows making bad programs, however big the program is.
I think it's more complicated than that. Ruby is higher level than Java, but I would argue that Java makes it harder to write bad programs, especially in a team setting.
for ( int i=0; i<ni; ++i )
for ( int j=0; j<nj; ++j )
for ( int k=0; k<nk; ++k ) {
a[ k*ni*nj + j*ni + i ] = ...
}
which sucks. Optimizing multidimensional array access (which characterizes most of scientific computing) is much easier for a Fortran compiler.Consider, "float a[X][Y][Z]" and "a[i][j][k]".
About a year later, testing a new JIT for Java, the Java time was down to 0.7 seconds
I've been surprised at the speed of Java recently. I wonder how much improvement is left in dynamic compilation.
The HP project Dynamo was an experimental JIT compiler where the bytecode format and the machine code format were of the same type; the system turned HPA-8000 machine code into HPA-8000 machine code. Counterintuitively, this resulted in speed ups, in some cases of 30% since doing this permitted optimisations at the machine code level. For example inlining code for better cache usage and optimizations of calls to dynamic libraries and many other run-time optimizations which conventional compilers are not able to attempt. http://en.wikipedia.org/wiki/Just-in-time_compilation
I find with java essentially you are amortising gains which are payed back with GC at a later date (in many cases I guess it is worth it).
Its not one size fits all !
The mapping from source to machine representation in C is relatively trivial, which is the source of all C's ups and downs. If you ever plan to count microseconds, cache misses, TLB misses, mispredicted branches, etc., you had better start with a toolchain whose machine-level output is grokable from the source.
It always annoys me when clueless people judge a language they don't even understand.
Repeat after me: there is no such thing as C/C++.
I believe you actually meant 'ignorant of C++ template metaprogramming techniques'. The author seems well aware of C++ templates and even says:
>> the thing I coded immediately before the Stellation experiments was a very hairy template analysis for a C++ compiler
>he was lumping C and C++ together in his "benchmarks".
I couldn't see where, could you point out which comment leads to that inference?
I am well aware that there are good reasons to optimize things in languages like C (and I use them), but consider...
If I take several extra weeks to code, debug and test a C solution, and I could have had a script done much sooner, then my results were not faster overall. Why? Well, the script could be slow as dirt, but if it has a few extra weeks to churn through data and produce results, it may be done before the C program is even ready.
It's also important to remember that not all bugs are in software. Suppose I was looking at an entire problem in the wrong way, and this wasn't apparent until I started seeing results? In that case, my earlier start with a "slow" program meant that this mistake was found much sooner, so the script can be thrown out and redone, producing correct results with not much of a time penalty.
Users' time is also much more expensive than the computer's. That's why we write software in the first place.
In some respects, having long-lived software with lots of users makes speed the least of my concerns, because they're always asking for new features, and those are relatively easy to add to scripts.
And the relationship between software speed and productivity isn't linear, because people multitask. If a program takes 10 seconds to run, I might sit and wait for it to complete, without doing anything else. Whereas, if the program takes a minute, I may decide to switch to another quick task, and then return to see results. In this case, both tasks needed to be done, one took longer but it ate up the "slow" runtime of the program, and was only parallelized because of that long runtime.
The comparison of development times and running times simply makes no sense if you assume the script is going to run a 1000 times. I agree that this relationship isn't linear. That's exactly why it's pointless to compare the the two numbers as if it were. The only number that is comparable is probably the profit you make in each case.
I use msvc mostly and gcc sometimes.
GCC's C99 implementation is mostly complete - you can find out more here: http://gcc.gnu.org/c99status.html
The test lies in the ability of the programmer to do so.
This might redeem C a little.
However, the real reason C will not go away any time soon is that there is no replacement for low-level software yet. Nothing eles has quite the same minimal dependencies.
Intel C vs Intel Fortran : http://shootout.alioth.debian.org/gp4/benchmark.php?test=all...
(I'm not even going to link to the GCC Fortran benchmarks. They're embarrassing.)
C is no slower than Fortran on any of those benchmarks, and on some it cleans Fortran's clock. The aliasing issue is the only thing Fortran has going in its favor, but clearly its not ubiquitous. The n-body benchmark, for instance, is fairly typical numerical code. You might even think, since it's simultaneously reading and writing through multiple pointers of the same type, that aliasing is an issue, but its not. And in the rare case that it becomes an issue, there's compiler hints (e.g. C99 restrict) for that.
Picking Fortran over C solely because of aliasing worries is premature optimization of the worst kind.
It was quite strange the first time looking through the objdump seeing things like punpwlkd and xmm.
And then discovering what -fast would do to things (it makes icc look at your whole program to optimize, so it does things like ignore CDECL and uses whatever registers it can.
And if you're application doesn't fit the native SIMD properly, there's no chance the compiler can really do anything meaningful with it anyway.
http://www.cas.mcmaster.ca/~kahl/Publications/TR/Anand-Kahl-...
http://wwwlasmea.univ-bpclermont.fr/Personnel/Jocelyn.Serot/...
http://tirania.org/blog/archive/2008/Nov-03.html
etc.
The first link is just a paper; the second is a 1.0 release that is seven years old, it only supports SSE, and it's all in French; the third is only for Mono, it only supports SSE and its SSE support is old and incomplete.
On the other hand, if you try to use SIMD types and intrinsics in C++ you'll find current and comprehensive support from the major compilers on all SIMD platforms.
(I'd love to use a current and comprehensive version of Haskell SIMD, but its just not ready for prime time.)
Since you're obviously an expert in the area, why aren't you helping make it ready for prime time?
The language is a tool, the efficiency of it really depends on how the programmer who designs and implements a program. You can have a horrible coder write something in C that would be very slow and inefficient, and given the same problem to good programmer and you can arrive at a faster and more efficient result using a bash script.
The reason why lot of people (including my self) believe that C/C++ is a high performance language is not because it fast for all applications, but gives the programmer more control instead of leaving all the fine details to the compiler to second guess. (@tptacek's reasons are perfect for this).
Also, the scalability of a single system is limited. If you really need extra speed, I think going parallel is the key. Most numerical methods are parallelizable. And C/C++ were not created keeping this mind. It can be a programmer's nightmare to debug multiple threads with memory leaks. Languages like Java do make this task easier using programming paradigms like Map/Reduce.
There might be a third option though - code generation in a HLL. Coconut is a nice example of this:
http://www.youtube.com/watch?gl=GB&hl=en-GB&v=yHd0u6...
Instead the compiler being a black box, Coconut is structured as a set of libraries for code generation, analysis and optimisation. They report outperforming c SIMD by up to 4x on the cell architecture.
Please remember that "char x[N];" and "char x = malloc(N);" are NOT the same. (Not sure if this is news to anyone, but when I was learning C reading that would have made me think otherwise).
The GPU is used with C.
C isn't faster than hand coded assembler on cpus... but is pretty damn quick.
There are specialist compilers for C, like vector C etc.
anyway... whatever. back to typing text into a file now.
btw, JVM is mere a c++ code.