C and C++ prioritize performance over correctness
research.swtch.com
research.swtch.com
That is the key, right there.
In the 1970s, C may have been considered a general-purpose programming langauge. Today, given the landscape of languages currently available, C and C++ have a much more niche role. They are appropriate for the "power users" described above, who need every last bit of performance, at the cost of more development effort.
When I'm working in C, I'm frequently watching the assembly language output closely, making sure that I'm getting the optimizations I expect. I frequently find missed optimization bugs in compilers. In these scenarios, undefined behavior is a tool that can actually help achieve my goal. The question I'm always asking myself is: what do I have to write in C to get the assembly language output I expect? Here is an example of such a journey: https://blog.reverberate.org/2021/04/21/musttail-efficient-i...
I created the https://github.com/protocolbuffers/upb project a long time ago. It's written in C, and over the years it has gotten to a state where the speed and code size are pretty compelling. Both speed and code size are very important to the use cases where it is being used. It's a relatively small code base also. I think focused, performance-oriented kernels are the area where C makes the most sense.
I really don't think this is true. I've worked in CAD and video games and embedded software, and in all those you're likely using C++ (not to mention a lot of desktop software that can't afford to embed a chromium instance for electron.) For some reason people here just assume that anything that isn't web development or backend server CRUD stuff is a niche.
As much attention as something like Rust or whatnot gets on hacker news, the reality is that if you can't afford garbage collector pauses, and you need performance, you're using C/C++ most of the time.
The set of reasons to start a new project in C/C++ are few and the list is shrinking by the day.
But when it works, wow. This has been the most fun embedded project I've ever done with the least debugging.
Edit:spelling
I have a couple of languages in my belt C, C++, Go, Python, Java are the primary ones. C and C++ are reserved for "power use". For power use, I mean scientific, HPC stuff or any code I need the full capacity of the processor.
To get that, sometimes you need -ffmast-math, sometimes said undefined behavior, or code patterns which translate to better code for the processor you target.
Rust doesn't allow any of that, moreover limits me the way I can write my programs, no thanks.
For security-sensitive compiled code, I can use Rust, but I can't use Rust anywhere, and everywhere. It also has its niche, and it can't and won't replace C or C++ at the end of the day. It'll get its slice (which might be sizeable), a couple of compiler implementations, and will evolve to become another mainstream , boring language, which is good.
Also, if you give half an effort, C++ is pretty secure and robust to begin with, esp. on the pointers department. Just be mindful, and run a couple of valgrind tests on your code, and you're set. Been there, done that.
There isn't really any fundamental limitation of Rust for those situations. A better reason why C++ is better than Rust for compute is the support for OpenACC, OpenCL, OpenMP, CUDA, ROCm/HIP, etc.
Also, if you give half an effort, C++ is pretty secure and robust to begin with, esp. on the pointers department.
The problem is that even if a "Safe" C++ could be defined, it would need to interoperate with the rest of the "Unsafe" C++ world. Perhaps a programmer can write absolutely safe C++ themselves but all bets are off once you're working with other programmers.
Just be mindful, and run a couple of valgrind tests on your code, and you're set. Been there, done that.
Seriously? A "couple of valgrind tests" would be insufficient for catching all but the most trivial and/or blatant memory safety issues in C++, especially when dealing with malicious users. Although I agree that dynamic analysis like valgrind and sanitizers should be the default for C++ development, industrial grade static analysis tools are at least as important. And needless to say, if you find Rust complains about code too much then just wait for what static analysis tooling will complain about.
Every piece of code you write is security-sensitive. IMO It is our professional responsibility to our users that we treat it that way.
For a vast majority of situations, even if you know what you are doing, you are human and therefore will make mistakes.
If the (probably) most heavily invested C++ codebase, Chrome, has memory use-after-free/overflow bugs, there is no way that truly safe C++ can be written at any product-scale.
> Rust doesn't allow any of that, moreover limits me the way I can write my programs, no thanks.
I'm willing to prove you wrong here. Can you give some concrete examples?
For -ffast-math you can most certainly enable it, but AFAIK for now only in a localized fashion through intrinsics, e.g.:
https://doc.rust-lang.org/std/intrinsics/fn.fadd_fast.html
https://doc.rust-lang.org/std/intrinsics/fn.fmul_fast.html
So instead of doing this the dumb way and enabling -ffast-math for the whole program (which most certainly won't need it) you can profile where you're spending the majority of your time, and only do this where it'll matter, and without the possibility of randomly breaking the rest of your numeric code.
Personally I find this to be a vastly better approach.
> Just be mindful, and run a couple of valgrind tests on your code, and you're set.
Thousands of severe CVEs every year attest to the effectiveness of that mindset. "Just git gud" is not a meaningful thing to say, as even experienced devs routinely make exploitable (and other) mistakes.
I like rust and I dick around with it a bit on some small REST projects I have for home automation but for day-to-day work I stick with c++ until rust support picks up in the industry a lot more than it currently is.
Ad absurdum it has quite great inline asm support.
> The set of reasons to start a new project in C/C++ are few and the list is shrinking by the day.
That'd be true if every project started from scratch with only the language standard library. And yet..... almost any project you start is going to be dependent on a large chunk of code you didn't write, even on greenfield projects.
I think this is true, and I'd refine my original statement accordingly. My original comment was thinking more from first principles, not as much about pragmatic considerations of ecosystem support.
If we were to disregard the momentum of existing ecosystems, I think C/C++ would be niche choices today: very important for certain, focused use cases, but not meant for the masses. Taking into account the momentum of existing ecosystems however, they still play a large role in many domains.
I think that's because anything more "low level" (using the phrase freely) quickly becomes highly specialized, whereas web / CRUD development is a dime a dozen.
Source: Am web / CRUD developer. It's a weird one, on the one side I've been building yet another numeric form field with a range validation in the past week, but on the other I can claim I've worked in diverse industries; public transit, energy, consumer investment banking, etc. But in the end it just boils down to querying, showing and updating data, what data? Doesn't matter, it's all just strings and ints with some validation rules in the end.
But there's my problem, I don't know enough about specialized software like CAD or video games or embedded software to even have an interest in heading in that direction, let alone actually being able to find a job opening for it, let alone being able to get the job.
VM and OS abstractions have been so successful that you can go a whole career without talking directly to hardware, but remember, at the bottom of the abstraction pile something has to resolve down to voltages on wires. Function calls, method names, and JSON blobs don't do that. So what does? What abstraction bridges the gap between ASCII spells and physical voltages?
Address space. I/O ports exist, but mostly for legacy/standard reasons. Address space is the versatile and important bridge out of a VM and into the wider world. It's no mistake that C and C++ let you touch it, and it's no mistake that other languages lock it away. Those are the correct choices for their respective abstraction levels.
I mean... yeah? I'd be surprised if most operating systems didn't have a facility for directly accessing memory mapped devices in some capacity. Even Windows does I think. Have any examples of an OS that doesn't support banging directly on memory?
> using a device with simple enough behavior that things like timing or extra/elided writes didn't matter
Yeah, but that's less of a function of the language like GP was referring to and more a property of using a non-RTOS that doesn't play nice with timing sensitive things from user-space. The language itself is not really the issue there, which was my point.
You might want to go and have a look at the implementation of 'mmap' in CPython. Here's a clue: it's in the name 'CPython'.
It is still “syscall”s at the end of the day, and many programming languages can output assembly just fine.
Personally I would like to see a native language which attempts to acknowledge and work with disjoint and/or "far away" address spaces. However the daunting complexity of such a feature would likely exclude it from any portable programming language.
For what its worth UMA/USM wasn't build to bridge a gap but rather to offer greater flexibility in resource utilization for embedded platforms, that's been moving upstream(along with tiling GPUs) over the years. With UMA you can page in other data on a use-case basis which is why they were relatively popular in phones, if you don't have a bunch of textures loaded on the GPU you can give that space back to other programs. Although come to think of it we used to stream audio data from GPU memory on certain consoles that didn't have large discrete system memory(the bus connecting System <-> GPU memory had some pretty harsh restrictions so you had to limit it to non-bursty, low throughput data which audio/music fit well into).
I have seen so, so, so many examples of systems where latencies, including and especially tail latencies, end up mattering substantially and where java becomes a major liability.
In my experience, actually, carefully controlling things like p99 latency is actually the most important reason C++ is preferred rather than the more vaguely specified "performance".
Edit: And probably for that specific example it’d be best to go all the way to some optimized library like FAISS, so maybe C++ still wins?
It would be better to say that "Metalness" is a sort of "Feature Set". IMO, most programmers would tend to agree that C++ is far closer to Java than C is, yet C++ is every bit as low level as C is. Indeed, even a managed language like C# supports raw pointers and even inline assembly if one is willing to get their hands dirty.
It wasn't until the 90s that it became clear that C was not appropriate for applications work (and this was in the era of 4-16MB machines), although that took a long time to sink in.
The statement "the alternative to C was assembly" is simply incorrect.
So that left C (before cfront was introduced). And we ran with it, everybody around was using C, there wasn't the enormous choice in languages that you have today, you either programmed in C or you were working in assembler for serious and time-critical work.
Easily proven by looking into computing magazine archives.
So lets not distorce history.
On the limited environment where C got created, it was the only option. And everybody suddenly adopted that limited environment, because it was cheap. And then it improved until you could use any of those other languages, but at that point everybody was using C already.
It's really dependent on which field you're in. Not all scientific computing requires a beefy computer, but for a very long time it (and I guess LISP) dominated scientific computing. That said, I think it's a very good point to bring up the network effect of using C - if I need to hire a developer in 1985, it's probably easier to find someone with industry (not academic) experience who knows C than it is to find someone who knows Fortran.
I do kinda prefer Fortran to C though, it's so much cleaner to do math in. Maybe somewhere there's an alternate universe where IBM invented C and Bell invented Fortran to win the popularity war.
I'm talking about applications developers who came out of the small mainframe/minicomputer world of the 70s and into the workstation/microcomputer world of the 80s. They started with assembly, and prying them off of it was as hard as convincing engineers to use FORTRAN. Convincing those application developers to use a garbage collected language, Java, was hard in the 90s.
Burroughs, IBM, DEC, Xerox, ETHZ, MIT, Intel,....
Plus, legacy code.
Gnome programs are largely still written in C, but it isn't actually C. Not really. It's glib, which is almost a whole new language written on top of C. It's its own little world and it's not a good world. For example, it calls abort() whenever it fails to allocate memory.
But, generally one should just reach for a library first before doing a complex data structure regardless of language. And for example, the linux kernel does a fine job of doing some fairly complex data structures in a reusable way with little more than the macro processor and sometimes a support module. In a way the ease of writing linked lists/etc are why there are so many differing implementations. So, if your application is GPL, just steal the kernel functions, they are reasonably well optimized, and tend to be largely debugged, are standalone, etc.
Another answer is that C doesn't have much in the way of guardrails; in a world where programmer time is much more expensive than machine time, guardrails make programming much faster.
Your answer is just buzzword after buzzword. What makes C bad for writing applications?
Do you repeat this exercise when you upgrade or change compilers?
If nothing else musttail aids testing and debugging. Unoptimized code uses a lot more stack, both because it hasn't spent the time to assign values to registers, but people often debug unoptimized code because having values live on the stack makes debugging easier. The combination of unoptimized code and calls in tail position not made in a way that keeps stack size constant means you hit stack overflow super easily. musttail means that problem is at least localized to the maximum stack use of each function, which is typically not a problem for small-step interpreters. Alternatives to musttail generally involve detecting somehow whether or not enough optimization was enabled and switching to a safer, slower interpreter if not positive... but that just means you're debug and optimized builds work totally differently, not at all ideal!
Besides, the industry hands down chose C++ for the truly performance-oriented workloads.
There's still a niche for it, but it's tiny and it keeps getting tinier.
No. This is what I call the "portable assembler"-understanding of undefined behavior and it is entirely false. Clang does not need to pretend that i is initialized with a value larger than 10, there is no requirement in the C standard that undefined behavior has to be explainable by looking at it like a portable assembler. Clang is free to produce whatever output it wants because the behavior of that code is literally _undefined_.
Also, compilers don't reason about code the same way humans do. They apply a large number of small transformations, each of these transformations is very reasonable and it is their combination that results in "absurd" optimization results.
I agree that undefined behavior is a silly concept but that's the fault of the standard, not of compilers. Also, several projects existed that aimed to fully define undefined behavior and produce a compiler for this fully defined C, none of them successful.
2. They were also adamant that being a "portable assembler" with predictable, machine-level semantics was an explicit goal of the standard.
3. The C standard actually does have text giving a list of acceptable behaviours for a compiler and "silently remove the code" is not in that list. And this text used to be normative, but was later made non-normative.
So I blame the people who messed with the standard, and guess who those people were?
The exact opposite is explicitly stated in the standard (from C11 section 3.4.3):
undefined behavior
behavior, upon use of a nonportable or erroneous program
construct or of erroneous data, for which this International
Standard imposes no requirements
The standard then lists some examples of undefined behavior, and it's true that "silently removing the code" is not in the list. Still, I think it's pretty clear that it's acceptable behavior, since the standard just stated it imposes no requirements.Note "permissible" and "ranges from ... to".
Again, this used to be normative in the original ANSI standard. It was changed in later versions to no longer be normative. Exactly as I wrote.
undefined behavior
behavior … for which this International Standard imposes no requirements
That is an obvious definition of what is undefined behavior. It’s not giving license to do whatever. That said the ship has sailed and what implementors do obviously matters more than what the standard says.There is not even a requirement that a theoretical program that contains e.g. only preceding code, would still maintain any invariants. So I don't see what an instance of "whatever" that violates "no requirements" would look like.
There is no "don't do this" or "please do this" in either edition. Both merely describe the possible consequences if you do.
C90: undefined behavior: Behavior, upon use of a nonportable or erroneous program construct, of erroneous data, or of indeterminately-valued objects, for which the Standard imposes no requirements. Permissible undefined behavior ranges from ignoring the situation completely with unpredictable results, to behaving during translation or program execution in a documented manner characteristic of the environment (with or without the issuance of a diagnostic message), to terminating a translation or execution (with the issuance of a diagnostic message).
If a "shall" or "shall not" requirement that appears outside of a constraint is violated, the behavior is undefined. Undefined behavior is otherwise indicated in this Standard by the words "undefined behavior" or by the omission of any explicit definition of behavior. There is no difference in emphasis among these three; they all describe "behavior that is undefined."
C99: undefined behavior behavior, upon use of a nonportable or erroneous program construct or of erroneous data, for which this International Standard imposes no requirements NOTE Possible undefined behavior ranges from ignoring the situation completely with unpredictable results, to behaving during translation or program execution in a documented manner characteristic of the environment (with or without the issuance of a diagnostic message), to terminating a translation or execution (with the issuance of a diagnostic message). EXAMPLE An example of undefined behavior is the behavior on integer overflow.
(Some of the wording in the C90 definition was moved to the Conformance section in C99.)
An assembly language program specifies a series of CPU instructions.
A C program specifies runtime behavior.
That's a huge semantic difference.
https://www.open-std.org/JTC1/SC22/WG14/www/docs/n897.pdf
p10, line 39
"C code can be portable. "
line 30
"C has been characterized (both admiringly and invidiously) as a portable assembly language" - Dennis Ritchie
The idea of C as a portable assembler is not without its problems, to be sure -- it is an oxymoron at worst, and a squishy idea at best. But the tendency of compiler people to refuse to take the idea seriously, even for a second, just seems odd. The Linux kernel's memory-barriers.txt famously starts out by saying:
"Some doubts may be resolved by referring to the formal memory consistency model and related documentation at tools/memory-model/. Nevertheless, even this memory model should be viewed as the collective opinion of its maintainers rather than as an infallible oracle."
Isn't that consistent with the general idea of a portable assembler?
> I agree that undefined behavior is a silly concept but that's the fault of the standard, not of compilers.
The people that work on compilers have significant overlap with the people that work on the standard. They certainly seem to share the same culture.
It's not taken seriously because it shouldn't be taken seriously. It's a profoundly ignorant idea that's entirely delusional about reality. Architectures differ in ways that are much more profound than how parameters go on the stack or what arguments instructions take. As a matter of fact the C standard bends over backwards in the attempt of not specifying a memory model.
Any language that takes itself seriously is defined in terms of its abstract machine. The only alternative is the Perl way: "the interpreter is the specification", and I don't see how that's any better.
The main problem is that C is not a "portable assembler". You mainly argue that it should be, but it simply isn't (and hasn't been for a long time if it ever was).
> The people that work on compilers have significant overlap with the people that work on the standard. They certainly seem to share the same culture.
Isn't that beside the point? If you want C to be a "portable assembler" you have to write a standard that specifies its behavior. The compilers will then follow.
So true.
It baffles me how sometimes I hear colleagues of mine just "assuming" that the compiler will deal with something. Something that does require a really high level reasoning to come true.
It's absurd.
Unreal Engine is a beautiful example of this. The source code of that engine is absolutely full of 'premature optimizations.' For instance repeatedly converting a Quaternion to a Rotator (Euler angles) is basically never going to get red-lined on a profiler, let alone be a bottleneck. Yet there's a caching class used in the engine for this exact purpose. It's all of these little 'premature optimizations' that end up creating a system that runs vastly better than most any other engine out there, feature for feature.
1.) I select the lowest from the set of first elements in the arrays, add it to the end of my new list, then increment the iterator for that array. 2.) Dump all of them into one array then sort the new array entirely.
Surprisingly the second option was faster and didn't have a higher memory cost. From what I could tell this was because it used fewer memory allocations, could run better optimizations, and the standard library in go has a good sorting algorithm. One of the greatest skills I think we can develop as programmers is knowing when to trust the compiler and when not to.
All of this is to say I agree with you on principle but there is nuance and a degree to which we can trust the compiler, but only if we've chosen one that we are familiar with and that is well suited for the task at hand.
You're kind of lawyering this. Sure, it's "undefined", but is that useful to anyone outside of compiler writers? How useful is it to have a program that's very fast but entirely wrong? If the behavior is undefined, I want an error, not a free license for the compiler to do whatever the hell it wants.
This only affects you if your program has undefined behavior, at that point your program was wrong anyway, you were just lucky that the compiler compiled it into something that works. Taking a different compiler or a different target architecture and you might not be lucky. So even the old "portable assembly" kind of undefined behavior is actually the opposite of portable.
> Sure, it's "undefined", but is that useful to anyone outside of compiler writers?
It may surprise you that compiler writers are not evil people that take joy in the suffering of their users. They implement these aggressive optimizations because they actually produce better results for valid programs. It just happens that this coincides with more "absurd" results if the program has undefined behavior. But at that point your program was wrong anyway.
If you absolutely want "portable assembler"-semantics for your C program you can do that by using (very) old C compilers. Your program will not get optimized as aggressively, but "portable assembler"-semantics and optimizations are inconsistent with each other anyway.
> They apply a large number of small transformations, each of these transformations is very reasonable
One of those transformation detects that we are reading uninitialized memory and acts accordingly. Given that most of the case where we do it is by mistake, I think that doing anything other than raising an error (or at least a big warning) is not a very reasonable thing to do. For those case where such behavior is desired, a compiler flag could be provided.
The fact that the standard allows the current behavior, doesn't mean that the compiler should do it.
This might be pedantic but variables do not have to correspond to memory locations. The compiler could decide to keep the variable in a register or the compiler might realize that the variable never changes and apply constant folding eliminating it entirely. Assuming that variables correspond to memory locations (or really any assumptions about how code is translated into assembly) is a part of the "portable assembler"-understanding of C that simply doesn't apply to (optimizing) C compilers.
> Given that most of the case where we do it is by mistake, I think that doing anything other than raising an error (or at least a big warning) is not a very reasonable thing to do.
I agree that the compiler should warn if it can prove that a programs behavior is undefined unconditionally. I don't think it's as easy as you make it out to be, but I encourage you to check out the source code of clang and fix this issue if that's possible. That said, in practice undefined behavior is often encountered conditionally (e.g. the behavior of this function is undefined if and only if the first argument is non-null and the second argument is null). Optimizing such programs is trivial (simply assume that undefined behavior cannot happen and optimize accordingly), detecting that the program actually calls that function with invalid parameters on the other hand is _literally_ impossible and even a best-effort approach would be computationally expensive and miss a lot of cases.
Specifically, implement UB the way it is described in the standard: pretend it isn't UB, do it anyway, consequences be damned. That's what "ignore the situation with unpredictable results" actually means.
Not these compilers for sure. But I don't agree that all compilers are broken.
> They apply a large number of small transformations, each of these transformations is very reasonable and it is their combination that results in "absurd" optimization results.
Humans use techniques like natural deduction to apply a series of transformations that do not lead to absurd results.
All men are immortal.
Socrates is a man.
Therefore, Socrates is immortal.
If you were to give a group of CS students the exercise to determine whether the this deduction is valid, they would all answer yes. So no, humans that apply natural deduction can also derive absurd results.That said, I don't think all C programmers would agree that the result of that optimization is absurd in the first place.
The standard says no such thing. It lists a number of acceptable ways to handle undefined behavior and none of them are do anything you like.
While undefined behavior is the standard's fault, its interpretation is up to the compiler.
Consider for (int i = 0; i < ARRAY_WIDTH; i++) out[i] = in[i + offset];
Assuming that i + offset, &out + i, and &in + i + offset do not overflow allows the loop to be cleanly vectorized without checking for wraparound.
The compiler developers in the 80s were trying to come up with rules that didn't require C to be 5x slower than Fortran on scientific applications, and dealing with the consequences of a[i] being equivalent to *(&a + i).
Then we could compile all code with the latter, except for specifically marked edge cases where wrap is a desired part of the logic.
But that's exactly one of the transformations that get enabled by assuming undefined overflow.
Or perhaps I'm not thinking of the specific sequence that would 1) not wrap during modifying index and 2) not hit bounds check after.
It would need to be a requirement that compilers can't upcast all your ints to 64 bit ones, do all the math, and then write them back - would need specific instructions for each size.
If just having the ability for an exception to occur during an instruction causes overhead, that would be a big problem though.
Edit to add: We need to do the check on every operation. Just going through one iteration of the loop might have already corrupted some arbitrary memory, for example. Manually inserted checks on some flag bits don't scale to securing real programs.
"in some debug configurations overflow is detected and results in a panic"
That's not good enough. We want to always detect it! Many critical bugs are caused by this in production builds too. Solving it at the language level would require inserting branches on every integer operation which is obviously not acceptable.
So, select the configuration where that's the behaviour? overflow-checks = true
> Solving it at the language level would require inserting branches on every integer operation
Yes, so that's what you have to do if you actually want this, if you won't pay for it then you can't have it.
The HotSpot C2 compiler (and AFAIK, Graal) do partitition iteration spaces in this way.
This does have some complexity cost in the compiler, of course, and it produces more code, especially if the compiler generates multiple copies of the loop. But given that relatively few super-hot loops of this kind are in typical programs, it is worth it.
In C programs these kind of super-hot loops are quite common, because if someone didn't have many such loops they probably wouldn't need to write in C/C++. And if C had the same overhead as Java in these cases, then people who needed to squeeze out every drop of performance would use a different language.
People say this, then they write microbenchmarks. Then compilers optimize the heck out of these microbenchmarks (and numerical kernels). Rinse and repeat for several decades, and every dirty trick that you can think of gets blessed under the guise of UB.
When in reality, real programs do not spend all their time in one (or even a handful) of super-hot loops. C was designed to let compilers have a field day so that 1980s supercomputers could get 5% more FP performance, by hook or by crook. This kind of dangerous optimizations do not make much different for the vast majority of application code, the majority of which spends its time chasing pointers and chugging through lots of abstraction layers.
This paper from 2015 finds that a large amount of CPU in Google data centers is spent in a relatively small number of core components, known as the "datacenter tax": https://static.googleusercontent.com/media/research.google.c...
> [We] identify common building blocks in the lower levels of the software stack.
> This “datacenter tax” can comprise nearly 30% of cycles
> across jobs running in the fleet, which makes its constituents
> prime candidates for hardware specialization in future server
> systems-on-chips.
Some of the components they identify are memmove() (definitely a loop), protocol buffers (proto parsing is a loop), and compression/decompression (a loop).C was designed to help create Unix. It just happened to turn out to be something that could be compiled into more efficient code than most other languages at the time, without being overly difficult to work with.
From what I understand, 80s supercomputers were more likely to run Fortran. Fortran has fewer problems with pointer aliasing than C, so a Fortran compiler could generate better code than a C compiler for the type of code they would be running.
But you're also missing the entire idea anyway. It's not about bounds checking, it's about at what point overflow occurs. `int` is a sized type, so if overflow is defined for it then it has to also overflow at that size. This prevents using native size of the machine if it's larger than that of `int`. Which these days it very often is since int is 32bit. So you couldn't promote it to a 64bit relative address as then it'll overflow at 64bits instead of 32bits.
Having implemented multiple compiler backends for 64-bit machines, this is not true. All 64-bit machines I am aware of, and certainly all modern ones, do 32-bit arithmetic just fine.
The "hot data processing loop" definitely exists in some systems, notably those that really just do one single thing, but those are also far from even the majority case. At least not on anything where the end user is directly paying the computational bill (aka, very few client apps fit the "single hot loop" profile)
But if you go pitch Apple with a "hey, we want iOS to be 20-30% slower because some people are uncomfortable with narrow contracts" they would rightly laugh you right out of the room.
I'd love to see an actual citation though if you aren't just guessing.
You don't trust your compiler to optimize the sane trivial C case, but you trust it to optimize all that garbage away?
> but you trust it to optimize all that garbage away?
Yep, if you learn about compilers you learn what kind of optimizations are easy and hard for them to make. The kind that just flattens a few levels of nested definitions are easier.
I've heard criticisms of C and C++ that they are simultaneously too high-level and too low-level. Too high-level in that the execution model doesn't actually match the sort of massively parallel numeric computations that modern hardware gives, and too low-level that the source code input into the compiler doesn't give enough information about the real structure of the program to make decisions that really matter, like algorithm choice.
It's interesting that the most compute-intensive machine learning models are actually implemented in Python, which doesn't even pretend to be low-level. The reason is because the actual computation is done in GPU/TPU-specific assembly, so Python just holds the high-level intent of the model and the optimization occurs on the primitives that the processor actually uses.
The complaint here is that the warnings/errors for the integer case don't seem to be on by default. With the warnings command line flags enabled, this case is easily detected by the compiler, same as if it was some iterator object.
Just 2 reasons why I prefer explicit vectorization anywhere that I feel it is important to have vectorized code.
I think that the nature of Clang and LLVM makes this kind of reporting non-trivial / non-obvious. Deletions of this form may manifest as a result of multiple independent optimizations being brought to bear, culminating in noticing a trivially always-taken, or never-taken branch. Thus, deletion is the just the last step of a process, and at that step, the original intent of the code (e.g. an overflow check) has been lost to time.
Attempts to recover that original intent are likely to fail. LLVM instructions may have source location information, but these are just file:line:column triples, not pointers to AST nodes, and so they lack actionability: you can't reliably go from a source location to an AST node. In general though, LLVM's modular architecture means that you can't assume the presence of an AST in the first place, as the optimization may be executed independent of Clang, e.g. via the `opt` command-line tool. This further implies that the pass may operate on a separate machine than the original LLVM IR was produced, meaning you can't trust that a referenced source file even exists. There's also the DWARF metadata, but that's just another minefield.
Perhaps there's a middle ground with `switch`, `select`, or `br` instructions in LLVM operating on `undef` values, though.
It appears the author is a Go advocate. I assume they are valuing clearly defined error checking/handling/reporting (the authors definition of correctness) over performance. If that's what you are looking for, consider Go.
"Oh, it was quite a while ago. I kind of stopped when C came out. That was a big blow. We were making so much good progress on optimizations and transformations. We were getting rid of just one nice problem after another. When C came out, at one of the SIGPLAN compiler conferences, there was a debate between Steve Johnson from Bell Labs, who was supporting C, and one of our people, Bill Harrison, who was working on a project that I had at that time supporting automatic optimization...The nubbin of the debate was Steve's defense of not having to build optimizers anymore because the programmer would take care of it. That it was really a programmer's issue.... Seibel: Do you think C is a reasonable language if they had restricted its use to operating-system kernels? Allen: Oh, yeah. That would have been fine. And, in fact, you need to have something like that, something where experts can really fine-tune without big bottlenecks because those are key problems to solve. By 1960, we had a long list of amazing languages: Lisp, APL, Fortran, COBOL, Algol 60. These are higher-level than C. We have seriously regressed, since C developed. C has destroyed our ability to advance the state of the art in automatic optimization, automatic parallelization, automatic mapping of a high-level language to the machine. This is one of the reasons compilers are ... basically not taught much anymore in the colleges and universities."
-- Fran Allen interview, Excerpted from: Peter Seibel. Coders at Work: Reflections on the Craft of Programming
The undefined behavior paradoxes were only added by the 90s, when optimizing compilers became a logic inference engine, feed with the unquestionable truth that the developer never exercises UB.
Just because it was a sane language for kernel development at the 1980s, it doesn't mean it is one now.
A bit of an understatement. The author is the current Golang project lead and a member since it’s inception
If you want the exact semantics of a hardware instruction, you cannot get it, because the compiler reasons with C's abstract machine that assumes your program doesn't have undefined behavior, like signed wraparound, when in some situations you in fact do want signed wraparound, since that's what literally every modern CPU does.
[1] If the standard said that "the integer addition operator maps to the XYZ instruction on this target", that'd be something! But then compilers would have to reason about machine-level semantics to make optimizations. In reality, C's spec is designed by compiler writers for compiler writers, not for programs, and not for hardware.
1. Return any 32-bit answer for the right shift. (The final result will be zero due to the bitwise AND, though, regardless of the intermediate answer.) The intermediate answer must be "frozen" so that if it is assigned to a variable and then used multiple times without writing to that variable again then you will get the same answer each time.
2. Result in a run-time error when that code is reached.
3. Result in a compile-time error (only valid if the compiler can determine for sure that the program would run with a shift amount out of range, e.g. if the shift amount is a constant).
4. Have a behaviour which depends on the underlying instruction set (whatever the right shift instruction does in that instruction set when given a shift amount which is out of range), if it is defined. (A compiler switch may be provided to switch between this and other behaviours.) In this case, if optimization is enabled then there may be some strange cases with some instruction sets where the optimizer makes an assumption which is not valid, but bad assumptions such as this should be reduced if possible and reasonable to do so.
In all cases, a compiler warning may be given (if enabled and detected by the compiler), in addition to the effects above.
[[nodiscard]] constexpr uint64_t
getBits( uint8_t nBits )
{
return BITBUFFER >> ( 64 - nBits ) & ( ( 1ULL << nBits ) - 1U );
}
int main()
{
std::cerr << getBits( 0 ) << "\n";
std::cerr << getBits( 1 ) << "\n";
return 0;
}
The first output will print a random number, 140728069214376 in my case, while the second line will always print 1. However, when I put the ( ( 1ULL << nBits ) - 1U ) part into a separate function and print the values for that, then getBits( 0 ) suddenly always returns 0 as if the compiler understands suddenly that it will and with 0. template<uint8_t nBits>
[[nodiscard]] constexpr uint64_t
getBits2()
{
return BITBUFFER >> ( 64 - nBits ) & ( ( 1ULL << nBits ) - 1U );
}
In this case, the compiler will only print a warning when trying to call it with getBits2<0>. And here I kinda thought that constexpr would lead to errors on undefined behavior, partly because it always complains about uninitialized std::array local variables being an error. That seems inconsistent to me. Well, I guess that's what -Werror is for ...Compiled with -std=c++17 and clang 16.0.0 on godbolt: https://godbolt.org/z/qxxWW93Tx
UB is used for performance optimizations, yes, but all of these difficult to diagnose UB issues and bugs happen because C++ makes it laughably easy to write incorrect code, and (as shown by Rust) this is by no means a requirement for fast code.
> because C++ makes it laughably easy to write incorrect code
I was going to ask how much you actually program in C++, but I found a past comment of yours:
> I frankly don't understand C++ well enough to fully judge about all of this
The Rust #1 through #6 entries use the regex crate, which is pure-Rust. Rust #7[rust7] (which is not shown in the main table or in the summary, only in the "unsafe" table[detail]) uses PCRE2, and it is interestingly also faster than the C impl that uses PCRE2[c-regex] as well (by a tiny amount). C++ #6[cpp6], which appears ahead of Rust #6 in the summary table (but isn't shown in the comparison page)[comp], also uses PCRE2 and is closer to Rust #7.
[comp]: https://benchmarksgame-team.pages.debian.net/benchmarksgame/...
[detail]: https://benchmarksgame-team.pages.debian.net/benchmarksgame/...
[rust7]: https://benchmarksgame-team.pages.debian.net/benchmarksgame/...
[c-regex]: https://benchmarksgame-team.pages.debian.net/benchmarksgame/...
[cpp6]: https://benchmarksgame-team.pages.debian.net/benchmarksgame/...
Lies, damn lies, and benchmarks?
I can at least say, the performance difference between C, C++, and Rust, is splitting hairs.
If you want to write something performant, low level, with predictable timing, all three will work.
I'm spending a lot of time building projects with Rust & C++ these days. The issue/tradoff isn't performance with C++, but that C++ is better for writing unsafe code than Rust.
https://www.p99conf.io/2022/09/07/uninitialized-memory-unsaf...
It also provides a lot of mechanisms and tools to produce correct safe code, especially modern C++. Most codebases you're not seeing a lot of pointer arithmetic or void pointer or anything of that nature. You hardly even see raw pointers anymore, instead a unique_ptr or a shared_ptr. So yes, you can write incorrect code because it's an explicit design goal of C++ not to treat you like a baby, but that doesn't mean that writing C++ is inherently like building a house of cards.*
I don’t think that is entirely true. C++ doesn’t have any aliasing requirements around pointers, so if the compiler sees two pointers it has to assume they might alias (unless the block is so simple it can determine aliasing itself, which is usually not the case), but in Rust mutable references are guaranteed to not alias.
This was part of the reason it took so long to land the “noalias” LLVM attribute in Rust. That optimization was rarely used in C/C++ land so it had not been battle tested. Rust found a host of LLVM bugs because it enables the optimization everywhere.
Isn't the overhead for C++ exceptions quite significant, especially if an exception is thrown?
Exception handling can also increase the size of the binary because of the additional data needed to handle stack unwinding and exception dispatch.
I think a number of optimizations are made quite a bit more complex by exception handling as well.
Go really is in many ways more similar to early C in spirit than modern C is.
I can't find a source now, but I believe Rob described Russ Cox as "the only programmer I've met as gifted as Ken." High praise indeed.
I suspect that there is some huge number of developer hours that have been wasted, and huge amount of money wasted, on cleaning up after security breaches and finding and fixing security issues. I suspect that those numbers dwarf any losses that might have arisen due to reduced developer productivity or reduced performance when using a (hypothetical) C-like language that doesn't allow the compiler to do these sorts of things.
It's not very hypothetical in 2023. There are plenty of languages whose compilers don't do this sort of thing and attain C-like performance. There isn't necessarily a single language that exactly and precisely replaces C right now, but for any given task where you would reach for C or C++ there's a viable choice, and one likely to be better in significant ways.
I also feel like this is missed by some people who defend this or that particularly treatment of a particular undefined behavior. Yeah, sure, I concede that given the history of where C is and how it got there and in your particular case it may make sense. But the thing is, my entire point is we shouldn't be here in the first place. I don't care about why your way of tapping a cactus for water is justifiable after all if you consider the full desert context you're living in, I moved out of the desert a long time ago. Stop using C. To a perhaps lesser but still real extent, stop using C++. Whenever you can. They're not the best option for very many tasks anymore, and if we discount "well my codebase is already in that language and in the real world I need to take switching costs into account" they may already not be the best option for anything anymore.
The point of undefined behavior is "if we specify this, we cause problems for some of the compile targets for this language".
https://thephd.dev/c-undefined-behavior-and-the-sledgehammer...
res, err := InterceptNuke(enemyNuke)
if err != nil {
fmt.Println("NUCLEAR LAUNCH DETECTED!")
log.Fatal(err)
} else {
fmt.Printf("Phew, we're safe. Nuke intercepted after %d seconds.\n", res)
}
Very clearly defined errors indeed.I want some sort of flag that disables all this nonsense.
* Uninitialized variable? Illegal, code won't compile.
* Arithmetic overflow? Two's complement
* Null pointer call? Either won't compile, or NULL pointer dereference at runtime worst case.
Yeah, I'm aware -fwrapw and the like exist. But it'd be nice to have a -fno-undefined-behavior for the whole set.
I don't really want this either; I'd rather the program abort. The vast majority of situations where I'm using signed arithmetic, I never want a positive number to overflow to negative (and vice versa).
Unsigned arithmetic is already designed to wrap back to zero; that's useful for things like sequence numbers where wrapping is ok after you "run out".
The ARM mode described elsewhere in the thread where there's an overflow flag that persists across operations would help; then you could do a check less frequently.
A mode were you get a processor exception would be great, if adding that doesn't add significant cost to operations that don't overflow. Assuming the response to such an exception is expected to be a core dump, the cost of generating such an exception can be high; of course if someone builds their bignumber library around the exception, that won't be great.
A predictable branch is basically free. In your case such a branch is almost never taken.
We have static analysis tools which do a decent job. But like, there's a reason Clang has UBSan to detect undefined behavior at runtime. It's a hard problem.
Failing compilation on an uninitialized variable is easy. The compiler can already warn about this situation (and gcc and clang at least allow you to promote individual warnings to errors). Making this default would be simple, and not at all a performance concern.
Allowing signed arithmetic to overflow (in a defined 2's complement manner) would be just exactly what the hardware does on modern machines, so there'd be no slowdown there. Sure, the compiler would no longer be allowed to omit code that checks for overflow, but that's fine: if the programmer truly doesn't care, they won't write that check in the first place.
(Changing these two behaviors might have backward-compatibility concerns, though.)
You are of course correct that NULL dereference checking is would incur a performance penalty at runtime. However, the compiler should be able to catch some subset of cases at compile time. At the very least, there could be a mode where it could at least warn you at compile-time that some dereferences could result in SIGSEGV. Unfortunately, I think it would be hard to get that warning to a point where there weren't a lot of false positives, so such a warning would be routinely ignored to the point of uselessness.
SIGSEGV on null pointer deference is not the problem. It's actually fine, in the sense that it predictably terminates the program. The problem is that modern optimizing compilers don't guarantee that SIGSEGV will happen; they may rewrite your program to do insane shit instead, as the example in the blog post shows. So we don't need NULL checks at runtime; we just need the compiler to stop doing insane optimizations.
On uninitialised variable, sure, but impossible to tell at compile time if the program is using an uninitialised value.
Compiler authors also appear, to me, to be malicious.
In the past, they worked hard to provide warnings when they detected uninitialised variables.
In the rare case, now, that the compiler is able to tell that a line is using an uninitialised value, instead of issuing a warning, it simply removes the offending code altogether.
If compiler authors now were more like compiler authors of the past, they'd value the user interface enough to make any and all dead code a compile error.
Imagine if past compiler authors had their compiler, when seeing the use of an uninitialised variable, simply go ahead and remove any code that used that variable.
Imagine how poor a user experience that would be considered, and then ask yourself why they feel it is okay now to simply remove dead code.
There is no situation where actual source code lines should be removed rather than warned.
Typing on phone so not going to go into all the data flow explanations.
In my opinion, any C or C++ program must always be compiled with such options by default, both for debug and release builds.
Only when an undesirable influence on performance is measured, the compilation options can be relaxed, but only for those functions where this matters.
No the compiler does not check for UB to generate code. A lot of UB (runtime out of bounds, use after free) are very difficult to detect statically (at least without specialized annotations).
The compiler applies transformations to the code that are valid (in the sense that they produce an equivalent program) only in the absence of UB.
So if the input program contains UB, the transformed program produced by the compiler may or may not be valid
We usually aim for correctness, and in small programs we often achieve it 100%.
100% maximum performance is rarely achieved in a high level language.
If you try hard at absolute correctness, you can get there; not so with performance.
So, obviously, that must mean performance is harder than correctness.
If you turn a performance aspect of a system into a correctness requirement (e.g. real time domain, whether soft or hard) you have hard work ahead of you.
In programming, we often make things easier for ourselves by sacrificing performance. Oh, this will be executed only three times over the entire execution lifetime of the program, so doesn't have to run fast. Thus I can just string together several library functions in a way that is obviously correct, rather than write loops that require proof.
That said, if you have certain kinds of correctness requirements, C becomes verbose. What is easy to express in C is an operation on inputs which have already been sanitized (or in any case are assumed to) so that the operation will correctly execute. It's not a priority to make it convenient to do that validating, or to just let the operation execute with inputs and catch the problem.
E.g. say that we want to check whether there would be overflow if two numeric operands were multiplied. It gets ugly.
To a point, sure. But your language and tools dictate a upper bound on speed. You are never going to get a trading app written in Python to be faster than your competitor's app written in C.
But performance is also
- defined by fairly simple, measurable parameters that everyone agrees on, like execution time, code size or memory use;
- subject to automation: compilers can take a program which, if run literally as written, is slow, and speed it up. (Or make it smaller or require less memory.)
Correctness is not like this.
- There is no universal definition of correctness; it is the degree to which a program matches its arbitrary specification.
- Compilers cannot take a program that is incorrect and optimize it to have more correctness. They don't read the specification.
If I ask someone to write a program which demonstrates how a null pointer deference results in a predictable segmentation fault, then this will be a correct program:
int main(void} { *((char *) NULL) = 0; }
A correct program could crash in this way even none of its requirements say that it should exercise null dereference or any other error on purpose.A correct program could be given incorrect data, for which its behavior is not specified. The program is then being misused; the ill behavior doesn't demonstrate that it's incorrect.
A program which has no requirements for handling unexpected inputs, and does not do so, cannot be safely deployed in certain situations, like when it runs in one security domain, but inputs are coming from a different one. That situation implicitly adds requirements, which that program does not meet; effectively the program is interpreted as incorrect. That shows that there can be misunderstandings and disagreements about what is correct, which boils down to disagreeing on what is and isn't a requirement.
We would never disagree that a program which calculates something in 3 seconds is faster (on that input case) than one which does the same in 5 seconds. (All else being same; hardware, OS, ...)
I think in the world of C, the compiler assumes the author knows what they're doing, and historically, you were probably supposed to use a separate tool, a linter [1] (static analysis tool), to help catch mistakes.
HN's hard disks were a recent (2022-07) victim of a firmware developer's lack of understanding of integer overflow exceptions. [2] The firmware was likely written in C, or "C++ C". A fix was released in 2020. Another reminder to update the firmware of these disks. [3]
++crash;
[1] https://en.wikipedia.org/wiki/Lint_(software)
[2] https://en.wikipedia.org/wiki/Integer_overflow
[3] https://www.thestack.technology/ssd-death-bug-40000-hours-sa...
It's true that historically C has not done much to protect programmers from their mistakes, but historically mistakes just meant suffering the natural consequences (such as a SIGSEGV on NULL pointer dereference). But these days, when you make a mistake, C compilers will exploit your mistake to the maximum extent possible, including changing the meaning of your programs.
For example adding a "assert(x < MAX_INT - 100)" in first example.
Running test suites on this debug binaries would find a lot of problems.
If you really want less UB, switch to a different language or change the language! Complain to the standards committee, have them define behavior that is currently undefined, or impose restrictions on allowable behaviors. Compilers are always going to optimize to the extent that the language standard allows, so change the standard.
That is bullshit. There are plenty of project that would gladly trade performance for more correctness. I would go as far to say most projects would make that choice if articles like this get mindshare.
“It’s mostly as fast as clang but errors upon UB” is an easy sell
What you describe fits for "implementation defined behavior". If you want to write code that works with different compilers, a single compiler with every UB defined, gains you nothing. If you don't need that flexibility, you can just use a language without UB in the first place.
Ideally it's a compile time error. Less ideally it jumps to a NULL pointer and immediately crashes.
Either means the programmer must fix the code, which also stops being a problem on other compilers.
I agree with that, but the standard does not agree with both of us. My point is that choosing C or C++ makes sense if you see an advantage in programming against an ubiquitous and almost universal standard. If you have the freedom to implement against a particular compiler implementation there is no good argument to not also taking the freedom to choose a different language altogether.
I know in the real world this is not so easy. I worked in automotive with their certified C compilers long enough and I wouldn't have had the choice to select another language. Doesn't mean everyone wouldn't have been better off with a language without UB in the first place and I think we are getting there.
https://www.dreamsongs.com/RiseOfWorseIsBetter.html
However our tolerance to accept "worse" over "better" is waning since we have more capable hardware, better tools, and "worse" leads to more problems later such as security vulnerabilities.
#include <stdio.h>
int f(int x) {
if(x+100 < x)
printf("overflow\n");
return x+100;
}
>Clang optimizes away the if statement. The justification is that since signed integer overflow is undefined behavior, the compiler can assume it never happens, so x+100 must never be less than x. Ironically, this program would correctly detect overflow on both ones'-complement and two's-complement machines if the compiler would actually emit the check.My god…
C was never standardized solely on the basis of what's most performant. The vast majority of explicit UB is there because someone knew of a system or situation where that assumption wasn't true.
So? They could have called it implementation defined. The reasoning you present was broken then, as it is broken now.
Your MIPS example displays no reason for UB to exist.
Look, a lot of UB is frankly stupid and shouldn't be in a modern language spec, including signed overflow. I'm not defending that, only giving an example where you have to break compatibility to eliminate it.
This is just bad programming. The compiler, like usual, is correct because you're not in reality checking anything. You've made an obviously non-sensical statement that the overflowed value will be less than the value. Compiler optimizes it away. You can argue the semantics of UB here but this particular UB is borderline a solved problem in any practical sense.
To be fair, a static analyzer should be able to catch this.
Therefore it should fail to compile.
You can even spin it as a performance enhancement: the whole program can be optimized away!
#include <limits.h>
int safe_add(int a, int b) {
if (a > 0 && b > INT_MAX - a) {
/* deal with overflow... */
} else if (a < 0 && b < INT_MIN - a) {
/* deal underflow.. */
}
return a + b;
}I know I should attribute it to stupidity instead but ...
The standards writers could have made all UB implementation defined, but they didn't. The compiler authors could have made all Uab implementation defined, and they didn't.
Take uninitialised memory as an example, or integer overflow:
The standard could have said "will have a result as documented by the implementation". They didn't.
The implementation can choose a specific behaviour such as "overflow results depend on the underlying representation" or "uninitialised values will evaluate to an unpredictable result."
But nooooo... The standard keeps adding more instances of UB in each revision, and the compiler authors refuse to take a stand on what overflow or uninitialised values should result in.
Changing the wording so that all UB is now implementation defined does not affect blegacy code at all, except by forcing the implementation to document the behaviour and preventing a compiler from simply optimising out code.
C17 Draft, § 3.4.3: "Possible undefined behavior ranges from...
. . . ignoring the situation completely with unpredictable results, to . . .
. . . behaving during translation or program execution in a documented manner characteristic of the environment (with or without the issuance of a diagnostic message) . . .
. . . to terminating a translation or execution (with the issuance of a diagnostic message)."
Compiler writers like to language-lawyer this in two ways. First, they interpret "possible" to mean that this is a non-exhaustive list, but that isn't how interpretation of such a document typically works. Expressio unius est exclusio alterius -- the express inclusion of one is the exclusion of others -- means that any list is exhaustive unless explicitly indicated otherwise. In other words, the reality is that, according to the standard, these are the only possible options.
Second, they interpret "ignoring the situation completely" to mean "actively seeking out the situation and deleting whole sections of code based on that." This is quite self-evidently a dishonest interpretation.
The C standard was written when people wanted a language that was easy to read and write, but could have similar performance to hand-written assembly. Performance, as the article title points out, was a bigger priority than correctness or eliminating foot guns.
Most of us here weren't around when many/most programs were written in assembly, and when machines were very constrained in how much memory they had and how many CPU cycles they could spare. Nowadays, performance concerns usually come behind speed and correctness of development. So it's hard to truly feel what trade offs the original designers of C (and the original C89 standards committee) had to make.
I'm not convinced implementation-defined behavior is really any better, though. Packagers and end-users should not have to ensure that the code their compiling is "compatible" with the compiler or hardware architecture they want to use. Developers shouldn't have to put a "best compiled with FooC compiler" or "only known to work on x86-64". That would not be an improvement.
I do agree that, these days, there's no excuse for writing language specifications that include undefined behavior. But C has a lot of baggage.
This is not possible without performance impact, because, in the general case, whether a program constitutes UB depends on data and/or previous program flow (halting problem), and thus would require additional runtime checks even for code that happens to never run into UB.
This is optimization run amok and I'd call it malice. If C/C++ are designed to let me shoot myself in the foot, the compiler should let me shoot myself in the foot instead of erasing my code.
Maybe it's about time to fire the C compiler developers
I guess the difference between Go and C/C++ is that Go at least won't optimize away the overflow check? But it still doesn't insert it for you either.
It is crazy that people decided to do it. It not only weird, but also wrong.
There's no such a thing like "side-effect-free" code.
There's always side-effect like CPU usage, temperature, mem usage, etc.
There is whole class of attacks based on those.
This kind of like “Ceci n'est pas une pipe.” In that it’s useful to remind people that there is no such thing as side-effect-free code, strictly speaking, but as soon as you remind people, you go back to talking about whether a particular piece of code has side effects. Someone points at object depicted in the painting and asks what it is, and you say “It’s a pipe.”
Basically, to reason about code, it’s useful to ignore certain classes of side effects, most of the time.
If you are writing a loop in your code with no classic side effects hoping to regulate temperature, that’s probably a bad and unreliable design to start with, whether or not the compiler optimizes it away.
If you read a temperature sensor to validate that the loop is doing the right thing and adjust frequency based on the result, its still a little crazy but its less completely insane and no longer free of classix side effects.
You can run heavy computation and do not do anything with the result just in order to increase CPU usage, right? (which then should result in temp increase ;))
* it is thorough, detailed and very thoughtful
* rsc’s essays usually end up with a major Go language change, so there’s a good chance this article is the seed of some change to Go and undefined behavior, or correctness or performance.
Even if it’s not a major upcoming change (I bet it is, tho) rsc is an extremely insightful dev.
To me the only real question is: what should the default be for your compiler? I don't want to be flooded with warnings when I'm working on a small file of handcrafted near-assembly code but I'm willing to change my compiler/tool options away from some more conservative, non-standard default settings.
I mean (and I'm asking because I'd genuinely like to bookmark someplace) is there a group that keeps an up-to-date list of aggressive flags and static analyzers to use?
-Weverything - this enables every clang warning
-Wno-c++98-compat - warns about using newer C++ features
-Wno-c++98-compat-pedantic - warns about using newer C++ features
-Wno-padded - warns about alignment padding. I optimize struct layout, so this warning only reports on cases I couldn't resolve.
-Wno-poison-system-directories - I'm not cross-compiling anything.
-Wno-pre-c++20-compat-pedantic - warns about using newer C++ features
For the flags I would start with "-Wall -Werror", then maybe disable some warnings based on the code base / use.
All this assuming a GCC/Clang friendly code base.
Under "5. Performance" section, it claims the performance impact to be negligible, and there is also a mechanism to opt-out.
Check out: -wall -werror https://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html
A Guide to Undefined Behavior in C and C++ (2010)
https://news.ycombinator.com/item?id=37165042 (166 comments)
A worthwhile read, IMO. Cheers.
C and C++ have as a basic design rationale
"as close to machine performance as possible"
"don't pay for what you don't use"
"don't break backwards compatibility"
The last one is what is what is responsible for most of the issues. It would be nice to fix stuff like UB, but the reason that C/C++ is so popular and still alive is precisely because it does care more about compatibility than fixing stuff likethat.Breaking existing code is extremely uncool for long-lived codebases.
Taking your argument, compilers couldn’t even add any benign optimization without breaking compatibility, because that would change the runtime of the affected code.
the problem is mixing in incorrect or unexpected things (like someone altering a global via a debugger). to some extent, it's fair to complain about archaic nonportabilities (such as whether dereferencing NULL will fault, or long-gone numeric representations) - the standards committees should take a deep breath and DTRT in those cases.
Took me some time to understand that part :)
I always thought that an unassigned variable of type slice T was deemed uninitialized. (but then, what of assigning nil to a previously non-nil variable? Is it considered deinitialized?)
In fact, at the language level it could be considered uninitialized/deinitialized. That's the variable typestate. (cf definite assignment, ineffectual assignment analysis)
For the compiler, it still "points" to a memory location so it is initialized.
Am I right? (more than 344 comments, no one will find this one question lol :-)
Well, probably except Rust, from which you get both performance and safety. Sometimes it's with a cost of ergonomics, though. Some people, including the Rust's creator himself, would trade performance for better ergonomics in the case of asynchronous programming. They prefer having a non-zero-cost green threads to the current state of async prog in Rust.
Try anything fancy, and these languages can punish you for it later on down the life-cycle. However, claiming it lacks correctness shows a level of ignorance.
Best of luck, =)
> Uninitialized variables
The compiler should be allowed to do one of:
1. Assume any arbitrary value, as long as it remains consistent until a new value is assigned (which might or might not depend on the previous value). (However, the exact value need not be known at compile-time, and might not even be known at run-time if the program doesn't care about the exact value (in the example if the loop is removed then the exact value doesn't matter at run-time). It might be different every time the program runs.) Removing the loop in the example is valid; it would also be valid to start at a negative number (since you did not specify an unsigned type), but if for example you declare i before the loop instead of inside and then you add printf("%d\n",i); after the loop, then the program is required to print a number which is at least 10 and can fit in the int type; if standard library optimizations are enabled then it would be allowed to e.g. replace it with puts("42"); instead (but if the loop is not removed, then the number printed by this must be 10).
2. Read the value from a register or memory which is then used as the storage for that variable (for whatever time it needs to, just as though it was given a value which was returned from a function with no side-effects), but without initializing it.
3. Do whatever the target instruction set does (which seems very unlikely to me to do anything other than the above).
> Arithmetic overflow
The C specification should specify two's complement (it is the only good representation of integers anyways). Fortunately GCC has -fwrapv and -ftrapv, and I often use -fwrapv to avoid problem with signed overflow.
> Infinite loops
There is a rationale with some sense, but maybe better would be nevertheless not optimizing out the loop entirely like that. However, if standard library optimizations are enabled then it should be allowed to replace the loop with:
for(;;) pause();
> Null pointer usageIn this case, the program should just actually attempt to read, write, or call the null address, which should result in a segfault or other run-time error if possible (instead of being optimized out). However, the compiler is allowed to assume that such a call can never return, if it wishes (whether or not that is actually true on the target computer).
In the case of a read or write that is not marked as volatile, the compiler may substitute any value for a read and may optimize out a write, although it does not have to; it may also do what is specified above.
However, the optimization shown in the article should be allowed if Do is a local variable which is uninitialized, rather than being initialized as null. (You can disable optimizations (or possibly change some options) if you don't like that.)
They don't prioritize anything. They are just bad languages with a random assortment of features mostly reflecting how people thought about computers in the 70's.
Just think about this: vectorization is one of the obvious ways to get better performance in a wide range of situations. Neither C nor C++ have any support for that. Parallelism -- C++ kinda has something... C has zilch. Memory locality anyone?
I mean, Common Lisp has a bunch of tools to aid compiler in optimizing code in the language itself, whereas C has like... "inline" and the ability to mess with alignment in structs, and I cannot really think about anything else. Stuff like UB isn't making the language perform better or easier to optimize. It's more of a gimmick that compiler authors for C and C++ found to produce more efficient code. It's a misfeature, or a lack of feature, that allowed for accidental beneficial side-effects. Intentional optimization devices in a language are tools that prove the code to be intentionally correct first, and that proof allows the compiler to elide some checks or to generate a more efficient equivalent code, based on the knowledge of the correctness of the proof (or, at least, upon explicit instructions from the code author to generate "unsafe" code).
Or, well, in a narrow range of situations where you have certain kinds of hardware with vectorization support.
> C has zilch
C has operating systems written in it which use parallel processing internally and make parallel processing available to applications. True, that may be done using techniques that are not described in ISO C.
Common Lisp has undefined behavior, a familiar example being (rplacd '(1 . 2) 3): clobbering a literal object.
Optimizations in Common Lisp are exactly like UB in C: you make declarations which specify that certain things hold true, and in an unsafe mode like (declare (optimize (safety 0) (speed 3))), the compiler blindly trusts your assertions and generates code accordingly. If you asserted that some variable is a fixnum, but it's actually a character string, that character string value's bit pattern will likely be treated as a fixnum. Or other consequences.
Common Lisp is nice in that you can control speed and safety tradeoffs on a fine granularity, but you do trade safety to get speed.
Common Lisps (not to mention Schemes) have extensions, just like C implementations. In plenty of code you see things like #+allegro (do this) #+sbcl (do that).
This certain kind of hardware exists on virtually all consumer-grade and server-grade CPUs produced in the last decade. This is where optimizations matter in the first place.
It doesn't matter that operating system written in C: it has no concurrency in the language. Here are examples of concurrency in the language: in Java, you have "synchronized", in Go you have "select" in Erlang you have "receive", in Ada you have "task" and so on. These languages provide some way to deal with concurrency and language implementations must have concurrency primitives.
There's nothing like that in C. You don't have to implement C in such a way that it supports concurrency in any way. No part of the standard requires it (unlike in C++, where you have threads since C++11).
> Optimizations in Common Lisp are exactly like UB in C:
My guess is that you have never even seen Common Lisp. You just wrote absolute nonsense.