[HN Gopher] C and C++ prioritize performance over correctness
___________________________________________________________________
C and C++ prioritize performance over correctness
Author : bumbledraven
Score : 206 points
Date : 2023-08-18 16:27 UTC (6 hours ago)
(HTM) web link (research.swtch.com)
(TXT) w3m dump (research.swtch.com)
| tester756 wrote:
| >The problem is that C++ defines that every side-effect-free loop
| may be assumed by the compiler to terminate. That is, a loop that
| does not terminate is therefore undefined behavior. This is
| purely for compiler optimizations, once again treated as more
| important than correctness.
|
| 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.
| gpderetta wrote:
| Side-effect is a Word of Power in the context of the standard
| with very specific meaning that doesn't include, for example,
| temperature increase. Even if you use it as a Control key
| replacement.
| dragonwriter wrote:
| > There's always side-effect like CPU usage, temperature, mem
| usage, etc.
|
| 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.
| tester756 wrote:
| All I'm saying is that code doesn't have to perform stuff
| like prints, file write, http call in order to generate side
| effect
|
| 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 ;))
| tylerhou wrote:
| The C abstract machine isn't a physical machine, so
| temperature is not a side effect :)
| layer8 wrote:
| What is meant are side effects as defined by the C abstract
| machine. It has no concept of CPU speed, temperature, etc.
| klodolph wrote:
| > There's no such a thing like "side-effect-free" code.
|
| 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.
| zzo38computer wrote:
| I have some of my opinions about how it should do.
|
| > 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 usage
|
| In 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.)
| grandinj wrote:
| rsc should really know better, he has been around long enough.
|
| 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.
| agwa wrote:
| Undefined behavior _permits_ backwards compatibility to be
| broken, because when a compiler adds a new optimization that
| exploits UB, existing programs can change behavior, breaking
| code that previously worked. The blog post cites infinite loops
| as an example. Thus, eliminating UB would improve backwards
| compatibility.
| layer8 wrote:
| Compatibility is defined in terms of a contract between
| program and language implementation. If the program violates
| the contract by containing UB, then compatibility does not
| apply.
|
| Taking your argument, compilers couldn't even add any benign
| optimization without breaking compatibility, because that
| would change the runtime of the affected code.
| agwa wrote:
| > _Compatibility is defined in terms of a contract between
| program and language implementation. If the program
| violates the contract by containing UB, then compatibility
| does not apply._
|
| Indeed, furthering my point that having UB in a language
| undermines backwards compatibility.
|
| > _Taking your argument, compilers couldn't even add any
| benign optimization without breaking compatibility, because
| that would change the runtime of the affected code._
|
| That does not follow from what I said.
| layer8 wrote:
| The point is, you have to define compatibility with
| regards to _what_. Otherwise nothing can ever change. The
| purpose of the C standard is to provide that reference
| point, via its definition of what constitutes a
| conforming program and a conforming language
| implementation. You only get compatibility problems when
| you step out of that scope.
| haberman wrote:
| > There are undeniably power users for whom every last bit of
| performance translates to very large sums of money, and I don't
| claim to know how to satisfy them otherwise.
|
| 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.
| overgard wrote:
| > 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.
|
| 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.
| kiratp wrote:
| Rust is not a GC language. It can and does achieve the same
| perf as any modem C++ with the latest flavor of the C++
| safety guardrails.
|
| The set of reasons to start a new project in C/C++ are few
| and the list is shrinking by the day.
| overgard wrote:
| I phrased it poorly, I didn't mean to imply Rust is GC'd. I
| mean that it's still niche. C++ very much is not niche,
| which is my point. The other non-niche languages generally
| are GC'd (java, C#, javascript, etc.)
|
| > 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.
| haberman wrote:
| > 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.
| MarkMarine wrote:
| I love Rust, but as a daily C++ user (embedded
| microcontrollers, not supported by Rust yet, and then
| native Linux tooling to interface with them) what I find
| most frustrating about Rust is the number of crates that
| need to be knit together to make working software. Often
| these crates are from a single dev who last committed in
| 2020 or something. I really wish there was something like
| go's standard library in Rust. That has been my barrier to
| adoption, and believe me I WANT to use it at work and I
| don't want to juggle chainsaws anymore.
| Ar-Curunir wrote:
| Er how is that different from C++? It doesn't have a go-
| like std library either, and you can totally use Rust
| without crates.io in the same manner.
| MarkMarine wrote:
| I suppose that wasn't clear. My mistake. I use C/C++ for
| micro controller code every day, because Rust doesn't
| support my microcontroller, doesn't have a real RTOS, and
| I'm making real hardware. Something that needs to run in
| the field for 10+ years and I can't get out there and
| ensure it's updated, and my company doesn't want to
| invest millions of dollars on beta testing embedded rust
| on. So embedded is out for now but I'm looking forward to
| when it's supported fully and out of beta. Most embedded
| controllers come with some RTOS and libraries they
| support, written in C.
|
| For tooling, I can context switch out of C++ which does
| have boost, into [rust, go, python... etc] and deal with
| that switch, or just be lazy and write it in C++. I've
| tried to write three tools in Rust so far, and the pain
| of not having a good stdlib, of essentially searching the
| internet for a blog post that solves my issue then
| finding the blog post was written by one dev 4 years ago
| to pump up their library that was written 4 years ago and
| then never supported after that... it's a bit exhausting.
|
| Again, before ya'll attack. This is from the perspective
| of a willing, excited customer. I want to use Rust at
| work and advocate for it. Just saying, it's not easy in
| the state it's in.
| seabass-labrax wrote:
| Which microcontroller are you using? Rust support for
| embedded targets is slowly improving, so there might be a
| beta build for your chip.
| xedrac wrote:
| In C++, you'll often see projects just use boost, which
| is a big monolith of useful libraries.
| tumdum_ wrote:
| Embedded microcontrollers are not the place where boost
| is used.
| dgacmu wrote:
| I'm sympathetic. I'm partway into an embedded project on
| an stm32 / m0 for which there _is_ good rust support
| through Embassy and it's utterly magical. And at the same
| time, trying to do something for which there isn't a good
| no-std crate is, er, anti-magical to the point of forcing
| me to change the design. The ecosystem isn't as
| comprehensive yet.
|
| But when it works, wow. This has been the most fun
| embedded project I've ever done with the least debugging.
| oll3 wrote:
| I have replaced my use of C (and C++) in embedded with
| Rust for the last couple of years. Sure, some parts are
| still missing or immature, but overall also very
| promising. And I do enjoy it so much more than the MCU
| manufacturer's please-insert-your-c-code-between-these-
| comments-and-pray kind of development.
| bayindirh wrote:
| However, Rust doesn't have a "I know what I'm doing, let me
| be" switch. No, "unsafe" is not that.
|
| 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.
| kouteiheika wrote:
| > 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.
|
| 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.
| bayindirh wrote:
| Thanks for the information, I'll look into that.
|
| > So instead of doing this the dumb way and enabling
| -ffast-math for the whole program (which most certainly
| won't need it)...
|
| When you write HPC enabled simulation code, you certainly
| need it, and this is where I use it.
| kiratp wrote:
| > Rust doesn't have a "I know what I'm doing, let me be"
| switch.
|
| 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.
| bayindirh wrote:
| > Every piece of code you write is security-sensitive.
|
| No, not every application is CRUD, not every application
| is interactive and no, not every application serves users
| and communicate with outside world in a way allowing to
| be abused during its lifetime.
|
| This doesn't mean it gives me the freedom to write
| reckless code all over, but the security model of an
| authentication module and a mathematical simulation is
| vastly different.
| andrepd wrote:
| In what sense does rust not expose the full power of the
| processor to the user? Can you give a concrete example?
|
| > 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.
| bayindirh wrote:
| Rust probably won't allow me to implement a couple of
| lockless parallel algorithms I have implemented in my
| Ph.D., which are working on small matrices (3K by 3K).
| These algorithms allow any number of CPUs do the required
| processing without waiting for each other, and with no
| possibility of stepping on each other, by design.
|
| In the tests, it became evident that the speed of the
| code is limited by the memory bandwidth of the system,
| yet for the processors I work with that limit is also
| very near to practical performance limit of the FPUs, as
| well. I could have squeezed a bit more performance by
| reordering the matrices to abuse prefetcher better, but
| it was good enough and I had no time.
|
| Well, the method I verify the said codebase is here [0].
| Also, if BSD guys can write secure code with C, everybody
| can. I think their buffer overflow error count is still
| <10 after all these years.
|
| [0]: https://news.ycombinator.com/item?id=31218757
| jacquesm wrote:
| For me the key is that I can lay things out in memory
| _exactly_ the way I want, if necessary to the point where I
| can fit things in cache entirely when I need the performance
| and only to break out of the cache when I 'm done with the
| data. This is obviously not always possible but the longer
| you can keep it up the faster your code runs and the gains
| you can get like that are often quite unexpected. I spend a
| lot more time with gprof than with a debugger.
| Cthulhu_ wrote:
| > For some reason people here just assume that anything that
| isn't web development or backend server CRUD stuff is a
| niche.
|
| 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.
| mcguire wrote:
| In the 1970s, the alternative to C was assembly. In fact, I can
| remember hearing stories of the fights needed to pry every-day,
| normal developers---not systems programmers, not power users---
| off of assembly to a "higher level language".
|
| 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.
| overgard wrote:
| The sheer amount of software that you're likely using right
| now that's written in C would seem to contradict your claim.
| SubjectToChange wrote:
| The market share of C in desktop, server, and other
| "enterprise" applications has _drastically_ dropped since
| the 90s. Nowadays it 's quite rare to see C chosen for new
| projects where it isn't required. In fact, despite of how
| pervasive it is, a huge amount of C code cannot be built on
| a pure C toolchain, i.e. C is essentially like Fortran.
| daymanstep wrote:
| Why is C inappropriate for applications work?
| grumpyprole wrote:
| The technical arguments should be obvious, e.g. spending
| ones complexity budget on manual memory management and
| avoiding footguns. But one amusing anecdote is that the
| open source GNOME project founders were so traumatized by
| their experience building complex GUI apps in C (already a
| dubious idea 20 years ago), that they started building a C#
| compiler and the Mono project was born.
| seabass-labrax wrote:
| In 2023, you'll be hard pushed to find a GNOME
| application actually using C# and Mono. The vast majority
| of GNOME components are written in C, with a large number
| of them written in Vala and some in Rust and JavaScript.
| grumpyprole wrote:
| It was indeed a big yak to shave. Cloning a Microsoft
| technology probably wasn't a good idea for OSS adoption
| either.
| TillE wrote:
| Try writing a generic, reusable data structure in C. It's
| agony.
| bluetomcat wrote:
| You don't need to write generic and reusable data
| structures in C. You write a data structure suited for
| the problem at hand, which often means that it's going to
| be simpler and more performant because of the known
| constraints around it.
| grumpyprole wrote:
| It _could_ be more performant because of the known
| constraints around it, or it could be an ad-hoc,
| informally-specified, bug-ridden, slow implementation of
| half of some data structure. At least with a generic and
| resuable data structure you have a known _reliable_
| building block. Again, performance over safety.
| commonlisp94 wrote:
| > It could be more performant
|
| No, it almost always is. The designers of a generic
| library can't anticipate the use case, so can't make
| appropriate tradeoffs.
|
| For example, compare `std::unordered_map` to any well
| written C hash table. The vast majority of hash tables
| will never have individual items removed from them, but a
| significant amount of complexity and performance is lost
| to this feature.
| SubjectToChange wrote:
| _No, it almost always is._
|
| A library author can spend ridiculous amounts of time
| refining and optimizing their implementations, far more
| than any application programmer could afford or justify.
|
| _The designers of a generic library can 't anticipate
| the use case, so can't make appropriate tradeoffs._
|
| This is definitely not true. Take C++ for instance, not
| only is it possible to specialize generic code for
| particular types, but it's absolutely routine to do so.
| Furthermore, with all sorts of C++ template features
| (type traits, SFINAE, CRTP, Concepts, etc) even user-
| defined types can be specialized, in fact it's possible
| to provide users with all sorts of dials and knobs to
| customize the behavior of generic code for their own use
| case. This functionality is not just a quality-of-life
| improvement for library users, it has profound
| implications for performance portability.
|
| _For example, compare `std::unordered_map` to any well
| written C hash table._
|
| std::unordered_map is a strawman. There are a plethora of
| generic C++ hash tables which would match, if not soundly
| outperform, their C counterpart. Also, even if we blindly
| accepted your claim, then how do you explain qsort often
| being beaten by std::sort or printf and its variants
| being crushed by libfmt? What about the fact that Eigen
| is a better linear algebra library than any alternative
| written in C?
| NavinF wrote:
| That must be why every C project has its own string,
| dynamic array, hashtable, etc. It's definitely more
| performant to have several different implementations of
| the same thing fighting for icache
| commonlisp94 wrote:
| It's a nice thought, but in practice C binaries are
| orders of magnitude smaller than any other language. Also
| compilers make that tradeoff for inlining all the time.
| overgard wrote:
| To be fair, most C++ projects (outside of embedded and
| games) use STL for string/dynamic array/hashtable, and
| while that standardization is certainly convenient I'm
| not sure STL is generally faster than most hand written C
| data structures, even with the duplicated code.
| commonlisp94 wrote:
| That's writing some other language in C syntax. You use
| arrays or you write a specialized version for the use
| case.
| StillBored wrote:
| Agony might be a bit much, and i'm not trying to defend C
| because this is one of the strong reasons for using
| C++/STL...
|
| 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.
| sn_master wrote:
| Try doing the same in Go and it's even worse.
| Thiez wrote:
| In the 1960s they already had languages such as Lisp,
| Fortran, Algol, Basic... Even Pascal is two years older than
| C, and ML also came out around that time.
|
| The statement "the alternative to C was assembly" is simply
| incorrect.
| notacoward wrote:
| The fact that those things _existed_ does not refute the GP
| 's point. Many companies well into the 80s at least had
| programmers and codebases that had to be weaned away from
| assembly, despite the fact that higher-level alternatives
| had existed for a while. That's just empirical fact,
| regardless of the reasons. I myself started programming in
| 68K assembly on the original Mac because _I couldn 't
| afford_ a compiler and interpreted languages (there were at
| least Lisp and Forth) couldn't be used to write things like
| desk accessories. Remember, gcc wasn't always something one
| could just _assume_ for any given platform. The original
| statement is correct; yours is not, because of limited
| perspective.
| jacquesm wrote:
| For practical purposes - involving real world constraints
| in memory size, cpu power and storage - C was _the_ tool of
| choice, it ran on just about everything, allowed you to get
| half decent performance without having to resort to
| assembler and was extremely memory efficient compared to
| other languages. Compilers were affordable (Turbo C for
| instance, but also many others such as Mark Williams C)
| even for young people like me (young then...) and there was
| enough documentation to get you off the ground. Other
| languages may have been available (besides the ubiquitous
| BASIC, and some niche languages such as FORTH) but they
| were as a rule either academic, highly proprietary and
| very, very expensive.
|
| 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.
| marcosdumay wrote:
| On practice those were either proprietary or required beefy
| machines that the people writing C couldn't get.
|
| 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.
| StableAlkyne wrote:
| Fortran predates C by over a decade, and dozens of
| compilers existed for it by the mid-60s. Much of that
| legacy code is still in use to this day in scientific
| computing. One example: John Pople won his Nobel prize
| for work he did (and implemented in Fortran) in
| computational chemistry - the prize was delayed until '98
| but he did the work in the 60s. The software he
| commercialized it into, Gaussian, still gets releases to
| this day and remains one of the most popular
| computational chemistry packages.
|
| 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.
| jacquesm wrote:
| I tried to get access to FORTRAN and it was just way too
| expensive and required machines that I would not have
| been able to get close to. C ran on anything from Atari
| STs, IBM PCs, Amiga's and any other machine that an
| ordinary person could get their hands on.
|
| The other mainstream language at the time was BASIC,
| comparable to the way PHP is viewed today by many.
|
| And with the advent of the 32 bit x86 era GCC and djgpp
| as well as early Linux systems really unlocked a lot of
| power. Before then you'd have to have access to a VAX or
| a fancy workstation to get that kind of performance out
| of PC hardware. It's funny how long it took for the 386
| to become fully operational, many years after it was
| launched you had to jump through all kinds of hoops to be
| able to use the system properly, whereas on a 68K based
| system costing a tiny fraction that was considered
| completely normal.
| StableAlkyne wrote:
| My perspective is a bit biased by scientific computing, I
| do more of that than enterprise stuff (and Python has
| been fine for personal use). It's cool to see the
| perspective of someone who was around for the early
| stages of it though.
|
| How did people see Fortran back then - nowadays it's seen
| as outdated but fast, but was it seen as interesting, and
| what drove you to seek it out?
|
| Other side question if it's okay, I keep seeing
| references to VAXen around historical documents and
| usenet posts from the 80s and 90s, what made them special
| compared to other hardware you had back then?
| jacquesm wrote:
| My one experience with FORTRAN was when working for a big
| dutch architect who made spaceframes, I built their cad
| system and a colleague built the finite element analysis
| module based on an existing library. We agreed on a
| common format and happily exchanged files with
| coordinates, wall thicknesses and information about the
| materials the structure was made out of and forces in one
| direction and a file with displacements in the other. It
| worked like a charm (combined C and FORTRAN). I thought
| it was quite readable, it felt a bit archaic but on the
| whole not more archaic than COBOL which I had also worked
| with.
|
| The reason that library existed in FORTRAN was that it
| had a native 'vector' type and allowed for decent
| optimization on the proper hardware (ie: multiply and
| accumulate) which we did not have. But the library had
| been validated and was approved for civil engineering
| purposes, porting it over would have been a ton of work
| and would not have served any purpose, besides it would
| have required recertification.
|
| As for VAXen: A VAX 11/780 is a 32 bit minicomputer,
| something the size of a large fridge (though later there
| were also smaller ones and today you could probably stick
| one on a business card). It had a - for the time - a
| relatively large amount of memory, and was a timesharing
| system, in other words, multiple people used the same
| computer via terminals.
|
| They weren't special per se other than that a whole raft
| of programmers from those days cut their teeth on them
| either because they came across them in universities or
| because they worked with them professionally. They were
| 'affordable' in the sense that you did not need to be a
| multinational or a bank in order to buy one, think a few
| hundred thousand $ (which was still quite a fortune back
| then).
|
| I had occasional access to one through the uni account of
| a friend, but never did any serious work with them. The
| first powerful machine I got my hands on was the Atari
| ST, which had a 68K chip in it and allowed the connection
| of a hard drive. Together those two things really boosted
| my capabilities, suddenly I had access to a whole 512K of
| RAM (later 1M) and some serious compute. Compared to a
| time shared VAX it was much better, though the VAX had
| more raw power.
|
| Concurrent to that I programmed on mainframes for a bank
| for about a year or so, as well as on the BBC Micro
| computer (6502 based) and the Dragon 32 (a UK based Color
| Computer clone).
|
| Fun times. Computing back then was both more and less
| accessible than it is today. More because the machines
| were _so_ much simpler, less because you didn 't have the
| internet at your disposal to look stuff up.
| grumpyprole wrote:
| IIRC, Mac OS Classic was written in Pascal, as were other
| operating systems. C just won the popularity contest.
| marcosdumay wrote:
| That was about a decade after C had already won.
| grumpyprole wrote:
| The Xerox Alto was early 70's and that GUI OS was written
| in Pascal. I always thought Pascal was better designed
| and less bizarre than C. Null terminated strings were
| particularly a bad idea.
| marcosdumay wrote:
| Oh, by 73. Impressive. I didn't know that.
|
| Still, that was a much more powerful machine than the
| ones people wrote C for. And when the cheap segment of
| those "fridge computers" became powerful enough to run
| whatever you wanted to put on it, people started using
| small workstations. And when those workstations became
| powerful enough, we got PCs.
|
| It's only when the PCs got powerful enough that we could
| start to ignore the compiler demands and go with whatever
| language fit us better. (The later reductions all were
| based on cross-compiling, so they don't matter here.)
| mcguire wrote:
| How many Lisp Machines did Texas Instruments sell? (I mean
| outside of that one lab at UT Austin.) :-)
|
| 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._
| pjmlp wrote:
| Only inside Bell Labs, the world outside was enjoying high
| level systems programming languages since 1958 with the
| introduction of JOVIAL.
| Gibbon1 wrote:
| When talking about hot path optimizations though assembly is
| still a good alternative.
| ianlevesque wrote:
| Thanks for giving an actual example of such optimizations. In
| my personal experience my C++ (and Rust) code was often
| outperformed by the JVM's optimizations so I've found it hard
| to relate to the tradeoffs C++ developers assume are obvious to
| the rest of us.
| grumpyprole wrote:
| +1. Part of the problem is that x86 assembly is hardly
| programming the metal anymore. The performance
| characteristics of processors has changed over the years
| also, compilers can be more up-to-date than humans.
| overgard wrote:
| x86 assembly doesn't represent the actual opcodes the CPU
| executes anymore, but it's still the low level "API" we
| have to the CPU. Even if assembly isn't programming to the
| metal, it's definitely more to the metal than C, and C is
| more to the metal than Java, etc. Metalness is a gradient
| grumpyprole wrote:
| Lol, I like the word "metalness".
| SubjectToChange wrote:
| _Metalness is a gradient_
|
| 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.
| kllrnohj wrote:
| It would be fascinating if you could give any such comparison
| examples. The only time I've seen JVM come anywhere close to
| C++ in normal usage is if the C++ code was written like it
| was Java - that is, lots of shared_ptr heap allocations for
| nearly everything. Or perhaps you're one of the rare few that
| write Java code like it was C instead? You can definitely get
| something fast like that, but it seems all too rare.
| overgard wrote:
| I think the reason to use C/C++ over java has less to do with
| the various optimizations and more to do with control over
| memory layout (and thus at least indirect control over cache
| usage and so on). Plus you remove a lot of "noise" in terms
| of performance (GC hiccups, JIT translation, VM load time,
| etc.).
| lbrandy wrote:
| I struggle to resonate with what you are saying, as my
| experience is the opposite. I'm curious where this
| discrepancy is rooted. Reckless hypothesis: are you working
| on majority latency or majority throughput sensitive systems?
|
| 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".
| ianlevesque wrote:
| The specific example that comes to mind was translating a
| Java application doing similarity search on a large dataset
| into fairly naive Rust doing the same. Throughput I guess.
| It may be possible to optimize Rust to get there but it's
| also possible to (and in this case did) end up with less
| understandable code that runs at 30% the speed.
|
| 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?
| grumpyprole wrote:
| I've seen C++ systems that are considerably slower than
| equivalent Java systems, despite the lack of stack
| allocation and boxing in Java. It's mostly throughput,
| malloc is slow, the C++ smart pointers cause memory
| barriers and the heap fragments. Memory management for
| complex applications is hard and the GC often gives better
| results.
| vvanders wrote:
| I've seen so may flat profiles due to shared_ptr. Rust
| has done a lot of things right but one thing it really
| did well was putting a decent amount of friction into
| std::sync::Arc<T>(and offering std::rc::Rc<T> when you
| don't want atomics!) vs &mut T or even Box<T>. Everyone
| reaches for shared_ptr when 99% of the time unique_ptr is
| the correct option.
| Gibbon1 wrote:
| From my experience with embedded coding you are correct.
| Most stuff lives and dies enclosed in a single call chain
| and isn't subject to spooky action at a distance. And
| stuff that is I often firewall it behind a well tested
| API.
| latenightcoding wrote:
| Read your article (and cloudfare's) and as someone who uses
| musttail heavily I don't understand the hype, as you mentioned
| in your blog: you can get tail call optimizations with (-O2),
| musttail just gives you that guarantee which is nice, but the
| article makes it sound as if it unlocks something that was not
| possible before and interpreters will greatly benefit from it,
| but it's more reasonable to ask your user to compile with
| optimizations on than it is to ask them to use a compiler that
| supports musttail (gcc doesn't). Moreover, musstail has a lot
| of limitations it would be hard to use in more complex
| interpreter loops
| mtklein wrote:
| Ordinarily you're at the whim of the optimizer whether calls
| in tail position are made in a way that grows the stack or
| keeps it constant. musttail guarantees that those calls can
| and are made in a way that does not let the stack grow
| unbounded, even without other conventional -On optimization.
| This makes the threaded interpreter design pattern safe from
| stack overflow, where it used to be you'd have to make sure
| you were optimizing and looking carefully at the output
| assembly.
|
| 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!
| jjoonathan wrote:
| Perf is a niche, here's another: address space lets you talk to
| hardware.
|
| 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.
| packetlost wrote:
| Idk what you're on about, I mmaped a `/dev/uio` from Python
| this morning. Yeah, I had to add it in a .dts file and
| rebuild my image, but even slow as shit high level languages
| like Python let you bang on registers if you really want to.
| notacoward wrote:
| That worked because you were on an OS that supported it,
| using a device with simple enough behavior that things like
| timing or extra/elided writes didn't matter. It's great
| when that works, but there are _very_ many environments and
| devices for which that option won 't exist.
| SubjectToChange wrote:
| Perhaps, but C and C++ assume flat address spaces and modern
| hardware includes many programmable devices with their own
| device memory, e.g. GPUs. Naturally this discontinuity causes
| a great deal of pain and many schemes have been developed to
| bridge this gap such as USM (Unified Shared Memory).
|
| 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.
| vvanders wrote:
| Those disjoint memory addresses can be an absolute pain to
| deal with, ask anyone who had to spend time dragging
| performance out of the PS3 despite it being faster on
| paper. UMA/USM can also bring it's own set of issues when
| you have pathological access patterns that collide with
| normal system memory utilization.
|
| 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).
| wrs wrote:
| Rust is making advances here (look at "embedded Rust"
| efforts). I am curious since I haven't written kernel code
| since C went sideways: how easy is it to write a driver that
| has to manipulate registers with arbitrary behavior at
| arbitrary addresses with modern C compilers and avoid all
| undefined behavior? I seem to recall Linus has a rant on
| this.
| SubjectToChange wrote:
| Of course the problem is that the vast corpus of legacy C code
| was not written with such aggressive compilers in mind.
| jfengel wrote:
| With modern CPUs, that kind of hand-tuned assembly gets rarer
| and rarer. Pipelines and caching and branch prediction make it
| hard to know what's actually going to be faster. And even when
| you do know enough about the CPU, you only know that CPU --
| sometimes only one model of a CPU.
|
| There's still a niche for it, but it's tiny and it keeps
| getting tinier.
| izacus wrote:
| That... really isn't all that true. I find that myth being
| mostly perpetuated by people who don't do any kind of
| performance work or have any understanding just how terribly
| unperformant most code out there is.
| jfengel wrote:
| That is my observation as a compiler writer.
| khuey wrote:
| > 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.
|
| Do you repeat this exercise when you upgrade or change
| compilers?
| 8n4vidtmkvmk wrote:
| If the assembly is that important I'd find a way to put it
| into my unit tests. Create a "golden" for the assembly and it
| should trigger a diff if it changes.
| jacquesm wrote:
| The easy way to achieve that is to freeze the assembly once
| it is generated and to keep the C inlined around as
| documentation, as well as a way to regenerate the whole thing
| should it come to that (and then, indeed you'll need to audit
| the diff to make sure the code generator didn't just optimize
| away your carefully unrolled loops).
| asvitkine wrote:
| Is there any tooling for that or are you talking
| hypothetically?
| jacquesm wrote:
| Just standard unix tooling, what else do you need? It's
| as powerful a set of text manipulation tools as you could
| wish for.
| [deleted]
| omoikane wrote:
| Regarding uninitialized variables, there is a proposal to make
| them default to zero-initialized:
|
| http://wg21.link/P2723
|
| Under "5. Performance" section, it claims the performance impact
| to be negligible, and there is also a mechanism to opt-out.
| deadletters wrote:
| There are a lot of warning and error options. Turn the guiderails
| and sanitizers on during development and testing. Turn the
| optimizations on when you ship.
|
| Check out: -wall -werror
| https://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html
| xjay wrote:
| C = Control
|
| 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...
| agwa wrote:
| Undefined behavior is the opposite of programmer control. As
| the examples in the blog post show, you can write code that
| explicitly deferences a NULL pointer, or enters an infinite
| loop, and the compiler will think _surely the programmer didn
| 't mean to do that_, and literally remove code that you wrote.
|
| 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.
| Rexxar wrote:
| IMHO, having an additional debug mode where all optimisations
| steps are performed but with asserts inserted to validate all
| preconditions would mitigate a lot of problems.
|
| 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.
| metadat wrote:
| This is likely in response to this relevant related submission
| discussed yesterday:
|
| _A Guide to Undefined Behavior in C and C++ (2010)_
|
| https://news.ycombinator.com/item?id=37165042 (166 comments)
|
| A worthwhile read, IMO. Cheers.
| mannyv wrote:
| If you don't like undefined behavior then avoid it. It's not
| hard, unless you're not paying attention.
| not2b wrote:
| The reason for leaving integer overflow undefined was not
| primarily because of one's complement machines. It was for loop
| optimization.
|
| 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).
| IshKebab wrote:
| That sounds very unlikely given that _unsigned_ overflow _is_
| defined. The original motivation for undefined behaviour was
| not performance. I 'm pretty sure autovectorisation was not a
| thing in 1989. The author's theory sounds far more likely.
|
| I'd love to see an actual citation though if you aren't just
| guessing.
| dundarious wrote:
| I don't think that was the historical justification, but
| regardless, it is a terribly limiting and oblique hack of a
| convention for guiding the compiler. Why should this guidance
| only be possible for signed values? Why have such an important
| distinction be controlled in such an indirect fashion?
|
| Just 2 reasons why I prefer explicit vectorization anywhere
| that I feel it is important to have vectorized code.
| andersa wrote:
| It sure would be nice if modern CPUs had two sets of integer
| instructions: one that wraps, and one that triggers an
| exception on wrap, with zero overhead in non-wrapping case.
|
| Then we could compile all code with the latter, except for
| specifically marked edge cases where wrap is a desired part of
| the logic.
| tpolzer wrote:
| That doesn't help at all if your loop variable is a 32 bit
| int that your compiler decided to transform away into
| vectorized loads from a 64 bit pointer.
|
| But that's exactly one of the transformations that get
| enabled by assuming undefined overflow.
| andersa wrote:
| But in that case, it's not going to be wrapping either.
| We'll just read beyond the end of the buffer, which a
| bounds check should catch.
|
| 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.
| fulafel wrote:
| x86 had overflow checking support (via OF flag and INTO insn)
| but it got slowed down and later dropped from 64 bit mode.
| solarexplorer wrote:
| MIPS for example has this. It has `addu` for normal integer
| addition that does not trap and `add` if you want to trap on
| overflows.
| eklitzke wrote:
| You can compile code with -fwrapv and for most programs the
| overhead is minimal (the exception being that if you're
| writing number crunching code, the overhead is going to be
| huge). For my personal projects I have -fwrapv as part of the
| default compiler flags, and I remove the flag in opt builds.
| I honestly haven't caught that many bugs using it, but for
| the few bugs it did catch it saved me a lot of debugging
| time.
| explaininjs wrote:
| Language-level concern imo. https://doc.rust-
| lang.org/nightly/core/num/struct.Wrapping.h...
| andersa wrote:
| It could be, if the hardware supported it. Consider this
| quote from that page:
|
| "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.
| tialaramex wrote:
| > That's not good enough. We want to always detect it
|
| 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.
| andersa wrote:
| Well, the whole point of my post was that I would really
| like a hardware feature that does it without overhead.
| How that would work behind the scenes, I have no idea.
| Not a hardware engineer.
| tialaramex wrote:
| If you really want it, use it. Hardware vendors optimise
| stuff they see being done, they don't optimise stuff that
| somebody mentioned on a forum they'd kinda like but have
| never used because it was expensive. _Maybe_ if you have
| Apple money and can buy ARM or something then you could
| just express such whims, but for mere mortals that 's not
| an option.
|
| Newer CPUs in several lines clearly optimise to do
| Acquire-Release because while it's not the only possible
| concurrency ordering model, it's the one the programmers
| learned, so if you make that one go faster your benchmark
| numbers improve.
|
| Modern CPUs often have onboard AES. That's not because it
| won a competition, not directly, it's because everybody
| uses it - without hardware support they use the same
| encryption in software.
|
| The Intel 486DX and then the Pentium happened because it
| turns out that people like FPUs, and eventually enough
| people were buying an FPU for their x86 computer that you
| know, why not sell them a $100 CPU and a $100 FPU as a
| single device for $190, they save $10 and you keep the
| $80+ you saved because duh, that's the same device as the
| FPU nobody is making an actual separate FPU you fools.
|
| Even the zero-terminated string, which I think is a
| terrible idea, is sped up on "modern" hardware because
| the CPU vendors know C programs will use that after the
| 1970s.
| weinzierl wrote:
| ARM actually kind of has that. The register file has an
| overflow flag that is not cleared on subsequent operations
| (sticky overflow). So instead of triggering an exception,
| which is prohibitively expensive, you can do a series of
| calculations and check afterwards if an overflow happened in
| any of them. A bit like NaN for floating point. From what I
| understand the flag alone is still costly, so we will have to
| see if it survives.
| spookie wrote:
| That's quite interesting and reasonable
| andersa wrote:
| It doesn't matter if triggering the exception is expensive.
| At that point overflow has already occured, so your program
| state is now nonsense, and you might as well just let it
| crash unhandled. Much better outcome than reading memory at
| some mysterious offset.
|
| 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.
| Findecanor wrote:
| > At that point overflow has already occured, so your
| program state is now nonsense, and you might as well just
| let it crash unhandled.
|
| The trick is to check and clear the flag before any
| instruction that would have a side-effect, that depends
| on the arithmetic result.
|
| IEEE 754-compliant floating point units have a similar
| behaviour with NaN that is a bit more versatile: an
| arithmetic instruction results in NaN if any operand is
| NaN, but an instruction with side-effect (compare,
| convert or store) will raise an exception if given a NaN.
| weinzierl wrote:
| Think about it like that. If you allow an exception you
| essentially create many branches with all their negative
| consequences. With the sticky bit you combine them to one
| branch (that is still expensive[1]).
|
| [1] https://news.ycombinator.com/item?id=8766417
| [deleted]
| Filligree wrote:
| Maybe the idea hadn't been invented back then, but it seems
| obvious to me that the correct response is an iterator
| protocol. Or at least a hardcoded for-in syntax.
| kimixa wrote:
| If you don't initialize the initial iterator state, or
| compare with something that isn't a valid iterator of the
| same container, you kinda end up with the same issues.
|
| 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.
| dahfizz wrote:
| Care to explain? An iterator is a nice high level concept,
| but the CPU still has to do the &in + i + offset arithmetic.
| I don't see how replacing `i` with syntactic sugar changes
| the need to check for overflow.
| nostrademons wrote:
| I think the point the GP is making is that with an iterator
| protocol, _the iterator implementation_ itself is free to
| make a different choice on implementation strategies, based
| on the shape of the data and the hardware available, and
| this is transparent to client code. So for example, a
| container containing only primitive ints or floats on a
| machine with a NVidia Hopper GPU might choose to allocate
| arrays as a multiple of 64, and then iterate by groups of
| 64, taking advantage of the full warp without needing any
| overflow checks. Obviously a linked list or an array of
| strings couldn 't do this, but then, they wouldn't want to,
| and hiding the loop behind an iterator lets the container
| choose the appropriate loop structure for the format and
| hardware.
|
| 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.
| PaulDavisThe1st wrote:
| "It's interesting that a lot of performance-critical code
| tends to be written in C++, which sometimes pretends not
| to be that low level. The reason is because the actual
| performance critical code is really running CPU-specific
| assembly, so C++ just holds the high level intent of the
| model and the optimization happens on the primitives that
| the processor actually uses."
| dahfizz wrote:
| > the iterator implementation itself is free to make a
| different choice on implementation strategies
|
| That's just UB with more steps. What will the spec say?
| "Behavior of integer overflow is undefined. Unless the
| overflow happens within an iterated for loop, in which
| case the behavior is undefined and the iterator can do
| whatever it wants".
|
| > I've heard criticisms of C and C++ that they are
| simultaneously too high-level and too low-level.
|
| I've heard this as well, and I think there is some truth
| to it, but C is the least-bad offender relative to any
| other language.
|
| C maps extremely well to assembly. The fact that assembly
| no longer perfectly captures the implementation of the
| CPU has nothing to do with C. Every other general
| purpose[1] language has to target the same abstraction
| that C does.
|
| Given that reality, C in fact maps better to the hardware
| than any other language. Because it is faster than any
| other language. Any higher level language that gives the
| compiler more information about algorithm choice is
| slower than C is. That's the bottom line.
|
| [1] This is ignoring proprietary, hardware specific tools
| like CUDA. That's clearly in a different category when
| discussing programming languages, IMO.
| nostrademons wrote:
| The reasoning behind the decision to make integer
| overflow UB changes. As the thread starter mentioned,
| that reasoning was loops, so you don't need an overflow
| counter for everyday loops. Take loops out of the
| equation, and take certain high-performance integer
| computations where arguably you should have a dedicated
| FixedInt type, and the logical spec behavior might be
| silent promotion to BigInt (like JS, Python 3, Lisp,
| Scheme, Haskell) or a panic (like in Rust).
|
| > [1] This is ignoring proprietary, hardware specific
| tools like CUDA. That's clearly in a different category
| when discussing programming languages, IMO.
|
| Arguably they should be part of the conversation. One
| main reason for the recent ascendancy of NVidia over
| Intel is that they're basically unwrapping all the layers
| of microcode translation that Intel uses to make a modern
| superscalar processor act like an 8086, and saying "Here,
| we're going to devote that silicon to giving you more
| compute cores, you figure out how to use them
| effectively."
| commonlisp94 wrote:
| > implemented in Python,
|
| A program which constructs an AST out of python classes
| and spits out GPU code is a compiler. The python is never
| executed.
|
| Trivially, compilers can generate code faster than their
| hose language, but that doesn't make the host language
| fast. The compiler would be even faster if it were
| written in C++.
| alexvitkov wrote:
| Adding an iterator in C++ means adding at least 2 more
| objects, multiple function calls, operator overload,
| templates, the whole package.
|
| You don't trust your compiler to optimize the sane trivial C
| case, but you trust it to optimize all that garbage away?
| RicardoLuis0 wrote:
| if your data's sequential, creating an iterator in C++ is
| as simple as returning a begin and end pointer, and will be
| optimized away by any level other than O0
|
| https://godbolt.org/z/WEjzEr5j4
| josefx wrote:
| But iterating over pointers is once again optimized with
| lots of undefined behavior at the corners. So you are
| replacing one source of undefined behavior with another.
| AYoung010 wrote:
| Replacing undefined behavior at the program-level with
| undefined behavior written and tested as part of the
| standard library, usually vendored and distributed in
| concert with the compiler, seems like an obvious net-
| positive to me.
| commonlisp94 wrote:
| > with lots of undefined behavior at the corners.
|
| What behavior is undefined in incrementing a pointer
| between a begin and end range?
| josefx wrote:
| Of course a basic iteration between begin()/end() will
| never contain out of range elements, but neither will
| valid increment between two integers. No need for
| iterators in that case either.
|
| Say I want to do something fancy, like getting element
| number 11 from an array.
|
| With an integer index I can pass 11, with random access
| iterators I can use begin() + 11.
|
| Now my array only has five elements. So I check.
|
| 11 < 5? Comparison valid, successfully avoided a crash.
|
| begin() + 11 < end() ? How where the rules for pointer
| comparison again, something about within an allocation
| and one past the end?
| yazaddaruvala wrote:
| Pointer arithmetic optimization based on undefined
| behavior is a problem regardless.
|
| Life is always better after minimizing the total number
| of types of undefined behavior.
| commonlisp94 wrote:
| A pointer is a valid C++ iterator.
|
| > 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.
| titzer wrote:
| The solution is to have the compiler automatically split
| iteration into a known in-bounds part and a possibly-out-of-
| bounds part. In this case, generating an additional check that
| {ARRAY_WIDTH < (INT_MAX - offset)} would be sufficient to
| guarantee that {i + offset} doesn't wrap around, enabling
| further reasoning in a specialized copy of the loop. (In this
| example, it's unclear what the relation to ARRAY_WIDTH is to in
| and out).
|
| 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.
| logicchains wrote:
| >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.
| titzer wrote:
| > these kind of super-hot loops are quite common
|
| 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.
| TapamN wrote:
| >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.
|
| 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.
| haberman wrote:
| > When in reality, real programs do not spend all their
| time in one (or even a handful) of super-hot loops.
|
| 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).
| kllrnohj wrote:
| The idea that only a tiny portion of code is "hot" is just
| not true.
|
| 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.
| crabbone wrote:
| This is a really, really misguided take on C and C++...
|
| 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).
| fooker wrote:
| Wow, don't get so invested in hating a technology, it's just a
| tool.
| kazinator wrote:
| > _vectorization is one of the obvious ways to get better
| performance in a wide range of situations._
|
| 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).
| thesuperbigfrog wrote:
| "Performance versus correctness" is the same design tradeoff as
| "Worse is Better":
|
| 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.
| ngrilly wrote:
| Masterful conclusion in the last paragraph!
| strangescript wrote:
| If only we had a modern language that did both... (which was
| strangely omitted from this article)
| aatd86 wrote:
| > C and C++ do not require variables to be initialized on
| declaration (explicitly or implicitly) like Go and Java. Reading
| from an uninitialized variable is undefined behavior.
|
| 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 :-)
| kazinator wrote:
| Correctness can come from the programmer in ways that performance
| cannot.
|
| 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.
| dralley wrote:
| Performance can absolutely come from the programmer. It's a
| question of defaults. One can default to correctness and opt-in
| to performance (in the places where critically needed, often a
| small portion of the program) much more easily than one can
| default to performance and opt-in to correctness.
| dahfizz wrote:
| > Performance can absolutely come from the programmer.
|
| 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.
| [deleted]
| Joel_Mckay wrote:
| Professional C/C++ coders tend to recognize the history of cross-
| platform compatibility issues, and tend to gravitate to a simpler
| subset of the syntax to avoid compiler ambiguity (GNU gcc is
| ugly, but greatly simplified the process).
|
| 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, =)
| andai wrote:
| This needs a NSFW tag! Good lord...
| philosopher1234 wrote:
| >For example, a common thing programmers expect is that you can
| test for signed integer overflow by checking whether the result
| is less than one of the operands, as in this program:
| #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...
| pdw wrote:
| This is why I hate it when people describe C as "portable
| assembler". The Usenet comp.lang.c FAQ was already thirty years
| ago warning people not to write overflow checks like this.
| coliveira wrote:
| You cannot check for overflow like this, because you're causing
| it. There are other ways to do this without adding first:
| #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; }
| pclmulqdq wrote:
| This is why people who put "C/C++" on their resume don't
| usually know either language. In C++, integers are defined to
| be two's complement now, so that check is acceptable.
| rsc wrote:
| Not true, as the post explains.
| pclmulqdq wrote:
| C++20 made the switch. See P1236. It appears C23 has
| followed suit.
| rsc wrote:
| If P1236 (https://www.open-
| std.org/jtc1/sc22/wg21/docs/papers/2018/p12...) is really
| what it says ("alternative wording") then I don't believe
| that's true. Certainly P0907R3 (https://www.open-
| std.org/jtc1/sc22/wg21/docs/papers/2018/p09...) is clear:
|
| > Status-quo If a signed operation would naturally
| produce a value that is not within the range of the
| result type, the behavior is undefined. The author had
| hoped to make this well-defined as wrapping (the
| operations produce the same value bits as for the
| corresponding unsigned type), but WG21 had strong
| resistance against this.
|
| My understanding is that the 2s complement change in
| C++20 ended up defining bitwise operations but not
| arithmetic.
| uecker wrote:
| Two's complement does not mean signed overflow became
| defined. So the check is still wrong. And no, I do not
| think making it defined would lead to more correct
| programs. You then simply have really difficult wrap-
| around bugs instead of signed overflow which you can be
| found more easily with UB sanitizer or turn into traps at
| run-time.
| e4m2 wrote:
| It made the switch, sure, but overflow remains undefined.
| woodruffw wrote:
| That, or they know that C23 includes two's complement[1].
|
| [1]: https://en.cppreference.com/w/c/23
| pdw wrote:
| Integer overflow is still undefined behavior in C23.
| zer8k wrote:
| I'm not convinced a "common" C program would ever do such a
| thing. Even the "Effective C" book (one of the best books on C,
| imo) discusses why this is wrong and why you should take
| advantage of `limits.h` for bounds checking.
|
| 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.
| _dain_ wrote:
| >You've made an obviously non-sensical statement--
|
| Therefore it should _fail to compile_.
|
| You can even spin it as a performance enhancement: the whole
| program can be optimized away!
| layer8 wrote:
| The problem is, that's not how the logical inference in a
| compiler/optimizer works. It's very difficult to translate
| such an optimization back to "this statement has been
| optimized away", in the general case.
| _dain_ wrote:
| If it's so difficult to figure out if the consequences of
| their optimization game are sensible or not, then they
| shouldn't play it in the first place.
|
| Who actually wants it to behave this way, other than
| compiler engineers competing on microbenchmarks? Who is
| this language even _for_?
| layer8 wrote:
| It's a side effect of desirable optimizations for UB-free
| code. You can't have both at the same time.
| _dain_ wrote:
| Desirable for whom? Are the beneficiaries of it going to
| pay for the externalities they are generating, like
| polluters should?
|
| You can say "ordinary workaday programmers benefit from
| speed improvements en passant", but they weren't really
| given a choice in the matter, were they? When programmers
| are given an explicit binary choice of "correct, but
| slightly slower", and "wrong, but slightly faster", they
| pick the former in practically all cases (or they should,
| at any rate). But they can't make this choice; the
| compiler and spec writers go behind their backs and
| construct these inscrutable labyrinths, then blame
| everyone else for getting lost in them.
| lelanthran wrote:
| I think what really annoys me is that this looks like actual
| malice on the part of the standards writers, and less severe
| malice on the part of the compiler authors.
|
| 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.
| layer8 wrote:
| > The standards writers could have made all UB implementation
| defined, but they didn't.
|
| 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.
| torstenvl wrote:
| The standard does make all definable UB implementation
| defined. The compiler writers intentionally misread the
| standard to allow these kinds of optimizations.
|
| C17 Draft, SS 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.
| cesarb wrote:
| The optimization opportunities do indeed come from the
| first option, "ignoring the situation completely with
| unpredictable results". That is: the compiler assumes that
| the undefined behavior will not happen, and optimizes based
| on it. They do not "actively seek out the situation", it's
| in fact the opposite, they assume that the situation simply
| won't happen. And "deleting whole sections of code" is just
| the normal "unreachable code" optimization: code which
| cannot be reached on any feasible execution path will be
| deleted.
| [deleted]
| torstenvl wrote:
| That is an obvious misreading.
|
| Assume you have an unsigned char array r[] declared with
| 100 elements and the compiler encounters the following
| line of code:
|
| r[200] = 0xff;
|
| "ignoring the situation completely with unpredictable
| results" means translating that to the appropriate object
| code without bounds checking and no guarantees are made
| about whether that's a segfault or what your environment
| does.
|
| "assum[ing] that the situation simply won't happen" means
| eliding the code entirely.
|
| The first is required by the standard. The second is
| prohibited by the standard.
|
| There is no section of any version of the standard that
| permits compilers to assume that anything which might be
| UB or predicated on UB is dead code.
|
| Compilers _must_ either ignore the fact that it 's UB, or
| behave in a documented implementation-defined manner, or
| stop translation.
| lelanthran wrote:
| While you have convinced me that the compiler authors are
| more culpable than the standards committee, I still feel
| that they are ultimately responsible - the buck stops with
| them.
|
| They're free to, and easily able to, add the word
| "exhaustively" when listing possible behaviours, but they
| don't. They can even do away with it entirely and replace
| it with implementation-defined, which makes compilers non-
| conformant if they optimise out code.
| pdw wrote:
| You're quoting from a footnote. The footnotes are not part
| of the normative text, so how you choose to interpret it is
| irrelevant.
|
| The actual definition of undefined behavior given by SS
| 3.4.3 is: "behavior, upon use of a nonportable or erroneous
| program construct or of erroneous data, for which this
| International Standard imposes no requirements."
|
| Since no requirement is imposed, compilers can do as they
| want.
| torstenvl wrote:
| > _You 're quoting from a footnote. The footnotes are not
| part of the normative text_
|
| I am not quoting from a footnote.
|
| Footnote 1 pertains to SS 1.
|
| Footnote 2 pertains to SS 3.19.5.
|
| There are no footnotes pertaining to SS 3.4.3.
|
| And do you have a citation for the proposition that
| footnotes or notes of any other kind are not normative?
| pdw wrote:
| Apologies, it's a note, not a footnote. But notes in ISO
| standards aren't normative either.
|
| This is a general principle of ISO standards, see https:/
| /share.ansi.org/Shared%20Documents/Standards%20Activi...:
|
| > 6.5 Other informative elements
|
| > 6.5.1 Notes and examples integrated in the text
|
| > Notes and examples integrated in the text of a document
| shall only be used for giving additional information
| intended to assist the understanding or use of the
| document. They shall not contain requirements ("shall";
| see 3.3.1 and Table H.1) or any information considered
| indispensable for the use of the document, e.g.
| instructions (imperative; see Table H.1), recommendations
| ("should"; see 3.3.2 and Table H.2) or permission ("may";
| see Table H.3). Notes may be written as a statement of
| fact.
| kelnos wrote:
| I think it's a bit out of line to call this malice.
|
| 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.
| pjmlp wrote:
| That is usually a myth.
|
| From the people that were there at the time.
|
| "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
| lelanthran wrote:
| I fail to see to the point of your post in context of
| this thread.
|
| What, in the parent post, do you consider a myth?
| pjmlp wrote:
| C's original performance.
|
| Optimization taking advantage of UB had to be introduced
| during the 1990's to make it into a reality.
|
| Until the widespread of 32 bit hardware, even junior
| Assembly devs could relatively easy outperform C
| compilers.
|
| Which is what the interview is about, originally C lacked
| the optimization tools, relying on developers, with tons
| of inline Assembly.
| lelanthran wrote:
| > Which is what the interview is about, originally C
| lacked the optimization tools, relying on developers,
| with tons of inline Assembly.
|
| I'm afraid that is not the conclusion I draw from the
| snippet you posted.
|
| It's very clear _to me_ that the HLL under question at
| that time _prevented_ the programmer from performing the
| low-level optimisations that the C programmer could
| optionally do. The debate appeared to be be whether to
| let the language exclusively optimise the code, or just
| do no optimisation and let the programmer do it.
|
| This is why the sentence ": 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. " is in there - it's because the HLL languages
| literally wouldn't let the programmer optimise at the
| level that C would let the programmer optimise.
|
| Honestly, if the proponent for the HLL language didn't
| add in "Use the HLL, but not for OS and low-level code
| where you really need to fine-tune", you'd have a point,
| but they said it, and so you don't.
| pjmlp wrote:
| Right, that is why C code was polluted with inline
| Assembly until optimizers started taking advantage of UB.
|
| Inline Assembly isn't C.
| lelanthran wrote:
| > I think it's a bit out of line to call this malice.
|
| I apologise, but note (warning: weasel words follow) I was
| careful to say it _looks_ like malice to me, not that I
| have any indication that it actually was malice.
|
| > 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.
|
| I think it is better, purely because then the resulting
| code can't simply be omitted when an integer may overflow,
| the _code_ still has to be emitted by the compiler.
|
| Right now all the worst bits of UB has to do with the
| compiler optimising out code. With IB replacing UB, the
| implementation will have to pick a behaviour and then stick
| with it. Much safer.
| nottorp wrote:
| > less severe malice on the part of the compiler authors.
|
| 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.
| boryas wrote:
| -fwrapv and it's all good :)
| not2b wrote:
| It really isn't optional as a C developer to turn on warnings
| and get your code warning-free. This will eliminate the most
| common unbounded behavior issues, like uninitialized
| variables, though unfortunately not all of them.
| gavinhoward wrote:
| This is why I implemented two's-complement with unsigned types,
| that don't have UB on overflow.
| kragen wrote:
| like x > 2147483547u in this case?
| gavinhoward wrote:
| Correct. It will still overflow, but it's not UB and won't
| be optimized away.
| kragen wrote:
| by 'overflow' do you mean 'set the high bit'
| gavinhoward wrote:
| Correct.
|
| I have routines that take the unsigned values and
| interpret them as signed, two's-complement values.
|
| So you would write this: val = temp1 +
| temp2; if (y_ssize_lt(val, temp1)) y_trap();
|
| That `y_ssize_lt()` function computes whether `val` is
| less than `temp1` as though they were signed,
| two's-complement integers. But because they are actually
| unsigned, the compiler cannot be malicious and delete
| that code.
| kragen wrote:
| i see, thanks
|
| maybe we should develop a nonmalicious compiler
| tempodox wrote:
| This has to be the worst part of UB: That the compiler can
| assume it never happens. This way UB can affect your program
| even if the code that would exhibit the behavior is never
| executed.
| AlotOfReading wrote:
| What the article misses is that the code _wouldn 't_ work on
| all hardware. Take MIPS for example, where the signed overflow
| would generate a hardware exception that the OS might have
| implemented to do anything from nothing to killing the
| generating process.
|
| 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.
| lelanthran wrote:
| > 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.
| AlotOfReading wrote:
| Implementation defined means _it has a defined behavior_ on
| every implementation. In the MIPS case defining that
| behavior would force the compiler to generate strictly
| signed instructions for signed values _and_ define how the
| runtime platform handles these interrupts, rather than
| leaving the compiler free to generate "whatever works".
|
| 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.
| lelanthran wrote:
| > In the MIPS case defining that behavior would force the
| compiler to generate strictly signed instructions for
| signed values and define how the runtime platform handles
| these interrupts,
|
| I respectfully disagree: in this particular scenario,
| it's enough for the compiler vendor to write "generates
| an interrupt on overflow" with no indication of what the
| handling should be, and still be well within
| _implementation-defined_ behaviour.
|
| After all, the standard specifies what `raise()` does,
| and what `signal()` does, but doesn't specify how the
| runtime is going to handle `raise(signum)` when the
| program has not yet called `signal(signum, fptr)`.
|
| This scenario you presented displays _exactly_ why UB
| should be replaced with IB in the standard. I 've yet to
| see one good reason (including performance reasons) for
| why the dereferencing of a NULL pointer (for example)
| cannot be documented for the implementation.
|
| With UB, we have real-world examples of a NULL pointer
| dereference causing compilers to omit code resulting in a
| program that continued to run but with security
| implications. If changed to IB, the compiler would be
| forced to emit that code and let the dereference crash
| the program (much better).
| justincredible wrote:
| [dead]
| hot_gril wrote:
| This is a cool summary of UB, but speaking to the title... Well
| yeah. Did anyone suggest otherwise?
| SenAnder wrote:
| A periodic reminder is good.
| uecker wrote:
| It should be "C and C++ Implementations" because nothing in the
| standard requires UB to be exploited for optimization instead
| of adding run-time checks.
| hot_gril wrote:
| The standard also doesn't require UB to have run-time checks,
| which is probably for the goal of performance. There was an
| implementation in mind when it was designed.
| uecker wrote:
| There were C implementations with bounds checking or
| implementations that trap on use of an invalid pointer or
| on read of uninitialized variables etc. A large part of UB
| was introduced for such reasons and not for performance.
| The standard was (and still is) about allowing a wide range
| of implementations.
| philosopher1234 wrote:
| To me this article is interesting because
|
| * 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.
| hot_gril wrote:
| Yeah, I agree. I was editing my comment to praise the
| article's body as you were replying.
| rsc wrote:
| There is no major upcoming Go change related to this post.
| This is just a much longer version of
| https://research.swtch.com/plmm#ub that I drafted years ago
| and finally cleaned up enough to post.
| philosopher1234 wrote:
| It's fine, I'm not sad or anything
| coliveira wrote:
| The issue of UB has everything to do with how compilers implement
| it. If people are having problems, they should complain to
| compiler writers. They always have the option of creating slower
| code that checks for obvious problems like initialized variables.
| However, if a company/project writes a compiler that is a little
| slower than the competitor, people with almost always complain
| that it is a bad compiler. So the result is what you have
| nowadays: they're always looking for every small opportunity to
| generate faster code at the expense of safety.
| weinzierl wrote:
| _" [..] this International Standard imposes no requirements on
| the behavior of programs that contain undefined behavior. "_
|
| 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.
| dale_glass wrote:
| You do. Take the EraseAll example
|
| 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.
| weinzierl wrote:
| _" Ideally it's a compile time error."_
|
| 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.
| dale_glass wrote:
| Nonsense. The standard literally doesn't bind us in any
| way whatsoever. UB means there are no rules, anything
| goes. If the compiler is free to make demons fly out of
| my nose, it's equally free to produce a nice error
| message to the effect of "Don't do that".
|
| I'd much prefer if the standards people started defining
| every possible instance of UB as a fatal error or as an
| implementation defined behavior, but that's not strictly
| required.
| layer8 wrote:
| Some instances of UB are data-dependent, and some would
| require solving the halting problem to statically
| distinguish them from non-UB. What you propose is
| therefore not generally possible at compile time, and at
| runtime only with considerable performance impact.
| weinzierl wrote:
| Thanks for pointing that out. Detecting UB is hard and
| sometimes impossible in C and C++. If you design your
| language accordingly you can avoid that. At least I think
| that is how Rust deals with it, but I'm happy to be
| convinced otherwise.
| coliveira wrote:
| If some form of UB is impossible to detect, then the
| compiler cannot do anything about it either, making the
| whole debate useless. Any action taken by a compiler
| relating to UB must be for some cause that is detectable
| at compilation time.
| layer8 wrote:
| That's not quite true, the compiler could add code to
| detect all UB at runtime, and then abort or whatever.
| It's just that this would also pessimize UB-free code,
| and most compilers opt to not do that, at least in
| release mode.
| weinzierl wrote:
| That is not true and very important to understand. The
| optimizations a compiler does by eliding UB stuff are
| often very effective and I think no one in the thread has
| questioned that. It is a fallacy, though, to think that
| these optimizations could be replaced by diagnostics.
| That would be hard in most cases and sometimes
| impossible.
| dale_glass wrote:
| So fix it to the extent possible. Eg, this:
| https://t.co/Z1Ib2fIyu6
|
| Is easily fixable.
|
| A. static Function Do; -- This is a syntax error,
| initialization is mandatory.
|
| B. It's initialized to nullptr, and compiled to jmp 0x0
| layer8 wrote:
| Yes, you can fix it in some cases, but the real errors
| usually happen in complex code where it is much harder or
| impossible to detect at compile time. The examples in TFA
| are very simplified for exposition purposes, they are not
| representative of ease-of-detection.
| dale_glass wrote:
| I don't see how this particular case could ever be hard
| to deal with.
|
| Using uninitialized memory is UB, so initialize every
| byte of memory allocated to NULL. Dereference NULL
| pointers as-is. Done.
|
| I'd be quite happy with a best effort approach. If you
| can't be perfect in every case, at least handle the low
| handling fruit.
| layer8 wrote:
| This approach would also impact UB-free code that
| initializes the memory later. C compilers don't want to
| pessimize such code. The difficulty lies in identifying
| only code with actual UB.
| dale_glass wrote:
| That's perfectly fine with me. If the compiler can prove
| the code is initialized twice, then it's free to
| deduplicate it. Otherwise I'll happily eat the cost and
| preserve more of my sanity when debugging.
| layer8 wrote:
| _You_ 'll happily eat the cost, but the target audience
| of C compilers traditionally doesn't. That's the point
| made by TFA. C prioritizes performance over safety.
| weinzierl wrote:
| I agree with you more than you think.
|
| _" if the standards people started defining every
| possible instance of UB as a fatal error or as an
| implementation defined behavior,"_
|
| We can wish for that, but it does not and never will.
|
| Writing C or C++, first and foremost, means writing code
| against the standard and we have to deal with what the
| standard says.
|
| Of course we are free to give up on the standard and
| target a particular implementation of C or C++ in a
| particular compiler. If you also like to to call that
| writing C or C++ I would not argue with you. It is still
| a very different thing because you give up the primary
| advantage of C and C++ that you can compile basically
| everywhere. My point now is that if you give that up it
| makes no sense to stick with C or C++ in the first place
| and in this day and age.
| dale_glass wrote:
| > My point now is that if you give that up it makes no
| sense to stick with C or C++ in the first place and in
| this day and age.
|
| You wouldn't be giving anything up. The standard defining
| something as UB means you absolutely shouldn't be doing
| that. So a compiler doing something defined in case of UB
| can't harm you in any way, and doesn't deviate from the
| standard, because the standard prescribes no rules in the
| case you're invoking.
| weinzierl wrote:
| With giving up I meant giving up portability by targeting
| a particular implementation of C or C++ in a particular
| compiler. The standard is what it is and discussing a
| hypothetical standard is moot.
| dale_glass wrote:
| You don't give up any portability.
|
| If the standard says that say, dereferencing a NULL
| pointer is UB it means you're not supposed to do that
| ever, on any OS or compiler. A compiler can choose to do
| something sensible like producing a fatal error message
| without any downside, since per standard that's not ever
| supposed to happen anyway.
| weinzierl wrote:
| To not giving up portability _all_ compilers would have
| to do what you propose. They won 't as long as the
| standard doesn't mandate it. So we are back at the point
| where we agree that the standard contains some
| unfortunate things.
|
| Now, standard conformant compilers do exactly what you
| propose, they are just not C or C++ compilers and the
| standard is not the C or C++ standard.
| dale_glass wrote:
| > To not giving up portability all compilers would have
| to do what you propose.
|
| Not in a lot of cases. Eg, let's suppose that GCC defines
| a NULL pointer dereference to act exactly like a normal
| one. That is, the compiler dereferences the pointer, and
| whatever the CPU/OS says is going to happen when you read
| from 0x0, happens.
|
| Meanwhile, Clang continues with behaviors that can say
| lead to a function being entirely replaced with "return
| true".
|
| There's no problem whatsoever with this. You're still not
| supposed to dereference null pointers. It's just that on
| GCC in particular you get a predictable error. And being
| an error you can't really rely on it -- your program
| still crashes and crashes are still undesirable, and
| therefore you will fix whatever leads to that outcome.
| And on Clang maybe you don't crash but the program
| doesn't do what you expect it to, which is still a bug to
| be fixed. You have a bug in both versions which manifests
| in different ways (but that is fine, because UB says
| anything goes so there's no requirement whatsoever for
| both compilers to have the program break identically),
| but it's the same bug that needs the same fix.
|
| After the bug fix, your GCC compiled version doesn't
| crash and runs correctly, and your Clang compiled version
| doesn't crash and runs correctly. The behavior in the end
| is identical.
| coliveira wrote:
| > Writing C or C++, first and foremost, means writing
| code against the standard
|
| This was never true and never will be. There are tons of
| C and C++ code that cannot be compiled in more than a few
| compilers. The standard is the minimum denominator, but
| all compilers have something beyond the standard that is
| used in practice. Just check the Linux code, for example.
|
| > if you give that up it makes no sense to stick with C
| or C++
|
| No, the whole point of UB is that every compiler can do
| what it wants in that situation. C/C++ actively embrace
| differences between compilers, while trying to
| standardize the core meaning of the language.
| weinzierl wrote:
| Linux is an excellent example. There has been a fork that
| made it compile with the Intel compiler and if I remember
| correctly it was moderately faster. Of course it went
| nowhere.
|
| Writing standard conformant code is hard and you do it if
| you have a good reason to do it. A lot of software has.
|
| The Linux kernal hasn't and that is fine. If your project
| also doesn't have that constraint, good for you. 30 years
| ago you would still choose C and a particular compiler
| and you could get things done. Nowadays, why bother? You
| could choose a language that has no UB, compiles on any
| hardware that is reasonably common and is still
| performant.
| Thiez wrote:
| Why complain to the compiler writers? As you say, people want
| the fastest compilers for their language, so compilers will
| prioritize that over other concerns. Users may rant and
| complain, but they won't use a slower compiler.
|
| 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.
| pyrolistical wrote:
| > However, if a company/project writes a compiler that is a
| little slower than the competitor, people with almost always
| complain that it is a bad compiler.
|
| 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
| zwieback wrote:
| Clearly we can have compilers and static analyzers that can catch
| a lot of what's UB in the standards.
|
| 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.
| eatonphil wrote:
| It isn't super obvious as someone interested in C or C++ what
| compiler flags and static analyzers I should turn on.
|
| 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?
| yaantc wrote:
| For static analysis I use CodeChecker, it's a wrapper on top
| of the Clang static analyzer and Clang tidy (linter). Now
| also supports cppcheck, but I disabled it (too many false
| alarms). It's free and open source, and I find it useful.
| Make sure you use it with a version of LLVM/Clang with
| support of Microsoft z3 enabled (it's the case in Debian
| stable, so should be OK in most distros).
|
| 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.
| soulbadguy wrote:
| -Wall -Werror (in cland and in think on GCC) is a good way to
| start. In general, clang is great at warning at UB. Also,
| address-san is a great tool
| dbremner wrote:
| I'm not sure about static analyzers, but here are the clang
| warnings I use for my personal C++20 projects. You will get
| an enormous number of warnings from typical C++ code. I fix
| all of them, but doubt it would make sense to do so in a
| commercial environment.
|
| -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
| zwieback wrote:
| Not that I know of, probably something you'll have to dig
| into based on your use case. I've used Coverity for static
| analysis, which is great but pricey. We have a corporate
| license so no-brainer for me.
| [deleted]
| k4st wrote:
| > It would certainly not hurt performance to emit a compiler
| warning about deleting the if statement testing for signed
| overflow, or about optimizing away the possible null pointer
| dereference in Do().
|
| 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.
| pogopop77 wrote:
| C and C++ are most often used when performance is the highest
| priority. Undefined behavior is basically the standards committee
| allowing the compiler developers maximum flexibility to optimize
| for performance over error checking/handling/reporting. The
| penalty is that errors can become harder to detect.
|
| 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.
| kelnos wrote:
| I think this allowance is a mistake.
|
| 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.
| PaulDavisThe1st wrote:
| The allowance wasn't "tradeoff performance time with
| developer time".
|
| The point of undefined behavior is "if we specify this, we
| cause problems for some of the compile targets for this
| language".
| patmorgan23 wrote:
| Yes, but then complier vendors started abusing UB to
| increase performance and while silently decreasing
| safety/correctness. If the compiler creates a security bug
| by optimizing away a bounds check the programmer explicitly
| put there, that's a problem.
|
| https://thephd.dev/c-undefined-behavior-and-the-
| sledgehammer...
| jerf wrote:
| "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.
| spookie wrote:
| I'm sorry, but the world doesn't really align with these
| ideas, sometimes that is. It's understandable, but at the
| same time it really isn't.
| jerf wrote:
| I assume you're referring to "my codebase is already in
| C/C++"? Which I did acknowledge?
|
| Because otherwise, what the world is increasingly not
| aligning with is using C when you shouldn't be. Security
| isn't getting any less important and C isn't getting any
| better at it.
| dahfizz wrote:
| I'll stop using C when there is a faster alternative. When
| nanoseconds count, there is no competition.
| jerf wrote:
| You don't need one faster. You need one _as_ fast. These
| options generally exist. Rust seems to have crept its way
| right up to "fast as C"; it isn't really a distinct
| event, but https://benchmarksgame-
| team.pages.debian.net/benchmarksgame/... (it tends to
| better on the lower ones so scroll a bit). There are some
| other more exotic options.
|
| C isn't the undisputed speed king any more. It hasn't
| necessarily been "roundly trounced", there isn't enough
| slack in its performance for that most likely, but it is
| not the undisputed speed king. It turns out the corners
| it cuts are just corners being cut; they aren't actually
| necessary for performance, and in some cases they can
| actually inhibit performance. See the well-known aliasing
| issues with C optimizations for an example. In general I
| expect Rust performance advantages to actually get
| _larger_ as the programs scale up in size and Rust
| affords a style that involves less copying just to be
| safe; benchmarks may actually undersell Rust 's
| advantages in real code on that front. I actually
| wouldn't be surprised that Rust is in practice a
| straight-up faster language than C on non-trivial code
| bases being developed in normal ways; it is unfortunately
| a very hard assertion to test because pretty much by
| definition I'm talking about things much larger than a
| "benchmark".
| optymizer wrote:
| This reasoning is why software keeps getting slower and
| more bloated, build times increase, and latency goes up
| despite having orders of magnitude more compute power.
| jerf wrote:
| If whatever language you're thinking of does that, it
| isn't one of the ones I'm talking about. I sure as heck
| aren't talking about Python here. Think Rust, D, Nim, in
| general the things floating along at the top of the
| benchmarks (that's not a complete list either).
| jeremyloy_wt wrote:
| > It appears the author is a Go advocate
|
| A bit of an understatement. The author is the current Golang
| project lead and a member since it's inception
| pjmlp wrote:
| Nowadays, in 1980's....
|
| "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
| marcosdumay wrote:
| Hum... We have to move further than this citation. The 1980s
| C was much more secure than our current one.
|
| 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.
| pjmlp wrote:
| Yeah, because the only way to achieve performance in 1980's
| C on 16 bit platforms was to litter it with inline
| Assembly, thus UB based optimisation was born to win all
| those SPEC benchmarks in computer magazines.
| marcosdumay wrote:
| Funny thing in that the same thing stopping people to
| write those crazy1 optimizers at the 1980s was exactly
| the lack of capacity of the computers to run them.
|
| What means that they appeared exactly at the time the
| need for them became niche. And yet everybody adopted
| them due to the strong voodoo bias we all have at
| computer-related tasks.
|
| 1 - They _are_ crazy. They believe the code has no UB
| even though they can prove it has.
| titzer wrote:
| A great quote. She's absolutely right. C has absolutely
| polluted people's understanding of what compiler
| optimizations _should_ be. Compilers should make a program go
| faster, invisibly. C makes optimization _everyone 's_ problem
| because the language is so absolutely terrible at defining
| its own semantics and catching program errors.
| btilly wrote:
| Given who invented it, Go can be thought of as, "What C might
| have been if we could have done it."
|
| Go really is in many ways more similar to early C in spirit
| than modern C is.
| akshayshah wrote:
| Expanding this for those not familiar with Go's history: Ken
| Thompson, formerly of Bell Labs and co-creator of B and Unix,
| was deeply involved in Go's early days. Rob Pike, also ex-
| Bell Labs, was also one of Go's principal designers.
|
| 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.
| anthk wrote:
| Go it's modelled after plan9'C [1-9]c = go cross compiling,
| and Limbo so it has a lot of sense. Both come from the same
| people after all. Unix->Unix8 -> Plan9 -> Go.
| mxmlnkn wrote:
| The undefined behavior I struggle with keeps me from better
| performance though. I have something like [(uint32_t value) >>
| (32 - nbits)] & (lowest nbits set). For the case of nbits=0, I
| would expect it to always return 0, even if the right shift of
| a 32-bit value by 32 bits is undefined behavior, then bit-wise
| and with 0 should make it always result in 0. But I cannot
| leave it that way because the compiler thinks that undefined
| behavior may not happen and might optimize out everything.
| titzer wrote:
| Exactly. The irony in all of this is that C is _not_ a
| portable assembler. It 'd be better if it _were_ [1]!
|
| 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.
| zzo38computer wrote:
| I think that the undefinde behaviour should be partially
| specified. In the case you describe, it should require that
| it must do one of the following:
|
| 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.
| mxmlnkn wrote:
| I wanted to reply that your point 3 should already be
| possible with C++ constexpr functions because it doesn't
| allow undefined behavior. But I it seems I was wrong about
| that or maybe I'm doing it wrong:
| [[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
| gpderetta wrote:
| Unfortunately constexpr doesn't imply constant
| evaluation. Your function can still potentially be
| executed at runtime.
|
| If you use the result in an expression that requires a
| constant (an array bound, a non-type template parameter,
| a static_assert, or, in c++20, to initialize a constinit
| variable), then that will force constant evaluation and
| you'll see the error.
|
| Having said that, compilers have bugs (or simply not
| fully implemented features), so it is certainly possible
| that both GCC and clang will fail to correctly catch
| constant time evaluation UB in some circumstances.
| mxmlnkn wrote:
| Ah thanks, I was not aware that these compile-time checks
| are only done when it is evaluated in a compile-time
| evaluating context.
|
| To add to your list, using C++20 consteval instead of
| constexpr also triggers the error.
| TwentyPosts wrote:
| Eh. The existence of Rust (and Zig, to a lesser extent) prove
| that you can, in fact, have both: Highest performance and safe,
| properly error checked code without any sort of UB.
|
| 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.
| alphanullmeric wrote:
| You can do any rust optimization yourself in C++ (ie.
| aliasing assumptions), whereas rust makes the other way
| around very difficult, often forcing you to use multiple
| layers of indirection where c++ would allow a raw pointer, or
| forcing an unwrap on something you know is infallible when
| exceptions would add no overhead, etc. Rust programmers want
| people to believe that whatever appeases the supposedly zero
| cost borrow checker is the fastest thing to do even though it
| has proven to be wrong time and time again. I can't tell you
| how many times I've seen r/rust pull the "well why do you
| want to do that" or "are you sure it even matters" card every
| time rust doesn't allow you to write optimized code.
| thedracle wrote:
| > when exceptions would add no overhead
|
| 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.
| tialaramex wrote:
| The argument for the Exception price is that we told you
| Exceptions were for Exceptional situations. This argument
| feels reasonable until you see it in context as a library
| author.
|
| Suppose I'm writing a Clown Redemption library. It's
| possible to Dingle a Clown during redemption, but if the
| Clown has already dingled that's a problem so... should I
| raise an exception? Alice thinks obviously I should raise
| an exception for that, she uses a lot of Clowns, the
| Clown Redemption library helps her deliver high quality
| Clown software and it's very fast, she has never dingled
| a Clown and she never plans to, the use of exceptions
| suits Alice well because she can completely ignore the
| problem.
|
| Unfortunately Bob's software handles primarily dingling
| Clowns, for Bob it's unacceptable to eat an exception
| every single damn time one of the Clowns has already been
| dingled, he _demands_ an API in which there 's just a
| return value from the dingling function which tells you
| if this clown was already dingled, so he can handle that
| appropriately - an exception is not OK because it's
| expensive.
|
| Alice and Bob disagree about how "exceptional" the
| situation is, and I'm caught in the middle, but _I_ have
| to choose whether to use exceptions. I can 't possibly
| win here.
| alphanullmeric wrote:
| Like I said, this argument doesn't work because you can
| use options in c++ but you can't use exceptions in rust.
| So when there's an occasion where you want to avoid the
| overhead of an option or result in rust - well too bad.
| kllrnohj wrote:
| Exceptions cost performance when thrown whereas return
| values _always_ cost performance.
|
| If all you care about is outright performance, having the
| option for exceptions is easily the superior choice. The
| binary does get bigger but those are cold pages so who
| cares (since exceptions are exceptional, right?)
| celeritascelery wrote:
| > You can do any rust optimization yourself in C++ (ie.
| aliasing assumptions)
|
| 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.
| LegionMammal978 wrote:
| While standard C++ has no equivalent of a noalias
| annotation, it's wrong to say that it has no aliasing
| requirements. To access an object behind a pointer (or a
| glvalue in general), the type of the pointer must be
| (with a few exceptions) similar to the type of the
| pointee in memory, which is generally the object
| previously initialized at that pointer's address. This
| enables type-based alias analysis (TBAA) in the compiler,
| where if a pointer is accessed as one type, and another
| pointer is accessed as a dissimilar type, then the
| compiler can assume that the pointers don't alias.
|
| Meanwhile, Rust ditches TBAA entirely, retaining only
| initialization state and pointer provenance in its memory
| model. It uses its noalias-based model to make up for the
| lack of type-based rules. I'd say that this is the right
| call from the user's standpoint, but it can definitely be
| seen as a tradeoff rather than an unqualified gain.
| cortesoft wrote:
| Isn't that the point of unsafe blocks in rust? So you can
| write optimized code when you need to and the rust borrow
| checker won't let you?
| gmueckl wrote:
| Unsafe blocks are subject to the same borrow checking
| that the rest of the language is.
| tialaramex wrote:
| That is correct. However, raw pointers are not borrow
| checked, in _safe_ Rust they 're largely useless, but in
| unsafe Rust you can use raw pointers if that's what you
| need to do to get stuff done.
|
| As an example inside a String is just a Vec<u8> and
| inside the Vec<u8> is a RawVec<u8> and _that_ is just a
| pointer, either to nothing in particular or to the bytes
| inside the String if the String has allocated space for
| one or more bytes - plus a size and a capacity.
| overgard wrote:
| > C++ makes it laughably easy to write incorrect code
|
| 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.*
| adamdegas wrote:
| The Computer Language Benchmarks Game has C++ outperforming
| Rust by around 10% for most benchmarks. Binary trees is 13%
| faster in C++, and it's not the best C++ binary tree
| implementation I've seen. k-nucleotide is 32% faster in C++.
| Rust wins on a few benchmarks like regex-redux, which is a
| pointless benchmark as they're both just benchmarking the
| PCRE2 C library, so it's really a C benchmark.
|
| > 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
| estebank wrote:
| > Rust wins on a few benchmarks like regex-redux, which is
| a pointless benchmark as they're both just benchmarking the
| PCRE2 C library, so it's really a C benchmark.
|
| 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/...
| thedracle wrote:
| I mean, it's outperforming C as well in that particular
| benchmark.
|
| 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...
| tylerhou wrote:
| The author, Russ Cox, was one of the inventors of Go.
| wheelerof4te wrote:
| 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.
| oude wrote:
| I guess we are rediscovering the 40 year old wisdom:
| https://www.dreamsongs.com/RiseOfWorseIsBetter.html
| kelnos wrote:
| On one hand this is sort of "duh" (none of the examples in the
| article were surprises to me). But I think it's very useful to
| phrase it this way. So many people seem to shrug off the dangers
| in using C (C++ of course has its issues, though I'd argue it's
| easier these days to use C++ correctly), especially for security-
| critical code. It may be a helpful argument to point out that C &
| C++ were not designed with the correctness of programs in mind.
| dale_glass wrote:
| I think DJB at some point expressed the desire for a boring
| compiler with an absolute minimum of UB, and I concur.
|
| 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.
| coliveira wrote:
| Just turn on all warnings in your C++ compiler, and make
| warnings the same as error. For example, uninitialized
| variables are easy to catch as a warning and then turned into
| an error. More sophisticated compilers can warn about the other
| issues too.
| UncleMeat wrote:
| This handles a few cases but nowhere near all of them. Null
| pointer dereference, use-after-free, data races, and much
| much more are all global properties with no hope of the
| compiler protecting you.
| coliveira wrote:
| We're only considering UB conditions here, not errors in
| general that may be impossible to detect. Every UB
| condition can be detected by the compiler because, after
| all, the compiler needs to check for UB to generate code.
| All it takes is for the compiler to generate warning/errors
| when this occurs. If this is not done by your compiler, ask
| them to add this feature instead of just complaining about
| UB in general.
| UncleMeat wrote:
| All of the things I described are UB.
| TillE wrote:
| Stuff like this sounds great but if you dig into the details of
| actually trying to implement it, it's not at all simple, and it
| would inevitably slow down the already-terrible compilation
| speed of C++.
|
| 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.
| kelnos wrote:
| Not really, at least for two of the three examples mentioned.
|
| 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.
| agwa wrote:
| > _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._
|
| 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.
| lelanthran wrote:
| > Failing compilation on an uninitialized variable is easy.
|
| 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.
| kelnos wrote:
| > _Arithmetic overflow? Two 's complement_
|
| 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".
| toast0 wrote:
| This is possible, but unless processor accelerated comes at a
| great cost. You'd need a branch after every math, unless the
| compiler could prove the math wouldn't overflow.
|
| 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.
| kccqzy wrote:
| > You'd need a branch after every math
|
| A predictable branch is basically free. In your case such a
| branch is almost never taken.
| celeritascelery wrote:
| "basically free" is very different then actually "free".
| Adding a ton of extra branches comes at a cost. Even
| though it is cheap it still takes an instructions that
| could be used for something else. You fill your branch
| prediction target cache must faster, ejecting branches
| you actually care about. It also makes it harder for the
| compiler to move code around and harder for the
| prefetcher to break data dependencies. This all adds up
| to non-trival overhead. You can tell your C compiler to
| always check arithmetic, but most don't because of the
| cost.
| [deleted]
| pdw wrote:
| Trapping on integer overflow was completely standard before
| C came along. Fortran and Pascal compilers did it. (Lisp
| compilers transparently switched to bignums :)
| UncleMeat wrote:
| This already mostly exists. It means building with
| asan/msan/tsan/ubsan enabled. And... you'll slow your code down
| by several factors.
| adrian_b wrote:
| With gcc you can use e.g. "-fsanitize=undefined,address
| -fsanitize-undefined-trap-on-error".
|
| 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.
| eatonphil wrote:
| Maybe I'm completely misunderstanding things, but this article
| mentions arithmetic overflow as being a problem in C and C++ but
| isn't this exactly a problem in Go as well? There's no checked
| arithmetic in Go, right?
|
| 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.
| nteon wrote:
| In Go, overflow is not checked, but it is well-defined in the
| spec including `programs may rely on "wrap around"`:
| https://go.dev/ref/spec#Integer_overflow
| beltsazar wrote:
| > To some extent, all languages do this: there is almost always a
| tradeoff between performance and slower, safer implementations.
|
| 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.
| cmovq wrote:
| Rust does tradeoff performance for safety. Example: integer
| division [1].
|
| [1]: https://godbolt.org/z/71sxnfff5
| jakobnissen wrote:
| Not always all of the performance. Rust encourages array access
| with superfluous bounds checks, unnecessary UTF8 validation,
| and uses tagged sum types over the more memory efficient raw
| union types.
| _dain_ wrote:
| one day, a bug like this will take down an airliner or a power
| grid and kill thousands of people. and there will be a
| congressional hearing about it. and some ageing compiler
| engineers will have to explain to a panel of irate and
| disbelieving senators the concept of UB. they will have to
| explain that, while computers _can_ add two numbers correctly, we
| choose to make them not do that. for performance.
| renox wrote:
| Note that in Zig an _unsigned overflow_ is also an UB in release
| unchecked mode. Don 't like it? Don't use unchecked mode or use
| wrapping operators..
| muldvarp wrote:
| > In effect, Clang has noticed the uninitialized variable and
| chosen not to report the error to the user but instead to pretend
| i is always initialized above 10, making the loop disappear.
|
| 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.
| mpweiher wrote:
| 1. The people creating the C standard were adamant that just
| following the standard was not sufficient to produce a "fit-
| for-purpose" compiler. This was intentional.
|
| 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?
| pwdisswordfishc wrote:
| Citation needed. Which people? What did they actually say?
| What was the text that supposedly forbade this interpretation
| of UB? Please don't tell me this is again that tired wankery
| over "permissible" versus "possible". As if the choice of
| synonym mattered.
| mpweiher wrote:
| "Permissible" and "possible" are not synonyms.
| 3836293648 wrote:
| It's a rather infamous change between C89 and C99 where the
| description of UB was changed from basically don't do this
| to please do this and compilers can do whatever they want
| if you do
| _kst_ wrote:
| The definition of "undefined behavior" did not change in
| the way you describe between C89/C90 and C99. In both
| editions, one possible consequence of undefined behavior
| is "ignoring the situation completely with unpredictable
| results" -- i.e., compilers can do whatever they want.
|
| 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.)
| mpweiher wrote:
| "Permissible" [?] "possible"
| _kst_ wrote:
| True -- but how does that affect the semantics?
|
| Both definitions say that undefined behavior can be dealt
| with by "ignoring the situation completely with
| unpredictable results". There are no restrictions on what
| can happen.
|
| (The standard joke is that it can make demons fly out of
| your nose. Of course that's not physically possible, but
| it would not violate the standard.)
| moefh wrote:
| > The C standard actually does have text giving a list of
| acceptable behaviours for a compiler
|
| 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_.
| User23 wrote:
| I think you're misinterpreting. Maybe it's clearer if we
| elide the relative subclause: 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.
| iso8859-1 wrote:
| If there are no requirements on what it's doing, how is
| that not a license to do whatever?
|
| 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.
| mpweiher wrote:
| " _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). "
|
| 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.
| mike_hock wrote:
| Which is logically equivalent to imposing no
| requirements. "ignoring the situation completely with
| unpredictable results" does not meaningfully constrain
| the possible behaviors.
| mpweiher wrote:
| That turns out not to be the case.
|
| "Ignoring" is not "taking action based on"
|
| Ignoring is, for example, ignoring the fact that an array
| access is out of bounds and performing the array access.
|
| Ignoring is _not_ noticing that there is undefined
| behavior and removing the access and the entire loop that
| contains the access. Or a safety check.
| _kst_ wrote:
| C is not a "portable assembler".
|
| An assembly language program specifies a series of CPU
| instructions.
|
| A C program specifies runtime behavior.
|
| That's a _huge_ semantic difference.
| mpweiher wrote:
| "Committee did not want to force programmers into writing
| portably, to preclude the use of C as a "high-level
| assembler:"
|
| https://www.open-std.org/JTC1/SC22/WG14/www/docs/n897.pdf
|
| p10, line 39
|
| "C code can be portable. "
|
| line 30
| petergeoghegan wrote:
| > No. This is what I call the "portable
| assembler"-understanding of undefined behavior and it is
| entirely false.
|
| "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.
| qsort wrote:
| > But the tendency of compiler people to refuse to take the
| idea seriously, even for a second, just seems odd.
|
| 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.
| mpweiher wrote:
| That's an interesting opinion.
|
| But it has very little to do with the C programming
| language.
| nickelpro wrote:
| > As a matter of fact the C standard bends over backwards
| in the attempt of not specifying a memory model.
|
| I mean, C explicitly specifies a memory model and has since
| C11
| petergeoghegan wrote:
| > It's not taken seriously because it shouldn't be taken
| seriously
|
| I really don't know what you're arguing against. I never
| questioned the general usefulness of an abstract machine. I
| merely pointed out that a large amount of important C code
| exists that is in tension with the idea that of an all
| important abstract machine. This is an empirical fact. Is
| it not?
|
| You are free to interpret this body of C code as "not true
| ISO C", I suppose. Kind of like how the C standard is free
| to remove integer overflow checks in the presence of
| undefined behavior.
| grotorea wrote:
| I wonder what's the best solution here then. A different
| language that actually is portable assembly, or has less
| undefined behaviour or simpler semantics (e.g RIIR), or
| making -O0 behave as portable assembly?
| _kst_ wrote:
| Step 1: Define just what "portable assembly" actually
| means.
|
| An assembly program specifies a sequence of CPU
| instructions. You can't do that in a higher-level
| language.
|
| Perhaps you could define a C-like language with a more
| straightforward abstract machine. What would such a
| language say about the behavior of integer overflow, or
| dereferencing a null pointer, or writing outside the
| bounds of an array object?
|
| You could resolve some of those things by adding
| mandatory run-time checks, but then you have a language
| that's at a higher level than C.
| dale_glass wrote:
| > Perhaps you could define a C-like language with a more
| straightforward abstract machine. What would such a
| language say about the behavior of integer overflow
|
| Whatever the CPU does. Eg, on x86, twos complement.
|
| > or dereferencing a null pointer
|
| Whatever the CPU does. Eg, on X86/Linux in userspace, it
| segfaults 100% predictably.
|
| > or writing outside the bounds of an array object?
|
| Whatever the CPU does. Eg, on X86/Linux, write to
| whatever is next in memory, or segfault.
|
| > You could resolve some of those things by adding
| mandatory run-time checks, but then you have a language
| that's at a higher level than C.
|
| No checks needed. Since we're talking about "portable
| assembly", we're talking about translating to assembly in
| the most direct manner possible. So dereferencing a NULL
| pointer literally reads from address 0x0.
| dooglius wrote:
| > What would such a language say about the behavior of
| integer overflow
|
| Two's complement (i.e. the result which is equivalent to
| the mathematical answer modulo 2^{width})
|
| > dereferencing a null pointer
|
| A load/store instruction to address zero.
|
| > writing outside the bounds of an array object
|
| A store instruction to the corresponding address. It's
| possible this could overwrite something important on the
| stack like a return address, in which case the compiler
| doesn't have to work around this (though if the compiler
| detects this statically, it should complain rather than
| treating it as unreachable)
| badsectoracula wrote:
| > I agree that undefined behavior is a silly concept but that's
| the fault of the standard, not of compilers.
|
| While undefined behavior is the standard's fault, its
| interpretation is up to the compiler.
| overgard wrote:
| > 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_.
|
| 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.
| Vvector wrote:
| Doesn't -Wall give you the error you want?
| robinsonb5 wrote:
| Not always - in some cases you need -Wextra or even
| -Weverything to get a warning. [Edit: was thinking of GCC
| here, and yesterday's similar thread - clang may be more
| forthcoming with warnings, I don't know.]
| anon946 wrote:
| Is all UB silly? E.g., wouldn't fully defining what happens
| when one goes beyond the end of an array impose a non-trivial
| performance hit for at least some code?
| grumpyprole wrote:
| > Also, compilers don't reason about code the same way humans
| do.
|
| 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.
| jonas21 wrote:
| That's why the author prefaced the sentence with " _In effect_
| ". The effect in this case is the same as if the compiler had
| pretended i was initialized to a high value - the loop is
| omitted.
| plorkyeran wrote:
| It's still fundamentally a misunderstanding of what the
| compiler is doing, and thinking about it that way is just
| going out of your way to cause confusion. The compiler saw a
| use of an uninitialized value, concluded that it must not be
| reachable as that's the only way for it to be a legal C
| program, and then deleted the dead code. You can make up all
| sorts of other behaviors that the compiler could have done
| which would have had the same result in a specific scenario,
| but why would you?
| stanleydrew wrote:
| I don't think the author misunderstands what the compiler
| is doing, just saying that it's probably unexpected
| behavior from the perspective of the program's author.
| afiori wrote:
| I don't think that the compiler approach is try and fix the
| code
|
| > it must not be reachable as that's the only way for it to
| be a legal C program, and then deleted the dead code.
|
| Rather it would make more sense if the approach was more
| like "this branch is UB so I can whatever is most
| convenient in terms of optimization" in this case it was
| merging the 2 branches but discarding the code for the UB
| branch.
|
| But from a behavioral point of view all these formulations
| describe the same result
| plorkyeran wrote:
| The compiler isn't trying to "fix" anything and I'm not
| sure where that idea came from. The core concept is that
| the compiler _assumes_ the program is valid and optimizes
| based on that assumption. If UB would occur if certain
| values are passed as an argument to a function, the
| compiler assumes that those values won 't be passed. If
| UB occurs unconditionally in a function, the compiler
| assumes that the function won't be called.
| afiori wrote:
| > The core concept is that the compiler assumes the
| program is valid
|
| I was referring to this.
|
| IIRC the spec language is that the compiler is free to
| assume that UB never happens, but from an operational
| perspective I believe that the compiler simply stop
| caring about those cases.
|
| by this I mean that for code like
| #include <stdio.h> int main() {
| printf("Hello, "); 0/0; *(NULL);
| printf("World!\n"); return 0; }
|
| most compiler will just pretend that _bad lines_ did not
| exists
|
| https://godbolt.org/#z:OYLghAFBqd5TKALEBjA9gEwKYFFMCWALug
| E4A...
| mrighele wrote:
| > Clang is free to produce whatever output it wants because the
| behavior of that code is literally _undefined_.
|
| > 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.
| weinzierl wrote:
| Exactly, and in addition to that, making the loop disappear is
| not only good for performance but from a compiler perspective
| _much_ easier than producing a helpful error message that
| explains the situation well.
| User23 wrote:
| > Clang is free to produce whatever output it wants because the
| behavior of that code is literally _undefined_
|
| 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.
| spookie wrote:
| > 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.
|
| 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.
| somenameforme wrote:
| IMO it all comes down to the, 'Premature optimization is the
| root of all evil.' That saying was, at its best, not great.
| But it seems at some point down the road the 'premature' part
| of that sentence was lost to history and it became something
| much worse. It goes some way towards explaining why we now
| need machines that would be considered supercomputers, just a
| couple of decades prior, to run a text editor.
|
| 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.
| mike_hock wrote:
| It's a dogma pendulum like "gotos are evil," "YAGNI,"
| object-oriented design (is good/bad), etc.
|
| A pattern or anti-pattern gets identified and lessons drawn
| from it. Then people take the lesson as dogma and drive it
| to absurd extremes. Naturally this doesn't have the desired
| effect, and then the baby gets thrown out with the
| bathwater, people do the exact opposite and take _that_ to
| absurd extremes.
|
| Caching potentially expensive (relatively speaking)
| operations isn't "premature optimization," it's a design
| principle. It's much harder to retrofit something like this
| into a project after the fact than having and using it from
| the start.
|
| "Premature optimization" is applying complicated micro-
| optimizations early on that won't survive any moderate code
| change.
| 6D794163636F756 wrote:
| It depends on the compiler really. C is nice is that you can
| choose a compiler fit for your purpose but it makes my next
| example harder to I'm going to discuss go. I was doing
| leetcode recently and one of the problems was to combine some
| set of sorted linked arrays into one bigger array. There are
| a few ways to do this but I ran two of them to see which was
| better.
|
| 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.
| almostnormal wrote:
| Not necessarily just a matter of details of the assembly,
| but also of cache utilization.
___________________________________________________________________
(page generated 2023-08-18 23:01 UTC)