The post is interesting, but was written in 2010. As you'd expect, some of the details have changed since then. For instance, garbage collected pointers have been moved out of the core language into a library.
I didn't realize how old it was until I got to the bottom. It's cool how the project has stayed true to the goals lined out way back then. While lots of things have been changed and removed, as far as I can tell, none of those high level goals has been compromised, while some have even been reinforced.
Swift came out less 2 weeks ago. As a beta announcement. So most certainly nobody comes from a "modern language background like Swift".
Plus, it's not like semicolons are any big deal. If they are in a language, you add them and move on. Dead simple to add, minimal noise, instantly familiar to most C-derivative programmers. The only comminity that regularly complaints about them are hipster (for lack of a better term) javascript programmers.
In fact I like semicolons because they give a clear delimiter to each statement or expression leaving them out and relying on whitespace like newlines is to me less readable since we're used to reading sentences with punctuation and without that everything runs together and you have to mentally separate them out while parsing not to mention it makes ambiguous cases like in JavaScript possible
Honestly, if semicolons are the one feature somebody mentions when comparing modern languages, I don't want to work with them.
I like Python. I think Python's whitespace-based blocking is interesting. But it is not the most interesting part of the language, and anybody who obsesses over that kind of stuff is just bikeshedding.
Well, semicolons have special meaning in Rust. In some contexts, like the last expression in a function or other expression block, if you don't use a semicolon it will return the value that the expression evaluates to. If you do use a semicolon, it won't return that expression's value. This is good, because it lets you avoid writing the "return" keyword all over the place, but it also lets you avoid returning a value with just a single character!
Stuff like this is what currently puts me off Rust, in spite of some initial excitement about the potential it has. This is like optimising for the least readable code possible. I imagine it's going to be a nightmare in practise.
Developers always seem to be complaining about having to type this or that, when they should be much more concerned with what they have to read!
Really, I couldn't care less about typing a few extra characters. I can type pretty fast anyway and usually spend more time thinking than I do typing. What I do care about is being able to tell exactly what some code does by glancing at it, without worrying too much about whether someone wrote = instead of ==. Rust seems to be setting itself up for loads of those kinds of errors by trying to make the syntax terse at the expense of making it readable.
I have high hopes for Rust and will reserve judgement until it is stable; from what I've seen it's still in a high state of flux. However for now, I much prefer the KISS approach taken by Go, in spite of a couple of things missing from the language (that will probably get there in the end).
> Developers always seem to be complaining about having to type this or that, when they should be much more concerned with what they have to read!
Or maybe they find terser syntax to be more readable. If verboseness was always more readable, if not necessarily more "writable", than terseness, then no one would have a problem with Java since it has good IDE support, including autocomplete and generation of boilerplate code. But it turns out that it's not just a matter of being lazy typists.
That is what's known as a straw man argument. Nobody said verbosity always improves readability, just that it's easy to get too terse, just as Java demonstrates it's possible to get too verbose.
I'm pretty comfortable writing complex regular expressions, but I don't know many other developers that are and I certainly don't like trying to grok anything longer than about 10-15 characters that I didn't write in the preceding 15 minutes. This is a perfect example of where terseness is fine for simple problems, but it does not scale.
I already mentioned Go because it has a pretty terse syntax, but one that is very carefully optimised for readability. One of the things the designers of Go are careful about is not adding too many operators, keywords, or usage rules to the language, which keeps everything nice and simple.
Rust OTOH seems to have a metric boatload of operators that work in different ways depending on the specific context. That's a recipe for a ton of cognitive load, which isn't helpful for writing code, but reading suffers even more.
I don't yet know enough about Rust to say whether this is as bad as operator overloading in C++. As I said, I will reserve judgement until 1.0 because it's entirely possible things will change drastically before then.
This is just my impression based on those tutorials I've looked at so far; I've decided to put off learning it properly until 1.0, whereas I know Go pretty well. IIRC, the move semantics and lifetime annotations were one area that stuck out as pretty heavy on context-specific operators.
Move semantics don't have operators; they happen automatically invisible. Lifetime annotations are not part of operators; they're part of types. The only operator is the same as in Go, &.
If anything Rust has been busy removing operators. As far as I can recall, move semantics have no operators at all (someone who remembers better can probably correct me on this), and lifetime annotations are just 'lifetime (I suppose you could consider ' an operator?). Rust is also LL(1), so it was a very explicit design decision to have as few context-specific operators as possible (indeed, a few suggestions for possibly more intuitive syntax have been decided against for this reason). So what you're saying really hasn't been my experience with Rust at all. In fact, the only really hard thing about Rust, from my perspective, is the semantics, not the syntax :)
There is readable based on each character such as semicolons, dots or curly braces and there is readable based on the shape of code.
That is two separate levels. When you first glance a page of code the first time in 1 second, you should tell what the structure of the program is. How many blocks (for/while/if) it has. How many functions, how big they. You haven't yet had time to read each individual character. That is one level.
Here variable and ambiguous indentation rules get in the way. If there is non-uniform, non-standard indentation then you have to start reading individual lines in detail. If there is standard indentation then it doesn't even matter about little commas and semicolons, it is a level higher than that.
Then past that it is about individual functions, classes, modules, and what have you. Then it becomes about == vs = or . vs , and so on. If there is ambiguity there it could be harder. Like in Python I added a , at the end of a some variable. So that turns it into a tuple. And it resulted in a strange exception down the line. In C the = vs == is notorious. But there are others. None of this make the task impossible, but just slightly harder. The problem is that if this is done many many time over the course of a lifetime of a piece of code. Maybe it take 10 extra seconds for reader to understand the code, that multiplied by thousands of times will add up.
You should keep an open mind about this until you've written and read a reasonable amount of rust code. Rust isn't the easiest language to write or to grok-at-a-glance, but the semicolon thing is a real red herring. It's a really interesting decision, but it really isn't a big deal in practice. There are always other more at-a-glance clues that you either do or don't want a value.
I came from a primarily-Python background; that the last value in a block is the outcome of the expression seemed to me a gimmick when I first started writing Rust (just under a year ago)—I, as you appear to be doing, only saw it as relevant at the end of a function, thus saving only half a dozen letters. I quickly discovered that it is not a gimmick; the fact that it applies to any expression is marvellously useful in Rust code, with its everything-is-an-expression doctrine. (For a language like Python, without this orientation, it would be just a gimmick.)
That the last value in a block is the value of the expression is something that makes a lot of code much more readable, as it frequently obviates the need for additional temporary variables.
I was sceptical until I actually used it. Now I’m converted; though I still also like Python’s pure statement-oriented approach in various ways, I prefer Rust’s model.
FWIW, I am a big fan of easy to read code (as I frequently need to read other people's code in order to 'audit' it). But like others have said, in real Rust code the ability to mix blocks and expressions does not seem particularly confusing to me, and is used in many places other than the end of a function, in more of a functional style. Here is a representative-ish example:
Personally I think most other things in Rust (namespaces, constant as_slice() unwrap() etc.) are currently too verbose, although I'd say that also impedes readability.
Hah, I would caution against looking into the depths of rustc for examples of good examples of rustic code. Many parts of the compiler have changed very little from the original bootstrapping from OCaml, and have only been incrementally updated since then. No doubt it will eventually be cleaned up, but it will be a big effort.
I do agree with the rest of your first paragraph though.
> This is like optimising for the least readable code possible. I imagine it's going to be a nightmare in practise.
We've written hundreds of thousands of lines of Rust and this has never been an issue. The typechecker will catch any misuses of semicolons.
> What I do care about is being able to tell exactly what some code does by glancing at it, without worrying too much about whether someone wrote = instead of ==.
The typechecker will catch misuses of = versus == as well. So assuming that the code you're looking at passes the typechecker, you don't have to mentally distinguish between = and ==.
> However for now, I much prefer the KISS approach taken by Go, in spite of a couple of things missing from the language (that will probably get there in the end).
They're different languages. Go does not have memory safety without garbage collection, and will never have it while remaining backwards compatible. But that was an explicit design goal of Rust. That is why Rust has the lifetime and unique pointer support, which allows Rust to support safe low-level programming in a way that wasn't possible before.
I thought it was crazy at first, but `;` operator as a statement separator that returns `()`, and an implicit return at the end of a block greatly reduces the need for mutable temporaries and leads to a more functional code style. This is a real win when it comes to code maintenance. I greatly miss it when shifting back to other more statement-driven languages. I would encourage you to try it!
> This is like optimising for the least readable code possible. I imagine it's going to be a nightmare in practise.
I have pushed a reasonable amount of code to the rust repo, my own libraries, and some to servo, and it has never been an issue, in fact it has been the opposite (proof: https://www.ohloh.net/accounts/bjz and https://github.com/bjz/).
> What I do care about is being able to tell exactly what some code does by glancing at it, without worrying too much about whether someone wrote = instead of ==.
Rust solves this by having assignment expressions always returning `()` from assignment expressions and not having implicit conversions. The issues with `==` vs `=` completely vanish.
I don't know much about Ada but it seems like Rust vs. Ada is perhaps the more salient question than Rust vs. C++. Why was Ada not good enough in terms of a very "safe" language?
Isn't Ada geared more towards runtime assertions of correctness of programs (contracts and such) rather than ensuring memory safety? Can you statically guarantee memory safety in Ada when using manual memory management?
It seems that one can choose to both use runtime assertions and static proofs of correctness proofs in Spark. But I don't know if that extends to statically ensuring memory safety in low-level code.
Ada was good enough, but it suffers from a bad reputation.
On the early days it was deemed too complex to implement, although I would say C++ became even more complex.
The companies that sold Ada compilers had customers with deep pockets, so Ada compilers were too expensive and required worksations to be used properly.
When affordable Ada compilers became available, not many cared about it.
Nowadays it has found its place where human lifes are at risk. Many avionic systems, train control systems, hospital devices are coded in Ada.
I do attend FOSDEM regularly and also get the feeling its use is increasing in Europe thanks to the security exploits in languages tainted by C compatibility.
I want to comment on one quote from the article:
"""It's impossible to be "as fast as C" in all cases while remaining safe""".
C is not that fast. One of the major problems is that it's close to hardware. 1970s hardware that is. Ken Thompson reportedly once said: "I'm not going to do nibbles. I have an 8 bit processor".
A good example of how bad it has become is that modern processors have a rather good understanding of the 'string' concept, and offer instructions to process them. C offers a char*.
Another problem is that C has strict contracts on how parameters are to be passed through, and combined with separate compilation units, this hurts compilers when they try to optimize things.
I see greater potential for a safe higher level language to be able to align closer to modern day hardware than C. Some nice examples: Linear types can avoid garbage altogether, and coroutines can be expressed clearly and correctly using monads.
On the other hand, raw performance is rarely needed, and most cycles are burned interpreting things like python and php.
I see this comment pop back every once in a while. I'm sure your arguments have value; yet, for some reason, C is always among the winners in almost every benchmark I've ever seen, and also in my personal experience (except for numerical stuff where fortran might be a little faster).
Why is that so? Do more modern languages need another twenty years before they can compete with C (and eventually beat it performance-wise) ?
I don't think that the OP is necessarily saying that there are ~mainstream languages that are faster than C; just that C is far from optimal when it comes to speed and how much it can be optimized.
"Not that fast" doesn't necessarily mean that it isn't the fastest we have got.
Because that is where compiler vendors spent the last 30 years doing compiler backend optimization.
When I started coding in the mid-80's C compilers were dog slow and no better than the alternatives (Pascal, PL/I, Modula-2, Cedar, ...).
In fact, game developers would talk about high level languages the same way they talk about current modern languages vs C/C++, and use nothing else other than Assembly.
However with UNIX spreading into the enterprise and bringing C along, it meant compiler vendors focused on optimizing for the language they were getting money for.
The main issue when people compare languages, is that they forget although the design drives the implementation, not all implementations are alike.
Because C allows for some straightforward mechanical sympathy. As Stutter said, C makes you care about memory layout and cache hierarchy a little, so you win by using all the hardware niceties (prefetchers). I'd say C has a good pareto index. You can go faster but it will require far? more work (to either write or learn a different platform/paradigm).
ps: for instance in some cases, jitted code will go faster than C, some Jitted kernels (never tested personally) allowed for a good 30% performance increase.
A good example of how bad it has become is that modern processors have a rather good understanding of the 'string' concept, and offer instructions to process them. C offers a char.
The x86 string functionality that I'm aware of was directly derived from C string functionality, and any decent library of course uses them. You don't directly map to those underlying opcodes because that would be silly, and would completely undermine any platform independence you might have.
The same for vectorization. You don't explicitly express vectorization in your code, but of course all decent C compilers can easily and robustly generate such code.
C isn't close to the hardware (beyond very high level notions like "contiguous memory"). But it's a simple enough language that it's very heavily optimizable.
> all decent C compilers can easily and robustly generate such code
From what I've seen auto-vectorization is anything but robust, often you need non-portable annotations to get anything decent. See e.g.: http://locklessinc.com/articles/vectorize/ where there's an interesting case where using "if" instead of a ternary operator is enough to prevent optimization.
I have never encountered a C compiler which can reliably vectorize code not specifically written for its "auto"vectorizer, and it's sometimes easier to just write some asm than to persuade the compiler to vectorize a loop.
> But it's a simple enough language that it's very heavily optimizable.
The simplicity of C means you can't make many assumptions of how it is handled. Same problem in C++. You can make a lot of optimizations to assembler if you can guarantee a value won't change, or if the relative location in memory needs to remain static because anything can rip the address and do awful pointer arithmetic with it.
I like to compare to asm.js - it is a reduced language in that it restricts the environment from doing things it can't easily optimize away, except in that case its to assembler. Reduced set versions of C exist to do the same already, but it would be nicer if you just had a fast language default with straightforward rules with ways to just declare wonderland functionality that can mess things up that means the compiler needs to preserve the machine assumptions rather than the language structures.
The problem with vectorization is that it cannot be applied to arbitrary code. You have to consciously design your algorithms, data and libraries to be SIMD-friendly.
I once wrote a SIMD C++ template library based on valarray: http://www.pixelglow.com/macstl. Sadly it has languished over the years but I would love to work on it again. It had novel (at the time?) vectorized trigonometric functions, for example.
> C is not that fast. One of the major problems is that it's close to hardware.
These days the most important factor to performance, in almost all programs, is memory accesses.
Writing cache-friendly code is made relatively easy by C. You control the placement of your data into aligned cache lines. You control the order in which the cache lines are accessed.
The cost of the computational instructions is usually swallowed by the memory misses.
C's common ABI (for x86/64) now passes up to 6 parameters via registers, and doesn't unnecessarily spill to the stack. Additionally, link-time optimizations now allow cross-compilation-unit inlining which can optimize away the ABI costs.
One performance issue C has is 0 terminated strings. String processing code is constantly hampered by having to do strlen or the equivalent to find the end of the string. 0 terminated strings also means that 'slices' of strings cannot be strings themselves, a copy must be made.
Of course, a C program doesn't have to use C strings, but so many libraries and APIs do (including the C standard library) that they are unavoidable for most practical purposes.
D eliminates this problem by using dynamic arrays to represent strings. A dynamic array is a pointer/length pair.
As a counterpoint, Alex Stepanov (the primary designer and implementer of the C++ Standard Template Library), had this crucial insight about the C programming language pervasiveness:
Let's consider now why C is a great language. It is commonly believed that C is a hack which was successful because Unix was written in it. I disagree. Over a long period of time computer architectures evolved, not because of some clever people figuring how to evolve architectures---as a matter of fact, clever people were pushing tagged architectures during that period of time---but because of the demands of different programmers to solve real problems. Computers that were able to deal just with numbers evolved into computers with byte-addressable memory, flat address spaces, and pointers. This was a natural evolution reflecting the growing set of problems that people were solving. C, reflecting the genius of Dennis Ritchie, provided a minimal model of the computer that had evolved over 30 years. C was not a quick hack. As computers evolved to handle all kinds of problems, C, being the minimal model of such a computer, became a very powerful language to solve all kinds of problems in different domains very effectively. This is the secret of C's portability: it is the best representation of an abstract computer that we have. Of course, the abstraction is done over the set of real computers, not some imaginary computational devices. Moreover, people could understand the machine model behind C. It is much easier for an average engineer to understand the machine model behind C than the machine model behind Ada or even Scheme. C succeeded because it was doing the right thing, not because of AT&T promoting it or Unix being written with it.
I think there's a cause and effect fallacy in that statement. C is a good representation of an abstract computer because alternative representations have died out. C (and very similar imperative languages) keep a strong arm pressure on CPU designers to work well with C.
I've spent some time working on VLIW architectures where C is a terrible abstract representation of the CPU's inner workings. Get anything done efficiently was awful and required huge, cumbersome, carefully structured intrinsics. Or raw assembly.
Is this is fault of VLIW? Not really. Its mostly the reality of a world that would rather run C code than use VLIW architectures. So we don't have VLIW or other CPU architectures that don't work well with C.
"people could understand the machine model behind C" -> that is why every substantial C program is ridden with undefined behavior, overflows, memory leaks, race conditions....
"C succeeded because it was doing the right thing" -> If the right thing is giving rise to the software exploit industry and causing billions in damages..
I suggest you read Richard Gabriel's "Worse is Better"
(http://www.jwz.org/doc/worse-is-better.html) and forget
anything that Stepanov has to say on the matter. Either
he is utterly clueless or a dangerous imbecile.
C has been in use for decades now. We only have to look
at the facts, not listen to fallacies that various cretins
feel the urge to proclaim.
Another problem is that C has strict contracts on how parameters are to be passed through, and combined with separate compilation units, this hurts compilers when they try to optimize things
Calling conventions can be a bottleneck but this is not unique to C, any other compiled language allowing separate compilation units to be linked together has the same issue. Techniques like LTCG can avoid it.
But given that C is almost always at or near the top of benchmarks both for size and speed, maybe it's not that much of a bottleneck after all.
61 comments
[ 2.9 ms ] story [ 148 ms ] threadPlus, it's not like semicolons are any big deal. If they are in a language, you add them and move on. Dead simple to add, minimal noise, instantly familiar to most C-derivative programmers. The only comminity that regularly complaints about them are hipster (for lack of a better term) javascript programmers.
But your post would still be decently readable if you just made a new paragraph for each sentence ;)
I like Python. I think Python's whitespace-based blocking is interesting. But it is not the most interesting part of the language, and anybody who obsesses over that kind of stuff is just bikeshedding.
Developers always seem to be complaining about having to type this or that, when they should be much more concerned with what they have to read!
Really, I couldn't care less about typing a few extra characters. I can type pretty fast anyway and usually spend more time thinking than I do typing. What I do care about is being able to tell exactly what some code does by glancing at it, without worrying too much about whether someone wrote = instead of ==. Rust seems to be setting itself up for loads of those kinds of errors by trying to make the syntax terse at the expense of making it readable.
I have high hopes for Rust and will reserve judgement until it is stable; from what I've seen it's still in a high state of flux. However for now, I much prefer the KISS approach taken by Go, in spite of a couple of things missing from the language (that will probably get there in the end).
Or maybe they find terser syntax to be more readable. If verboseness was always more readable, if not necessarily more "writable", than terseness, then no one would have a problem with Java since it has good IDE support, including autocomplete and generation of boilerplate code. But it turns out that it's not just a matter of being lazy typists.
I'm pretty comfortable writing complex regular expressions, but I don't know many other developers that are and I certainly don't like trying to grok anything longer than about 10-15 characters that I didn't write in the preceding 15 minutes. This is a perfect example of where terseness is fine for simple problems, but it does not scale.
I already mentioned Go because it has a pretty terse syntax, but one that is very carefully optimised for readability. One of the things the designers of Go are careful about is not adding too many operators, keywords, or usage rules to the language, which keeps everything nice and simple.
Rust OTOH seems to have a metric boatload of operators that work in different ways depending on the specific context. That's a recipe for a ton of cognitive load, which isn't helpful for writing code, but reading suffers even more.
I don't yet know enough about Rust to say whether this is as bad as operator overloading in C++. As I said, I will reserve judgement until 1.0 because it's entirely possible things will change drastically before then.
What operators does Rust have that Go does not? I believe Rust has no more operators than Go.
You're right.
That is two separate levels. When you first glance a page of code the first time in 1 second, you should tell what the structure of the program is. How many blocks (for/while/if) it has. How many functions, how big they. You haven't yet had time to read each individual character. That is one level.
Here variable and ambiguous indentation rules get in the way. If there is non-uniform, non-standard indentation then you have to start reading individual lines in detail. If there is standard indentation then it doesn't even matter about little commas and semicolons, it is a level higher than that.
Then past that it is about individual functions, classes, modules, and what have you. Then it becomes about == vs = or . vs , and so on. If there is ambiguity there it could be harder. Like in Python I added a , at the end of a some variable. So that turns it into a tuple. And it resulted in a strange exception down the line. In C the = vs == is notorious. But there are others. None of this make the task impossible, but just slightly harder. The problem is that if this is done many many time over the course of a lifetime of a piece of code. Maybe it take 10 extra seconds for reader to understand the code, that multiplied by thousands of times will add up.
Prefer "x = y + z" to "ADD y TO z GIVING x".
Prefer "[[NSThread alloc] initWithTarget:t selector:s object:o]" to "pthread_create(&p, &a, &f)".
That the last value in a block is the value of the expression is something that makes a lot of code much more readable, as it frequently obviates the need for additional temporary variables.
I was sceptical until I actually used it. Now I’m converted; though I still also like Python’s pure statement-oriented approach in various ways, I prefer Rust’s model.
https://github.com/rust-lang/rust/blob/master/src/librustc/m...
Personally I think most other things in Rust (namespaces, constant as_slice() unwrap() etc.) are currently too verbose, although I'd say that also impedes readability.
I do agree with the rest of your first paragraph though.
We've written hundreds of thousands of lines of Rust and this has never been an issue. The typechecker will catch any misuses of semicolons.
> What I do care about is being able to tell exactly what some code does by glancing at it, without worrying too much about whether someone wrote = instead of ==.
The typechecker will catch misuses of = versus == as well. So assuming that the code you're looking at passes the typechecker, you don't have to mentally distinguish between = and ==.
> However for now, I much prefer the KISS approach taken by Go, in spite of a couple of things missing from the language (that will probably get there in the end).
They're different languages. Go does not have memory safety without garbage collection, and will never have it while remaining backwards compatible. But that was an explicit design goal of Rust. That is why Rust has the lifetime and unique pointer support, which allows Rust to support safe low-level programming in a way that wasn't possible before.
> This is like optimising for the least readable code possible. I imagine it's going to be a nightmare in practise.
I have pushed a reasonable amount of code to the rust repo, my own libraries, and some to servo, and it has never been an issue, in fact it has been the opposite (proof: https://www.ohloh.net/accounts/bjz and https://github.com/bjz/).
> What I do care about is being able to tell exactly what some code does by glancing at it, without worrying too much about whether someone wrote = instead of ==.
Rust solves this by having assignment expressions always returning `()` from assignment expressions and not having implicit conversions. The issues with `==` vs `=` completely vanish.
Rust code is very readable despite this quirk. It's a different language. There are things to learn. Get over it.
It seems that one can choose to both use runtime assertions and static proofs of correctness proofs in Spark. But I don't know if that extends to statically ensuring memory safety in low-level code.
On the early days it was deemed too complex to implement, although I would say C++ became even more complex.
The companies that sold Ada compilers had customers with deep pockets, so Ada compilers were too expensive and required worksations to be used properly.
When affordable Ada compilers became available, not many cared about it.
Nowadays it has found its place where human lifes are at risk. Many avionic systems, train control systems, hospital devices are coded in Ada.
I do attend FOSDEM regularly and also get the feeling its use is increasing in Europe thanks to the security exploits in languages tainted by C compatibility.
C is not that fast. One of the major problems is that it's close to hardware. 1970s hardware that is. Ken Thompson reportedly once said: "I'm not going to do nibbles. I have an 8 bit processor".
A good example of how bad it has become is that modern processors have a rather good understanding of the 'string' concept, and offer instructions to process them. C offers a char*.
Another problem is that C has strict contracts on how parameters are to be passed through, and combined with separate compilation units, this hurts compilers when they try to optimize things.
I see greater potential for a safe higher level language to be able to align closer to modern day hardware than C. Some nice examples: Linear types can avoid garbage altogether, and coroutines can be expressed clearly and correctly using monads. On the other hand, raw performance is rarely needed, and most cycles are burned interpreting things like python and php.
Why is that so? Do more modern languages need another twenty years before they can compete with C (and eventually beat it performance-wise) ?
"Not that fast" doesn't necessarily mean that it isn't the fastest we have got.
When I started coding in the mid-80's C compilers were dog slow and no better than the alternatives (Pascal, PL/I, Modula-2, Cedar, ...).
In fact, game developers would talk about high level languages the same way they talk about current modern languages vs C/C++, and use nothing else other than Assembly.
However with UNIX spreading into the enterprise and bringing C along, it meant compiler vendors focused on optimizing for the language they were getting money for.
The main issue when people compare languages, is that they forget although the design drives the implementation, not all implementations are alike.
ps: for instance in some cases, jitted code will go faster than C, some Jitted kernels (never tested personally) allowed for a good 30% performance increase.
So where are all those modern CPU features exposed in C?
The x86 string functionality that I'm aware of was directly derived from C string functionality, and any decent library of course uses them. You don't directly map to those underlying opcodes because that would be silly, and would completely undermine any platform independence you might have.
The same for vectorization. You don't explicitly express vectorization in your code, but of course all decent C compilers can easily and robustly generate such code.
C isn't close to the hardware (beyond very high level notions like "contiguous memory"). But it's a simple enough language that it's very heavily optimizable.
From what I've seen auto-vectorization is anything but robust, often you need non-portable annotations to get anything decent. See e.g.: http://locklessinc.com/articles/vectorize/ where there's an interesting case where using "if" instead of a ternary operator is enough to prevent optimization.
The string instructions tend to be slower than using generic instructions, especially in the presence of SSE.
The simplicity of C means you can't make many assumptions of how it is handled. Same problem in C++. You can make a lot of optimizations to assembler if you can guarantee a value won't change, or if the relative location in memory needs to remain static because anything can rip the address and do awful pointer arithmetic with it.
I like to compare to asm.js - it is a reduced language in that it restricts the environment from doing things it can't easily optimize away, except in that case its to assembler. Reduced set versions of C exist to do the same already, but it would be nicer if you just had a fast language default with straightforward rules with ways to just declare wonderland functionality that can mess things up that means the compiler needs to preserve the machine assumptions rather than the language structures.
I once wrote a SIMD C++ template library based on valarray: http://www.pixelglow.com/macstl. Sadly it has languished over the years but I would love to work on it again. It had novel (at the time?) vectorized trigonometric functions, for example.
These days the most important factor to performance, in almost all programs, is memory accesses.
Writing cache-friendly code is made relatively easy by C. You control the placement of your data into aligned cache lines. You control the order in which the cache lines are accessed.
The cost of the computational instructions is usually swallowed by the memory misses.
C's common ABI (for x86/64) now passes up to 6 parameters via registers, and doesn't unnecessarily spill to the stack. Additionally, link-time optimizations now allow cross-compilation-unit inlining which can optimize away the ABI costs.
Of course, a C program doesn't have to use C strings, but so many libraries and APIs do (including the C standard library) that they are unavoidable for most practical purposes.
D eliminates this problem by using dynamic arrays to represent strings. A dynamic array is a pointer/length pair.
Let's consider now why C is a great language. It is commonly believed that C is a hack which was successful because Unix was written in it. I disagree. Over a long period of time computer architectures evolved, not because of some clever people figuring how to evolve architectures---as a matter of fact, clever people were pushing tagged architectures during that period of time---but because of the demands of different programmers to solve real problems. Computers that were able to deal just with numbers evolved into computers with byte-addressable memory, flat address spaces, and pointers. This was a natural evolution reflecting the growing set of problems that people were solving. C, reflecting the genius of Dennis Ritchie, provided a minimal model of the computer that had evolved over 30 years. C was not a quick hack. As computers evolved to handle all kinds of problems, C, being the minimal model of such a computer, became a very powerful language to solve all kinds of problems in different domains very effectively. This is the secret of C's portability: it is the best representation of an abstract computer that we have. Of course, the abstraction is done over the set of real computers, not some imaginary computational devices. Moreover, people could understand the machine model behind C. It is much easier for an average engineer to understand the machine model behind C than the machine model behind Ada or even Scheme. C succeeded because it was doing the right thing, not because of AT&T promoting it or Unix being written with it.
I've spent some time working on VLIW architectures where C is a terrible abstract representation of the CPU's inner workings. Get anything done efficiently was awful and required huge, cumbersome, carefully structured intrinsics. Or raw assembly.
Is this is fault of VLIW? Not really. Its mostly the reality of a world that would rather run C code than use VLIW architectures. So we don't have VLIW or other CPU architectures that don't work well with C.
"people could understand the machine model behind C" -> that is why every substantial C program is ridden with undefined behavior, overflows, memory leaks, race conditions....
"C succeeded because it was doing the right thing" -> If the right thing is giving rise to the software exploit industry and causing billions in damages..
I suggest you read Richard Gabriel's "Worse is Better" (http://www.jwz.org/doc/worse-is-better.html) and forget anything that Stepanov has to say on the matter. Either he is utterly clueless or a dangerous imbecile.
C has been in use for decades now. We only have to look at the facts, not listen to fallacies that various cretins feel the urge to proclaim.
Back in th 8 and 16 bit days of the home computer systems, I never felt the need to use C.
I could touch all the hardware using Assembly, Turbo Pascal.
Apple was using Object Pascal for system programming and a few friends were doing Amiga games with Amos.
Sure I did learn C at high school, but it only became a requirement for the university work that had to run on UNIX systems.
Calling conventions can be a bottleneck but this is not unique to C, any other compiled language allowing separate compilation units to be linked together has the same issue. Techniques like LTCG can avoid it.
But given that C is almost always at or near the top of benchmarks both for size and speed, maybe it's not that much of a bottleneck after all.
I hadn't heard of this -- is it still true now that segmented stacks are gone? If so, why does C need a separate stack?
tldr; C's stack is interleaved with Rust's stack.