Hacker News new | past | comments | ask | show | jobs | submit login
What is C in practice? (cam.ac.uk)
192 points by 0x09 on June 29, 2015 | hide | past | web | favorite | 106 comments

It remains unclear what behaviour compilers currently provide (or should provide) for this.

It might be nice if future surveys explicitly asked a followup question "Regardless of the standard or behavior of existing compilers, is there one of these answers that is the 'obviously correct' manner in which compilers should behave? Which one?"

If practically all users believe that the same answer is 'obviously correct', compiler writes might want to take this into account when deciding which behavior to implement.

  For MSVC, one respondent said:

  "I am aware of a significant divergence between the LLVM 
  community and MSVC here; in general LLVM uses "undefined 
  behaviour" to mean "we can miscompile the program and get 
  better benchmarks", whereas MSVC regards "undefined 
  behaviour" as "we might have a security vulnerability so 
  this is a compile error / build break". First, there is 
  reading an uninitialized variable (i.e. something which 
  does not necessarily have a memory location); that should 
  always be a compile error. Period. Second, there is reading 
  a partially initialised struct (i.e. reading some memory 
  whose contents are only partly defined). That should give a 
  compile error/warning or static analysis warning if 
  detectable. If not detectable it should give the actual 
  contents of the memory (be stable). I am strongly with the 
  MSVC folks on this one - if the compiler can tell at 
  compile time that anything is undefined then it should 
  error out. Security problems are a real problem for the 
  whole industry and should not be included deliberately by 
I'm much less familiar with MSVC than the alternatives, but this is a refreshing approach. Yes, give me a mode that refuses to silently rewrite undefined behavior. Is MSVC possibly able to take this approach because it isn't trying to be compliant to modern C standards? Does it actually reduce the ability to apply useful optimizations? Or just a difference in philosophy?

While I am quite sympathetic to the "break the build on undefined behavior" position, I have a nit to pick in the quoted passage.

Initialization of a variable has no relation to whether it has a memory location. You can legitimately take the address of an uninitialized variable (that is often one step in initializing them, such as when you pass the address to memset), and even an initialized variable may not have a memory location (if it lives only in register, or is compiled away entirely).

To pick your nit, note that they specifically said reading an uninitialized or partially initialized variable, not reading the address of the same.

I didn't mean to imply (and don't think I did...) that reading an uninitialized variable is not a problem, just that the problem is entirely unrelated to whether it has a memory location.

Hate to ask the stupid question, but I've been wondering and it seems to be along the same lines... Why can't this be valid?

int f; f = 2;

To me this says, there is an int pointer f. Let f point to 2. Is this not possible only because 2 does not occupy memory? I don't see why this couldn't be valid.

Assuming you're asking about

    int *f; *f = 2;

The statement,

    *f = 2;
says "dereference f to get a location; set the contents of that location to 2". "Set f to the location of 2" would be spelled

    f = &2;
This is partly a matter of the difference between assignment and definition(/equality).

The first of these is invalid C (undefined behavior) because it uses a value (the contents of f) before initialization.

The second is invalid C (compile error) because 2 is not an l-value (that is, it does not have a location).

Edited to add: I will note that I don't think there is any reason

    *f = 2;
couldn't mean to give f the addess of 2, as by analogy to structural pattern matching, but that the syntax is already taken for something else.

And you might want to access a memory location legitimately that was not written by this program, for instance a chunk of dual-port memory, a shared memory block, a piece of memory mapped hardware and so on.

In those cases some compiler override could provide the solution, while still allowing the compiler to flag all the other cases as errors.

Your examples are not expected to work unless you specifically ask the compiler for it.

If you use the volatile key word, then you are guaranteed that all reads and stores will actually happen and not be optimized out. But volatile still allows non-atomicity and reordering so you probably want something which prohibits that too.

You mean volatile memory?

None of those are going to be autos though, so the behavior isn't undefined.

> If practically all users believe that the same answer is 'obviously correct', compiler writes might want to take this into account

The problem with this democratic approach is that most users are not qualified such that their opinion is particularly valuable.

The small minority who doesn't find it "obviously correct" may actually in some cases be the minority with a clue.

It could work if only those users are given a vote who pass a language lawyer exam.

> The problem with this democratic approach is that most users are not qualified such that their opinion is particularly valuable.

