What is C in practice?
cl.cam.ac.uk
cl.cam.ac.uk
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
compilers."
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?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).
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.
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.In those cases some compiler override could provide the solution, while still allowing the compiler to flag all the other cases as errors.
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.
int foo() {
int bar;
return bar + 5; /* C4700: local variable 'bar' used without having been initialized */
}
https://msdn.microsoft.com/en-us/library/axhfhh6x.aspxIn 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.
Maybe you are using a old version.
If you enable -Wall which you always should then this flag is automatically enabled.
https://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html#inde...
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();
a.toString();
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.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.
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.)
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.
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:
http://blog.llvm.org/2011/05/what-every-c-programmer-should-...
http://blog.llvm.org/2011/05/what-every-c-programmer-should-...
http://blog.llvm.org/2011/05/what-every-c-programmer-should-...
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.
let x;
println!("x:{}", x);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? fn f() {
if let Some(i) = init() {
do_stuff(i);
}
}
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.)
int i = 0; // or some other default value 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.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.
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.
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.
int x;
h(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.)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.
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.
Often there is no sane default value.
g() might have side-effects, and those side-effects have to happen regardless of whether or not f() returned true.
if (f()) {
int x = 1;
if (g()) {
h(x);
}
} else {
g();
}
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) {
h(x);
}
|
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) {
h(1);
}
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.
For example, your example could legally be rewritten to:
int x;
f();
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: f();
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.
It could apply if f computes a value based on global state but doesn't change anything, which is slightly weaker than "pure".
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?
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.
int b; int c = b * 0;
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.
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.
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;
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.
Disclaimer: I have no C experience.
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).
(I believe I follow the intended conversational import; I certainly follow the code.)
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.
*(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.
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.
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.)
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.
What reduces the set of compilers is the reliance on compiler specific behaviours or recently approved standards, when compilers are still catching up.
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.
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).
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".
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.
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.
Most programmers today will never encounter any of those.
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.
Is it possible to build a language, which would reduce the number of false assumptions?
hides
(Just to be clear, I am referring to the people that downvoted you.)
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.)