They aren't advocating a democratic approach, they are saying that the general expectations of users is valuable information.

You don't rely exclusively on what the users want, but users expectations for how compilers behave is very valuable behavior, whether it means finding out how to inform users of the real behavior or changing the behavior.

"First, there is reading an uninitialized variable (i.e. something which does not necessarily have a memory location); that should always be a compile error. Period."

You cannot implement that efficiently without rejecting valid programs. Consider code like this:

  int x;
  if( f()) x = 1;
  if( g()) h(x);
For sufficiently complex functions f and g, there's no reasonable way to decide at compile time whether x will be set whenever g() returns true. For example, f() might always return false because Fermat's last theorem is true.

And that 'reasonable' likely isn't a necessary part of that statement.

Yes, this is exactly the reason for undefined behavior. It is often overlooked in the "well, the compiler should just throw an error instead of invoking undefined behavior" debate: the compiler doesn't just "choose to invoke" undefined behavior: it relies on it to do program analysis.

For example, your example could legally be rewritten to:

    int x;
    x = 1;
    if (g()) { h(x); }
The only difference is if f() were false, and someone would then access x and see 1; but that's undefined behavior, so you can ignore it. In fact, assuming x is not accessible outside this block:

    if (g()) { h(1); }
These optimizations happen all over the place, it's not the compiler invoking or causing undefined behavior, but assuming that it won't ever happen.

EDIT: Note the further optimization that looms: if f() can be proven pure (no side effects), then it can be removed. This makes little sense for a function with no arguments (in which case it would just be a constant). If, however, f(y, ...) is some expensive but pure function, it can just be removed completely.

"Note the further optimization that looms: if f() can be proven pure (no side effects), then it can be removed. This makes little sense for a function with no arguments (in which case it would just be a constant)."

It could apply if f computes a value based on global state but doesn't change anything, which is slightly weaker than "pure".

Why does this have to be a valid program? There is obviously a chance that x is undefined at line 3 so why not allow the compiler to throw an error?

Not necessarily. Just because the compiler can't prove that g only returns true iff f also returned true doesn't mean the programmer doesn't know it to actually be true.

So changing that behavior means that the compiler now rejects 40 years worth of correctly working legacy code (and some buggy code, as well). Newer languages (e.g. Java, C#) that don't have to support existing code can afford to do what you want and reject programs where a simple heuristic isn't enough to tell whether a variable is initialized or not.

You can already break a lot of programs with -Wall -Werror (I generally compile my code with both, but turn them off for libraries). Just hide the amazing optimisation behind an -foptimise-undefined-behaviour which you can only turn on if you've specified at least a significant portion of -Wall.

So don't raise an error, instead publish a warning.

I think the idea is to give an error when there is absolutely no way that variable could have been initialised when it's read, i.e. cases like this:

    int x;
The point is not to solve the Halting Problem, but to catch code that is so obviously wrong that no programmer would deliberately write it (unless they were testing the compiler's reaction.)

If so, "that should always be a compile error. Period.” is IMO a poor way to express that idea.

On top of that, the C compilers I know more or less have that, as they give warnings for basically cases where Java (with its stricter rules) would refuse to compile equivalent code, and have a flag that turns warnings into errors.

The Java compiler WILL reject this program, because x is not initialised on all code-paths. This is not a valid program.

That should be rejected as an invalid program, you'd fix it by initializing x to a sane default.

Or by putting the second if inside the body of the first.

Often there is no sane default value.

Putting the second if inside the body of the first is not necessarily equivalent.

g() might have side-effects, and those side-effects have to happen regardless of whether or not f() returned true.

Then the correct code was something like:

    if (f()) {
        int x = 1;

        if (g()) {

    } else {
If g() can be called before f() then it could also be written as:

    int g_flag = g();

    if (f()) {
        int x = 1;

        if (g_flag) {
If you really only had one assignment to x, and both f() and g() have side effects that must be run in-order, this can be more clearly written as:

    int f_flag = f(); // force f() to run for ${reasons}
    int g_flag = g(); // force g() to run for ${reasons}

    if (f_flag && g_flag) {
This is a lot clearer about what the code is doing, and the compiler is probably going to optimize away the two int flag variables anyway.

You should never rely on uinitialized values not just because of the problems relating to undefined behavior, but also because you're adding in assumptions about the runtime state. The code depended on the return value of f() but did not fully express that dependency in the code.

It's true that if the only change you make is wrapping braces from "x = 1;" through the end of the quoted code, you would need to be sure that g does not have desired side effects. Otherwise, you could lift both the f() and g() calls above the branching (which still leaves the second if inside the body of the first, as I described).

Valid as well.

My guess would be that LLVM is not so much motivated by better benchmarks. You could always make it a parameter. The bigger issue usually is: If we mark something as an error, which is actually allowed by the C standard, then lots big old libraries will not compile anymore and nobody is willing to fix all the old code.

If we miscompile (or outright delete) something which is marked "undefined behaviour" by the C standard, then lots of old libraries will build without errors, but fail in various fun ways during use, including having severe security issues. I think failing the build is better than giving me a library with deleted NULL checks and whatnot.

> then lots big old libraries will not compile anymore and nobody is willing to fix all the old code.

This is begging for one question in my opinion. Should we keep using old libraries that nobody is maintaining anymore? Isn't that a big security issue?

Can it be put behind a flag? If some library fails to compile, toss the flag into the toolchain invocation and try again?

MSVC will very confidently warn on cases like this by default:

    int foo() {
        int bar;
        return bar + 5; /* C4700: local variable 'bar' used without having been initialized */

In the same situation, GCC says "may be used uninitialized in this function" if you enable the warning (-Wmaybe-uninitialized), despite this being a trivial and certain case.

gcc 4.9.2 gives: warning: 'bar' is used uninitialized in this function

Maybe you are using a old version.

If you enable -Wall which you always should then this flag is automatically enabled.


-Wuninitialized has been broken in GCC for over a decade: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=18501

As if we needed more proof that GCC is a fucking joke...

Hey Linus, how are you doing!

While I like MSVC team approach to this I don't think it's necessarily worse to just warn as long as that warning consistently work. I mean, just add some warning options to your compiling script and you are good to go. I am getting this error from GCC very consistently and as good as compilation error in practice.

First, there is reading an uninitialized variable (i.e. something which does not necessarily have a memory location); that should always be a compile error. Period.

You're out of luck. You have to solve the halting problem to statically analyze whether or not a variable will be used when it's undefined. The reason why this is not solved well is because it's impossible to solve perfectly! Java made another approach that I hate: For example if you have

    Object a;
    for(int i = 0; i < 1;++i) a = new Object();
then Java will give you

    error: variable a might not have been initialized  
even though it's plain and obvious that a is initialized. Things like that make me mad, it's a non-solution.

I'm not asking for the compiler to consistently identify undefined behavior, only to have a mode where it refuses to silently make 'optimizations' when it does identify UB.

The parallel to your example would be if the for loop was "for(int i = 0; i < j;++i)". If the compiler was able to determine that there is a code path whereby j might be undefined, should it be allowed to remove the body of the loop, even in those cases where the programmer knows by other means that "j >= 1"?

My request is that it either keep the loop body, or complain about the undefined behavior, but not silently make 'optimizations' based on the fact that it has identified the potential for undefined behavior to occur.

Note that I'm just using an 'uninitialized variable' as a hypothetical example. Given a chance, I always compile with -Wall -Wextra, and in practice, GCC, CLang, and ICC (the compilers I use) do a good job of issuing warnings for the use of uninitialized variables. I like this current behavior, but would prefer a philosophical approach that makes warnings like this more rather than less common.

I agree in cases where the compiler knows that undefined behavior is taking place. A lot of the silent optimizations LLVM and GCC make are in cases where the compiler isn't really sure it has identified undefined behavior, though.

To put it in classical logic terminology, one case is modus ponens reasoning. Undefined behavior implies the compiler can do whatever it wants. The compiler finds undefined behavior. Therefore it does whatever it wants. This is the case where it'd be better for the compiler to error out than do something nutty.

But many of the optimizations are doing modus tollens reasoning. If X were true, then the program would perform undefined behavior. Conforming programs do not perform undefined behavior. Therefore NOT-X must hold in conforming programs, and this fact can be used in optimizations.

> If the compiler was able to determine that there is a code path whereby j might be undefined, should it be allowed to remove the body of the loop, even in those cases where the programmer knows by other means that "j >= 1"?

No; rather, the correct logic is that compiler must preserve the body of the loop if there exists the possibility that it can be reached by a valid code path without any undefined behavior (j is defined, and so forth). Only if the compiler can prove that no well-defined execution path can reach the body can it remove it.

(A bad idea to do without any warning, though. If undefined behavior is confirmed, it should be diagnosed.)

> only to have a mode where it refuses to silently make 'optimizations' when it does identify UB.

That's not always possible. It's not that it makes the optimization when it identifies UB. It's that it makes an optimization that is valid to make if UB doesn't occur, but if UB were to occur then that optimization could cause all kinds of unexpected problems. But the compiler can't necessarily identify those cases.

Please read the "what every C programmer should know about undefined behavior" series of articles from LLVM; they describe the reason why they can't, in general, provide warnings or errors for these cases in which optimizations rely on lack of undefined behavior:




The third article describes why the compiler can't, in general, warn about those cases in which it's relying on lack of UB, but you should read the first two as well.

Note that for some of those cases, clang and GCC have recently added undefined behavior sanitizers, invoked via "-fsanitize=undefined", which can help even more than the warnings they can add. However, what they do is add extra instrumentation to the executable, and then either log a warning or crash when you hit undefined behavior. The runtime aspect helps avoid the "getting this right would involve solving the halting problem" aspect of why they can't, in general, provide appropriate warnings, but it does mean that this is generally only appropriate in test builds, and that you will only find the undefined behavior that you can trigger during test, while there may be more hiding that only show up in obscure circumstances.

If you really don't want undefined behavior, it's best to use a language, like Rust, which does not have any undefined behavior (outside of "unsafe" blocks). The problem with any kind of warnings that are tacked on after the design of the language is that you are either going to get lots of false positives, lots of false negatives, or both. With a language that is designed not to allow undefined behavior, you know that if the code compiles, it doesn't invoke UB.

Fyi Rust has solved this problem; the compiler forbids the use of undefined variables. Compile error example:

    let x;
    println!("x:{}", x);

Not a rust programmer so forgive me if this is a dumb question.

Sometimes in C one might initialize a variable by passing a pointer to it to an init function:

  void f( void )
    int i;
    bool success;

    success = init( &i );

    if ( success )
      do_stuff( i );
That "init" function might be located in a separate .c file, so there's no way for the compiler to know whether or not the memory whose address is passed to init is initialized or not. So how can Rust "solve" the problem? Does Rust simply not allow taking addresses of variables? Or does it not use .o files, compile all codefiles at once and actually analyze globally for uninitialized variables?

The `init` function you use would not be valid. You would instead write something like this:

    fn f() {
        if let Some(i) = init() {
In this case, the `init` function would return an `Option<i32>`. In a failure state, this would return `None`, and the pattern match would fail. In a success state, this would return `Some(i)`, where i corresponds to the variable you describe.

The Rust pattern is not only safer, but briefer than yours. It describes the code flow such that you can't remove or repeat a part and end up with inadvertently broken code, and it's memory safe. There is no way for `init` to blow up the stack (whereas in your example, a malicious or buggy init can use the address of i to smash the stack.)

Rust doesn't allow this direct use-case. You can have conditionally-successful initialization by returning an `Option` or `Result`, however.

This should be an error and you should be required to write

    int i = 0; // or some other default value

And dozens of other languages.

I love Java's behaviour. It gives a few false positives as you've noticed, but you can fix that just by doing

    Object a = null;
and go on your way. You do run the risk of a null pointer exception if you don't assign it a valid object reference, though.

Main problem with breaking on undefined behavior is detecting it in the first place. It's not a choice not to warn/break it's an implementation challenge to find those.

I think one could reliably break (more safely) on undefined behavior by inserting defensive run-time checks. Whether that's worth the performance hit is liable to be domain specific.

There is a relevant article http://blog.regehr.org/archives/1180 and discussion https://news.ycombinator.com/item?id=8233484 about what programmers really think C should be like (i.e. the "portable assembler" it was designed to be originally); it incidentally also shows what they think a sane machine architecture should be like.

Question 2 is :

Is reading an uninitialised variable or struct member (with a current mainstream compiler):

(This might either be due to a bug or be intentional, e.g. when copying a partially initialised struct, or to output, hash, or set some bits of a value that may have been partially initialised.)

a) undefined behaviour (meaning that the compiler is free to arbitrarily miscompile the program, with or without a warning) : 128 (43%)

b) ( * ) going to make the result of any expression involving that value unpredictable : 41 (13%)

c) ( * ) going to give an arbitrary and unstable value (maybe with a different value if you read again) : 20 ( 6%)

d) ( * ) going to give an arbitrary but stable value (with the same value if you read again) : 102 (34%)

e) don't know : 3 ( 1%)

f) I don't know what the question is asking : 2 ( 0%)


I know of one datastructure (a sparse set of integers from 1-n) which relies on this behavior: http://research.swtch.com/sparse . I always thought it was a neat trick. However, from the article is seems that may NOT give stable values to uninitialized members. Which may make that data structure behave strangly or cause the program to miscompile.

Generally speaking, it depends on where the uninitialized value is located (Though, AFAIK, by the standard they are all to be considered unstable for the most part). The big catch is stack-allocated uninitialized variables. What gcc may do (and I presume clang/LLVM too) is assign an uninitialized variable to a register, but then never give it any stack-space. So, what happens is that the code my always treat register 'a' as though it contains variable 'i', but since variable 'i' is uninitialized, it never allocates or reads anything from the stack, so the value of 'i' just becomes whatever happens to be inside of register 'a' before you attempted to use it. This would cause the variable to appear to randomly change from one value to another, even during single statements, depending on what register 'a' get's used for.

For the sparse set of integers, generally speaking that's going to be allocated somewhere else all at once, so there is not much the compiler can do to 'realize' it's uninitialized and decide to just ignore the read from memory completely. A fancy compiler could presumably flag every uninitialized location, then do checks and use some random value every-time you attempt to use one, but practically speaking no compiler is going to do that, so this data-structure isn't technically standards compliant, but it should probably still work anyway.

That's a question for which multiple answers are correct: a, b, and c.

I disagree regarding b. I think you'll find the outcome of this expression quite predictable:

int b; int c = b * 0;

Sure, if the expression doesn't depend on the value at all, it probably won't have unpredictable results. (Though as with any kind of undefined behavior, don't count on it, as compilers can be "clever" sometimes.)

A lot of C programmers make valid assumptions based on their system architecture and compiler. Is there any point trying to unify their obviously different practices, while at the same time ignore the Standard? No.

Many C programmers assume a single flat memory space. Most machines today have that. There's a long history of machines that didn't: Intel 286 in segmented mode, some Pentium variants in segmented mode beyond 4MB (Linux supports this in the kernel), IBM AS/400, Unisys Series B 36-bit word machines, Burroughs 5500/6700/Unisys Series A segmented machines where an address is a file-like path, Motorola CPUs where I/O space and memory space are distinct, and old DEC PDP-11 machines where code and data address spaces are separate. More recently, there are non shared memory multiprocessors where addresses are duplicated across processors - the PS3's Cell and many GPUs, for example.

Most programmers today will never encounter any of those.

There's also the assumption that null is address 0. Notably breaking the purity of C++s type system with magic 0s for 20 odd years before nullptr came along.

Actually the C++ spec says that 0 as a pointer doesn't actually have to have the value 0 - it just refers to a null value the same way as nullptr.

That is the point. They went through the process of tightening down the type system by invalidating automatic casts and making void* less promiscuous and then they break that whole philosophy with a magic literal "0" that can work as a normal integer or as an address placeholder for any pointer even though it won't necessarily evaluate to address zero. At that point why can't I assign "0" to a float too and enjoy some more magic there?

To me, the whole point of C is to enable one to take advantage of one's system architecture. Thats not really possible in higher level languages.

Well, one of the interesting takeaways from the article is that C as specified often does not let you take advantage of your system architecture. The specification has things like GPUs and segmented memory architectures in mind when it forbids you from doing seemingly reasonable things like taking the difference between the addresses of two separately-allocated objects, even though chances are very good what you're trying to do works just fine on all architectures you care about.

Yes and no. The standard specification defines a C that is very portable. It is quite reasonable that you cannot portably take advantage of your system architecture, which means you cannot do it using C as defined by the standard.

But you still can write stuff that looks just like C, that a C compiler will accept, and that lets you take advantage of your system architecture. You just need to understand that a new version of your compiler can break all of your nifty tricks.

Finally someone that understands.

Not really because there are two kinds of c programer out there.

1. Those that communicate with the outside world exclusively via library calls to an operating system.

2. Those that do not.

People that live in world #1, usually don't get people living in world #2. Example.

  // wait for write ready
  while((spi.S & SPI_S_SPTEF_MASK) == 0)

  spi.D = data;
Guess what,

No spi.D; is actually important. No just because the program never writes to spi.S does not mean you can delete the while loop. No you cannot reorder any of this and have it work.

Are you saying something other than "you, the programmer, must declare spi to be volatile, or else the compiler might lay a bear trap for you?".

Based on your comment it seems clear that you understand that... but given that you understand that I don't get the point you're trying to make (except perhaps that writing low-level hardware code in C often means actively stopping the compiler from screwing you over).

(for non-C programmers: "volatile" can be approximated by "hey compiler, this variable can change unexpectedly even if you didn't do anything to change it, so don't use any optimizations that assume you're the only one changing it).

Sorry, I have no idea what you are trying to say. Maybe stick to the relevant issue and be less abstract?

His example is anything but abstract. Embedded programming is a world unto itself. You sometimes need to read an address before you can write, etc.

I think the parent meant "implicit" rather than "abstract". The code is not abstract; but the pragmatic intent of the comment is not made very clear. I believe I follow but I'm not confident enough to risk confusing things by guessing.

(I believe I follow the intended conversational import; I certainly follow the code.)

Sort of the point is, in pure languages only the symbolic operations are important. And the side effects (memory operations) are unimportant and beyond the control of the programmer. So you can do all sorts of transformation on the code without effecting the end result.

C is definitely impure. There is a large set usage cases where the memory layouts are important. Where the orders of operation and memory accesses are important.

What I worry about is when the optimizer is allowed to make assumptions about undefined behavior, that may be actually important on certain targets. Consider referencing a null pointer.

Winows7, Linux, in user land if you do that and you'll get a seg fault. And if the default handler runs your program dies.

The ARM cortex processor I've been slopping code for, reading a null pointer returns the initial stack pointer. Writing generates a bus fault. Since I actually trap that, and error message gets written into non-initialize memory and then the processor gets forcibly reset.

On an AVR reading a null pointer gives you the programs entry point. Writing to that location typically does nothing. Though you can write to it if running out of a special 'boot sector' and you've done some magic incantations.

So that's the problem I have with the idea that 'undefined means the optimizer can make hash of your program' instead of trusting the back end of the compiler will do something target appropriate.

Yes, embedded is very side-effect-ful. When you have to make that kind of thing work, you usually have to know both your hardware architecture and your compiler.

  *(unsigned long*)0xEF010014 = 0x1C;
presumes memory mapped I/O, a 32-bit hardware register at 0xEF010014, and what 0x1C will mean to that register. It also presumes that the compiler will generate a 32-bit access for an unsigned long.

Going back to Gibbon1's code sample, you have to know what your compiler is going to do at what optimization level. You either have to declare spi.S to be volatile, or you have to compile at -O0, or some such. And you may well need to review the generated assembly code a few times in order to really understand what your compiler is doing with your code.

If you're writing the Linux kernel and you want to be portable across hardware architectures, it gets harder. You can't just be processor- or hardware-specific. You probably need the volatile keyword. I don't know what the kernel compiles in terms of optimizations, but I bet it's not aggressively optimized.

His comment is abstract. A fragment of correct code doesn't magically change that.

Why? That's the issue that people should solve.

Disclaimer: I have no C experience.

Ive worked on embedded systems where stuff like that would be a problem, howver I also knew the memory map for my target.

I wanted to make some crypto run faster so i used memcpy on a pointer to a function to copy executable code from slow flash to fast ram.

"take advantage of one's system architecture."

Yes, if your system is essentially a PDP-11.

I don't mean that sarcastically; pcwalton's sibling message only begins to mention the ways in which C is not a match to modern systems. Vector processing, NUMA, umpteen caching layers, CPU features galore... it's not really a match to the "architecture" any more.

(To the extent that you may think C supports those things, I don't really think "lets you drop arbitrary assembler in the middle of a function" constitutes "support". YMMV. To be fair to C there's a lot of features that seem to be unsupported by any high-level language today. Hardware moves way faster than programming languages. If you want to figure out what's coming after the current generation of languages, "a language that actually lets you use all the capabilities of modern hardware without dropping to assembler and giving up all the safety of the higher-level language" is at least one possible Next Big Thing.)

That's why I am hopeful that OpenCL will get more mindshare. It is very fitting to a wide variety of architecture (CPU, GPU, FPGAs...).

C matches NUMA and caching just as well as assembly does. Basically the only thing C doesn't really expose is SIMD, but compiling "high-level" (i.e. C-level or above) code into SIMD is an open research problem (AFAIK) - I mean, you can easily compile array programs into SIMD, but the more general case is still open.

Really?! How do you specify alignment and packing in ANSI C?

_Alignof( ) and _Alignas( )?

Yeah, since C11 only.

However very few compilers, specially in the embedded and real time OS space do offer C11 compliance.

Gcc and clang aren't the only game in town. If one aims to write compliant C code much more compilers come into the picture.

Isn't it that if you write standard compliant C code, much less compilers come into the picture ;)

No, there is C89, C99 and C11.

What reduces the set of compilers is the reliance on compiler specific behaviours or recently approved standards, when compilers are still catching up.

"but compiling "high-level" (i.e. C-level or above) code into SIMD is an open research problem (AFAIK)"

In general, "taking a language never written for X and applying X on it" is an open problem. See also, for instance, trying to statically type a program in a dynamically-typed language. Of course it's hard to statically type a language that was designed to be dynamically typed. Of course it's hard to take C and make it do SIMD operations. You'd have to write a higher-level language designed to do SIMD from the beginning, if you really want it to be slick.

"C matches NUMA and caching just as well as assembly does."

I'm going to make a slightly different point, which is that C does not support caching as well as a language could. For one particular example, C still lays structs out as rows, and has no support for laying them out in columns. (It doesn't stop you, but you're going to be rewriting a lot of code if you change your mind later. Or doing some really funky things with macros, which is also rewriting lots of code, too.)

Of course C doesn't support this crazy optimization... when it was written the order-of-magnitude difference between CPUs and memory was much smaller, indeed outright nonexistent on some architectures of the time (though I can't promise C was on them). Computers changed.

(I'll give another idea that may or may not work: Creating a struct with built-in accessors designed to mediate between the in-RAM representation and the program interface, which the compiler will analyze and determine whether to inline into the memory struct or not. For instance, suppose you have two enumerations in your struct, one with 9 values and one with 28. There's 252 possible combinations, which could be packed into one byte, but naively, languages won't do that. This language could transparently decide whether to compute the values upon access, or unpack them into two bytes.)

Of course, virtually nothing does support this sort of thing, which is why I said C isn't really uniquely badly off... almost no current high-level languages are written for the machines we actually have today. (I'm hedging. I don't know of any that truly are, but maybe one could argue there's some scientific languages that are.) C is the water we all swim through, even if we hardly touch it directly, and ever since GPUs become standard-issue the mismatch between our programming languages and our hardware has become comically large.

Assembly supports everything, but by so doing so, supports nothing. It will of course permit you to lay your structs out in rows or columns or anything in between, but it doesn't particularly support any of them, either.

Incidentally, unlike some of my age, I don't actually complain about this; it is what it is, there are reasons for it, and what we have in hand is still pretty darned powerful. But, again, I'd suggest that if you are a language designer and you're looking to write the Next Big Thing, you could do worse than figure out how to start integrating this stuff into a programming language that can cache smarter and use the GPU in some sensible manner and all these other things. It's one way you might actually be able to create a language that will not merely tie C, but straight-up beat it.

I don't know of any way to compile non-array-ish, non-explicitly-SIMD-ish code into efficient SIMD code. Thinking about it, this seems to be the general case with parallelism - you can take fairly naïvely-written code, maybe add some memoization and hash-table-dictionaries, and it turns into not-too-inefficient sequential code, but I don't know of a way to do an equivalent with parallel code (especially if you add the memoization) - STM promises, but (AFAIK) still doesn't deliver.

Of course, the best way to have an SoA representation is to use an ECS :-).

C's main claim to fame is being "an HLL" (in the classical sense) but still being able to do essentially everything Assembly does. Also, having relatively-simple semantics surely helps (C is the only imperative language to have completely formalized semantics that I know of).

@qznc: I don't know of a way to do it natively in Rust (you must write accessors).

"I don't know of any way to compile non-array-ish, non-explicitly-SIMD-ish code into efficient SIMD code."

Which is why I'm proposing that somebody creating one might get some traction, after all...

"C's main claim to fame is being "an HLL" (in the classical sense) but still being able to do essentially everything Assembly does."

And I discussed at length the things that assembly can do today that C can not, without callouts to assembler. I mean, I know the party line, I've heard it for like twenty years now, and my entire point is that it's not true anymore. C is not a "high level assembler" for a 2015 machine. It's a high-level assembler for a PDP-11. Which is still useful enough, thanks to backwards compatibility, but it's high time for it to get out of the way and stop being "the high level assembler", just like it's high time for it to get out of the way and stop being "the systems language".

C certainly can do SIMD just as well as assembly (via intrinsics). It does not allow high-level code to be compiled to SIMD, but there is no known way to compile high-level general-purpose code to SIMD. Finding such a method is an Open Research Problem AFAIK (of course, solving it would be Very Welcome).

Your two-enums-in-a-byte example: I'm pretty sure I could do this in D. Hence, it should be possible in C++. Maybe Rust can do it as well.

It's possible to do manually in all those languages, but I'm not sure it can be done without programmer intervention: C++ and Rust both allow interior pointers/references to point to fields, which inhibits automatic application of many of the craziest layout changes.

I had to add that naively, languages won't do that, because of course there's no trick to it otherwise. Even Javascript can do it, it just won't be any faster. It's a simple example to fit a simple paragraph of text. One could imagine other possible optimizations to harness the fact that it's faster to do work on what you have than to pull more stuff in from RAM, like perhaps a string type that transparently compresses itself with a fixed dictionary (or optional dictionary) or something depending on runtime performance heuristics.

Of course anything I say is possible in an existing language, with enough work, enough assembler, and enough compromises, but it's not what languages are based around.

It is also not possible in C. People just think C is any better.

The standard doesn't say anything about SIMD, GPU, instruction reording, IO registers, interrupts...

All of that are language extensions or library functions written in Assembly. Any programming language can offer similar extensions.

I suspect the parent is more referring to people writing code that does e.g. foo(i++, i++); or e.g. relying on signed integer overflow behvior based on observing a toy program on their own machine - assuming the code will behave the same in all contexts, optimization levels or minor versions of the compiler

No, the whole point of C is to abstract away one's system architecture. C is a higher level language in this sense.

As noted in the article, a summary of this document (about one-third the length) from the same authors is available at http://www.cl.cam.ac.uk/~pes20/cerberus/notes51-2015-06-21-s...

It's scaring to notice that all this is undefined behavior. Hell, glibc network functions rely on undefined behavior!

Note that the C99 (I'm not up on C11 yet) standard specifically allows type-punning through the use of unions (and disallows it in essentially all other cases except when one type is a character type).

Also, there seems to be some confusion about storing and loading pointers, when the standard speaks to this as well; roughly: a pointer which is converted to a "large enough" integer type will point to the same object when converted back. It is permissible for an implementation to not provide a large enough integer type, but excepting that, the behavior is well defined.

C89 prohibited union type-punning. C99 allowed it, but mistakenly left the prohibition in the (non-normative) list of undefined behaviors. C11 did rectify the list.

One of the TCs for C99 fixed the list I believe.

As far as it's possible compilers should check as much as possible when false assumptions are made.

Is it possible to build a language, which would reduce the number of false assumptions?

Interestingly git used bit fields in it's code.

This is why we need to implement everything in JavaScript


Apparently, to some people, irony is an undefined behavior...

(Just to be clear, I am referring to the people that downvoted you.)

Good one. Too bad humour is not tolerated here :)

As I've said before: the thing is, the HN crowd is a really tough audience -- they will only be amused by jokes if they're actually funny.

If your idea of humour is "make some pop-culture reference and it will automatically be funny" or "be generically cynical about everything and it will automatically be funny" then, yeah, you're going to have a difficult time on HN.

Something like 1% or so of my HN comments are jokes. They usually do pretty well for karma. (Slightly to my surprise, the most recent one to flop completely was one that was also making a slightly serious point, and one that I don't think many HN readers would disagree with. Ah well, can't win 'em all.)

Guidelines | FAQ | Support | API | Security | Lists | Bookmarklet | Legal | Apply to YC | Contact