Hacker Newsnew | past | comments | ask | show | jobs | submit | jmillikin's commentslogin

The https://github.com/blackjetrock/ghidra-6303 repository your post links to (containing a SLEIGH spec for the HD6303) is no longer available, did you happen to save a local clone that could be re-uploaded somewhere?


Thank you very much for pointing this out! Fortunately I still have the code locally. I'll try to raise another PR to get 6303 support into Ghidra.



Thank you for finding this! Depili does great work! In another comment I mentioned that I've been working on the Casio CZ-101, which uses the NEC μPD7810 processor. Depili created a processor spec for the μCOM-87 architecture, which I've continued working on in this PR: https://github.com/NationalSecurityAgency/ghidra/pull/7930


There's at least one proprietary platform that supports Git built by via a vendor-provided C compiler, but for which no public documentation exists and therefore no LLVM support is possible.

Ctrl+F for "NonStop" in https://lwn.net/Articles/998115/


Shouldn't these platforms work on getting Rust to support it rather than have our tools limited by what they can consume? https://github.com/Rust-GCC/gccrs


A maintainer for that specific platform was more into the line of thinking that Git should bend over backwards to support them because "loss of support could have societal impact [...] Leaving debit or credit card authorizers without a supported git would be, let's say, "bad"."

To me it looks like big corps enjoying the idea of having free service so they can avoid maintaining their own stuff, and trying the "too big to fail" fiddle on open source maintainers, with little effect.


It's additionally ridiculous because git is a code management tool. Maybe they are using it for something much more wild than that (why?) but I assume this is mostly just a complaint that they can't do `git pull` from their wonky architecture that they are building on. They could literally have a network mount and externally manage the git if they still need it.

It's not like older versions of git won't work perfectly fine. Git has great backwards compatibility. And if there is a break, seems like a good opportunity for them to fork and fix the break.

And lets be perfectly clear. These are very often systems built on top of a mountain of open source software. These companies will even have custom patched tools like gcc that they aren't willing to upstream because some manager decided they couldn't just give away the code they paid an engineer to write. I may feel bad for the situation it puts the engineers in, I feel absolutely no remorse for the companies because their greed put them in these situations in the first place.


> Leaving debit or credit card authorizers without a supported git would be, let's say, "bad".

Oh no, if only these massive companies that print money could do something as unthinkable as pay for a support contract!


Yes. It benefits them to have ubiquitous tools supported on their system. The vendors should put in the work to make that possible.

I don’t maintain any tools as popular as git or you’d know me by name, but darned if I’m going to put in more than about 2 minutes per year supporting non-Unix.

(This said as someone who was once paid to improve Ansible’s AIX support for an employer. Life’s too short to do that nonsense for free.)


As you're someone very familiar with Ansible, what are your thoughts on it in regards to IBM's imminent complete absorption of RedHat? I can't imagine Ansible, or any other RedHat product, doing well with that.


I wouldn’t say I’m very familiar. I don’t use it extensively anymore, and not at all at work. But in general, I can’t imagine a way in which IBM’s own corporate culture could contribute positively to any FOSS projects if they removed the RedHat veneer. Not saying it’s impossible, just that my imagination is more limited than the idea requires.


IBM has been, and still is, a big contributor to a bunch of Eclipse projects, as their own tools build on those. The people there were both really skilled, friendly and professional. Different divisions and departments can have huge cultural differences and priorities, obviously, but “IBM” doesn’t automatically mean bad for OSS projects.


I'm sure some of RedHat stuff will end up in the Apache Foundation once IBM realizes it has no interest in them.


There isn't even a Nonstop port of GCC yet. Today, Nonstop is big-endian x86-64, so tacking this onto the existing backend is going to be interesting.


That platform doesn’t support GCC either.


Isn’t that’s what’s happening? The post says they’re moving forward.


[flagged]


On the other hand: why should the entire open-source world screech to a halt just because some new development is incompatible with the ecosystem of a proprietary niche system developed by a billion-dollar freeloader?

HPE NonStop doesn't need to do anything with Rust, and nobody is forcing them to. They have voluntarily chosen to use an obscure proprietary toolchain instead of contributing to GCC or LLVM like everyone else: they could have gotten Rust support for free, but they believed staying proprietary was more important.

Then they chose to make a third-party project (Git) a crucial part of that ecosystem, without contributing time and effort into maintaining it. It's open source, so this is perfectly fine to do. On the other hand, it also means they don't get a say in how the project is developed, and what direction it will take in the future. But hey, they believed saving a few bucks was more important.

And now it has blown up in their face, and they are trying to control the direction the third-party project is heading by playing the "mission-critical infrastructure" card and claiming that the needs of their handful of users is more important than the millions of non-HPE users.

Right now there are three options available to HPE NonStop users:

1. Fork git. Don't like the direction it is heading? Then just do it yourself. Cheapest option short-term, but it of course requires investing serious developer effort long-term to stay up-to-date, rather than just sending the occasional patch upstream.

2. Port GCC / LLVM. That's usually the direction obscure platforms go. You bite the bullet once, but get to reap the benefits afterwards. From the perspective of the open-source community, if your platform doesn't have GCC support it might as well not exist. If you want to keep freeloading off of it, it's best to stop fighting this part. However, it requires investing developer effort - especially when you want to maintain a proprietary fork due to Business Reasons rather than upstreaming your changes like everyone else.

3. Write your own proprietary snowflake Rust compiler. You get to keep full control, but it'll require a significant developer effort. And you have to "muck around" with Rust, of course.

HPE NonStop and its ecosystem can do whatever it wants, but it doesn't get to make demands just because their myopic short-term business vision suddenly leaves them having to spend effort on maintaining it. This time it is caused by Git adopting Rust, but it will happen again. Next week it'll be something like libxml or openssl or ssh or who-knows-what. Either accept that breakage is inevitable when depending on third-party components, or invest time into staying compatible with the ecosystem.


At this point maybe it's time to let them solve the problem they've created for themselves by insisting on a closed C compiler in 2025.


[flagged]


>> insisting on a closed C compiler in 2025.

> Everything should use one compiler, one run-time and one package manager.

If you think that calling out closed C compilers is somehow an argument for a single toolchain for all things, I doubt there's anything I can do to help educate you about why this isn't the case. If you do understand and are choosing to purposely misinterpret what I said, there are a lot of much stronger arguments you could make to support your point than that.

Even ignoring all of that, there's a much larger point that you've kind of glossed over here by:

> The shitheads who insist on using alternative compilers and platforms don't deserve tools

There's frequently discussion around the the expectations between open source project maintainers and users, and in the same way that users are under no obligation to provide compensation for projects they use, projects don't have any obligations to provide support indefinitely for any arbitrary set of circumstances, even if they happen to for a while. Maintainers sometimes will make decisions weighing tradeoffs between supporting a minority of users or making a technical change they feel will help them maintain the project better in the long-term differently than the users will. It's totally valid to criticize those decisions on technical grounds, but it's worth recognizing that these types of choices are inevitable, and there's nothing specific about C or Rust that will change that in the long run. Even with a single programming language within a single platform, the choice of what features to implement or not implement could make or break whether a tool works for someone's specific use case. At the end of the day, there's a finite amount of work people spend on a given project, and there needs to be a decision about what to spend it on.


For various libs, you provide a way to build without it. If it's not auto-detected, or explicitly disabled via the configure command line, then don't try to use it. Then whatever depends on it just doesn't work. If for some insane reason git integrates XML and uses libxml for some feature, let it build without the feature for someone who doesn't want to provide libxml.

> At the end of the day, there's a finite amount of work people spend on a given project

Integrating Rust shows you have too much time on your hands; the people who are affected by that, not necessarily so.


> Integrating Rust shows you have too much time on your hands; the people who are affected by that, not necessarily so.

As cited elsewhere in the this thread, the person making this proposal on the mailing list has been involved in significant contributions to git in the past, so I'd be inclined to trust their judgment about whether it's a worthwhile use of their time in the absence of evidence to the contrary. If you have something that would indicate this proposal was made in bad faith, I'd certainly be interested to see it, but otherwise, I don't see how you can make this claim other than as your own subjective opinion. That's fine, but I can't say I'm shocked that the people actually making the decisions on how to maintain git don't find it convincing.


Weighted by user count for a developer tool like Git, Rust is a more portable language than the combination of C and bash currently in use.


> There's at least one proprietary platform that supports Git built by via a vendor-provided C compiler, but for which no public documentation exists and therefore no LLVM support is possible.

That's fine. The only impact is that they won't be able to use the latest and greatest release of Git.

Once those platforms work on their support for Rust they will be able to jump back to the latest and greatest.


It's sad to see people be so nonchalant about potentially killing off smaller platforms like this. As more barriers to entry are added, competition is going to decrease, and the software ecosystem is going to keep getting worse. First you need a lib C, now you need lib C and Rust, ...

But no doubt it's a great way for the big companies funding Rust development to undermine smaller players...


It's kind of funny to see f-ing HPE with 60k employees somehow being labeled as the poor underdog that should be supported by the open-source community for free and can't be expected to take care of software running on their premium hardware for banks etc by themselves.


I think you misread my comment because I didn't say anything like that.

In any case HPE may have 60k employees but they're still working to create a smaller platform.

It actually demonstrates the point I was making. If a company with 60k employees can't keep up then what chance do startups and smaller companies have?


> If a company with 60k employees can't keep up then what chance do startups and smaller companies have?

They build on open source infrastructure like LLVM, which a smaller company will probably be doing anyway.


Sure, but let's not pretend that doesn't kill diversity and entrench a few big players.


The alternative is killing diversity of programming languages, so it's hard to win either way.


HP made nearly $60b last year. They can fund the development of the tools they need for their 50 year old system that apparently powers lots of financial institutions. It's absurd to blame volunteer developers for not wanting to bend over backwards, just to ensure these institutions have the absolute latest git release, which they certainly do not need.


Oh they absolutely can, they just choose not to. To just make some tools work again there's also many slightly odd workarounds one could choose over porting the Rust compiler.


> It's sad to see people be so nonchalant about potentially killing off smaller platforms like this.

Your comment is needlessly dramatic. The only hypothetical impact this has is that whoever uses these platforms won't have upgrades until they do something about it, and the latest and greatest releases will only run if the companies behind these platforms invests in their maintenance.

This is not a good enough reason to prevent the whole world from benefiting from better tooling. This is not a lowest common denominator thing. Those platforms went out of their way to lag in interpretability, and this is the natural consequence of these decisions.


Maybe they can resurrect the C backend for LLVM and run that through their proprietary compilers?

It's probably not straightforward but the users of NonStop hardware have a lot of money so I'm sure they could find a way.


Rust has an experimental C backend of its own as part of rustc_codegen_clr https://github.com/FractalFir/rustc_codegen_clr . Would probably work better than trying to transpile C from general LLVM IR.


Some people have demonstrated portability using the WASM target, translating that to C89 via w2c2, and then compiling _that_ for the final target.


Given that the maintainer previously said they had tried to pay to get GCC and LLVM ported multiple times, all of which failed, money doesn’t seem to have helped.


Surely the question is how much they tried to pay? Clearly the answer is "not enough".


I mean at one point I had LLVM targeting Xbox 360, PS3, and Wii so I'm sure it's possible, it just needs some imagination and elbow grease :)


Why should free software projects bend over backwards to support obscure proprietary platforms? Sounds absurd to me


Won't someome think of the financial sector


Reminds me of a conversation about TLS and how a certain bank wanted to insert a backdoor into all of TLS for their convenience.


Sucks to be that platform?

Seriously, I guess they just have to live without git if they're not willing to take on support for its tool chain. Nobody cares about NonStop but the very small number of people who use it... who are, by the way, very well capable of paying for it.


I strongly agree. I read some of the counter arguments, like this will make it too hard for NonStop devs to use git, and maybe make them not use it at all. Those don’t resonate with me at all. So what? What value does them using git provide to the git developers? I couldn’t care less if NonStop devs can use my own software at all. And since they’re exclusively at giant, well-financed corporations, they can crack open that wallet and pay someone to do the hard work if it means than much to them.


"You have to backport security fixes for your own tiny platform because your build environment doesn't support our codebase or make your build environment support our codebase" seems like a 100% reasonable stance to me


> your build environment doesn't support our codebase

If that is due to the build environment deviating from the standard, then I agree with you. However, when its due to the codebase deviating from the standard, then why blame the build environment developers for expecting codebases to adhere to standards. That's the whole point of standards.


Is there a standard that all software must be developed in ANSI C that I missed, or something? The git developers are saying - we want to use Rust because we think it will save us development effort. NonStop people are saying we can't run this on our platform. It seems to me someone at git made the calculus: the amount that NonStop is contributing is less than what we save going to Rust. Unless NonStop has a support contract with git developers that they would be violating, it seems to me the NonStop people want to have their cake and eat it too.

According to git docs they seem to try to make a best effort to stick to POSIX but without any strong guarantees, which this change seems to be entirely in line with: https://github.com/git/git/blob/master/Documentation/CodingG...


An important point of using C is to write software that adheres to a decades old very widespread standard. Of course developers are free to not do that, but any tiny bit of Rust in the core or even in popular optional code amounts to the same as not using C at all, i.e. only using Rust, as far as portability is concerned.

If your codebase used to conform to a standard and the build environment relies on that standard, and now the your codebase doesn't anymore, then its not the build environment that deviates from the standard, its the codebase that brakes it.


Had you been under the impression that any of these niche platforms conform to any common standard other than their own?

Because they don’t. For instance, if they were fully POSIX compliant, they’d probably already have LLVM.


I expect them to conform to the C standard or to deal with the deviation. I don't think POSIX compliance is of much use on an embedded target.


I’m sold.


How is this git's concern?


They enjoy being portable and like things to stay that way so when they introduce a new toolchain dependency which will make it harder for some people to compile git, they point it out in their change log?


I don't think "NonStop" is a good gauge of portability.

But, I wasn't arguing against noting changes in a changelog, I'm arguing against putting portability to abstruse platforms before quality.


I don’t think staying portable means you have to do concession on quality. That merely limit your ability to introduce less portable dependancies.

But even then Git doesn’t mind losing some plateformes when they want to move forward on something.


Git's main concern should, of course, be getting Rust in, in some shape or form.


I am curious, does anyone know what is the use case that mandates the use of git on NonStop? Do people actually commit code from this platform? Seems wild.


Nonstop is still supported? :o


Among ecosystems based on YAML-formatted configuration defaulting to YAML 1.1 is nearly universal. The heyday of YAML was during the YAML 1.1 era, and those projects can't change their YAML parsers' default version to 1.2 without breaking extant config files.

By the time YAML 1.2 had been published and implementations written, greenfield projects were using either JSON5 (a true superset of JSON) or TOML.

  > While JSON numbers are grammatically simple, they're almost always distinct
  > from how you'd implement numbers in any language that has JSON parsers,
  > syntactically, exactness and precision-wise.
For statically-typed languages the range and precision is determined by the type of the destination value passed to the parser; it's straightforward to reject (or clamp) a JSON number `12345` being parsed into a `uint8_t`.

For dynamically-typed languages there's less emphasis on performance, so using an arbitrary-precision numeric type (Python's Decimal, Go's "math/big" types) provide lossless decoding.

The only language I know of that really struggles with JSON numbers is, ironically, JavaScript -- its BigInt type is relatively new and not well integrated with its JSON API[0], and it doesn't have an arbitrary-precision type.

[0] See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe... for the incantation needed to encode a BigInt as a number.


Arguably the root problem was lack of user namespacing; the incident would have been less likely to happen in the first place if the packages in question were named "~akoculu/left-pad" and "~akoculu/kik".


That's right and probably a lot less people would have used left-pad because it looks like a package for a specific org.


I think that statement is parsed as "npm was the first incredibly accessible package manager for [server-side JavaScript, which at the time was] an emergent popular technology,"


I get that, but there was plenty of prior art to learn from anyway.


You wrote "water has great compressive strength", sk5t directly (and correctly) refuted that claim. What is there to think about?

Are you confusing "compressive strength" with compressibility?


I think his point is that things very rarely experience purely compressive forces. Just being compressed induces tension in other directions, like water being squished out between your clapping hands. So even though water has great compressive strength, in practice this isn't very useful.


Exactly.

Many materials would have compressive strength easily, just by being relatively uncompressible.

But most loads have a (troublesome) tensile component. Fundamentally, the ability of a rigid material to resist deformation (in the most general sense) is what is most important, and that requires tensile strength.

See this comment elsewhere in this sub-thread that explains it probably better than I did: https://news.ycombinator.com/item?id=43904800


Look up the Wikipedia definition [1] of compressive strength:

> In mechanics, compressive strength (or compression strength) is the capacity of a material or structure to withstand loads tending to reduce size (compression). It is opposed to tensile strength which withstands loads tending to elongate, resisting tension (being pulled apart).

Google search AI summary states:

> Compressive strength is a material's capacity to resist forces that try to reduce its volume or cause deformation.

To be fair, compressive strength is a complex measure. Compressibility is only one aspect of it. See this Encyclopedia Britannica article [2] about how compressive strength is tested.

[1] https://en.wikipedia.org/wiki/Compressive_strength

[2] https://www.britannica.com/technology/compressive-strength-t...


Please tell me how to make a water prism to test compressive strength and deformation resistance. Water is an incompressible fluid, that is different.

These are well understood terms in the field. Unfortunately, this illustrates the bounds of ai in subfields like materials: it confuses people.


I'm not saying water meets the strict definition of a material with high compressive strength (it does meet some, since it resists forces that attempt to decrease its volume well). I am just using as an extreme example of the issues with the concept of compressive strength.


lower the temperature


Nothing that you wrote here indicates you understand what is being discussed.

Water has very low compressive strength, so low that it freely deforms under its own weight. You can observe this by pouring some water onto a table. This behavior is distinct from materials with high compressive strength, such as wood or steel.

(I say "very low" instead of "zero" because surface tension could be considered a type of compressive strength at small scales, such as a single drop of water on a hydrophobic surface)


Your comments betrays a lack of comprehension and understanding. Please reads my comments and linked definitions carefully.

See this comment elsewhere in this sub-thread that explains it probably better than I did: https://news.ycombinator.com/item?id=43904800


  > use SECCOMP_SET_MODE_STRICT to isolate the child process. But at that
  > point, what are you even doing? Probably nothing useful.
The classic example of a fully-seccomp'd subprocess is decoding / decompression. If you want to execute ffmpeg on untrusted user input then seccomp is a sandbox that allows full-power SIMD, and the code has no reason to perform syscalls other than read/write to its input/output stream.

On the client side there's font shaping, PDF rendering, image decoding -- historically rich hunting grounds for browser CVEs.


The classic example of a fully-seccomp'd subprocess is decoding / decompression.

Yes. I've run JPEG 2000 decoders in a subprocess for that reason.


Well, it seems that lately this kind of task wants to write/mmap to a GPU, and poke at font files and interpret them.


I flagged this for being LLM-generated garbage; original comment below. Any readers interested in benchmarking programming language implementations should visit https://benchmarksgame-team.pages.debian.net/benchmarksgame/... instead.

---

The numbers in the table for C vs Rust don't make sense, and I wasn't able to reproduce them locally. For a benchmark like this I would expect to see nearly identical performance for those two languages.

Benchmark sources:

https://github.com/naveed125/rust-vs/blob/6db90fec706c875300...

https://github.com/naveed125/rust-vs/blob/6db90fec706c875300...

Benchmark process and results:

  $ gcc --version
  gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0
  $ gcc -O2 -static -o bench-c-gcc benchmark.c
  $ clang --version
  Ubuntu clang version 14.0.0-1ubuntu1.1
  $ clang -O2 -static -o bench-c-clang benchmark.c
  $ rustc --version
  rustc 1.81.0 (eeb90cda1 2024-09-04)
  $ rustc -C opt-level=2 --target x86_64-unknown-linux-musl -o bench-rs benchmark.rs

  $ taskset -c 1 hyperfine --warmup 1000 ./bench-c-gcc
  Benchmark 1: ./bench-c-gcc
    Time (mean ± σ):       3.2 ms ±   0.1 ms    [User: 2.7 ms, System: 0.6 ms]
    Range (min … max):     3.2 ms …   4.1 ms    770 runs

  $ taskset -c 1 hyperfine --warmup 1000 ./bench-c-clang
  Benchmark 1: ./bench-c-clang
    Time (mean ± σ):       3.5 ms ±   0.1 ms    [User: 3.0 ms, System: 0.6 ms]
    Range (min … max):     3.4 ms …   4.8 ms    721 runs

  $ taskset -c 1 hyperfine --warmup 1000 ./bench-rs
  Benchmark 1: ./bench-rs
    Time (mean ± σ):       5.1 ms ±   0.1 ms    [User: 2.9 ms, System: 2.2 ms]
    Range (min … max):     5.0 ms …   7.1 ms    507 runs

Those numbers also don't make sense, but in a different way. Why is the Rust version so much slower, and why does it spend the majority of its time in "system"?

Oh, it's because benchmark.rs is performing a dynamic memory allocation for each key. The C version uses a buffer on the stack, with fixed-width keys. Let's try doing the same in the Rust version:

  --- benchmark.rs
  +++ benchmark.rs
  @@ -38,22 +38,22 @@
   }
 
   // Generates a random 8-character string
  -fn generate_random_string(rng: &mut Xorshift) -> String {
  +fn generate_random_string(rng: &mut Xorshift) -> [u8; 8] {
       const CHARSET: &[u8] = b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
  -    let mut result = String::with_capacity(8);
  +    let mut result = [0u8; 8];
   
  -    for _ in 0..8 {
  +    for ii in 0..8 {
           let rand_index = (rng.next() % 62) as usize;
  -        result.push(CHARSET[rand_index] as char);
  +        result[ii] = CHARSET[rand_index];
       }
   
       result
   }
   
   // Generates `count` random strings and tracks their occurrences
  -fn generate_random_strings(count: usize) -> HashMap<String, u32> {
  +fn generate_random_strings(count: usize) -> HashMap<[u8; 8], u32> {
       let mut rng = Xorshift::new();
  -    let mut string_counts: HashMap<String, u32> = HashMap::new();
  +    let mut string_counts: HashMap<[u8; 8], u32> = HashMap::with_capacity(count);
   
       for _ in 0..count {
           let random_string = generate_random_string(&mut rng);
Now it's spending all its time in userspace again, which is good:

  $ taskset -c 1 hyperfine --warmup 1000 ./bench-rs
  Benchmark 1: ./bench-rs
    Time (mean ± σ):       1.5 ms ±   0.1 ms    [User: 1.3 ms, System: 0.2 ms]
    Range (min … max):     1.4 ms …   3.2 ms    1426 runs
 
... but why is it twice as fast as the C version?

---

I go to look in benchmark.c, and my eyes are immediately drawn to this weird bullshit:

  // Xorshift+ state variables (64-bit)
  uint64_t state0, state1;

  // Xorshift+ function for generating pseudo-random 64-bit numbers
  uint64_t xorshift_plus() {
      uint64_t s1 = state0;
      uint64_t s0 = state1;
      state0 = s0; 
      s1 ^= s1 << 23; 
      s1 ^= s1 >> 18; 
      s1 ^= s0; 
      s1 ^= s0 >> 5;
      state1 = s1; 
      return state1 + s0; 
  }
That's not simply a copy of the xorshift+ example code on Wikipedia. Is there any human in the world who is capable of writing xorshift+ but is also dumb enough to put its state into global variables? I smell an LLM.

A rough patch to put the state into something the compiler has a hope of optimizing:

  --- benchmark.c
  +++ benchmark.c
  @@ -18,25 +18,35 @@
   StringNode *hashTable[HASH_TABLE_SIZE]; // Hash table for storing unique strings
   
   // Xorshift+ state variables (64-bit)
  -uint64_t state0, state1;
  +struct xorshift_state {
  +       uint64_t state0, state1;
  +};
   
   // Xorshift+ function for generating pseudo-random 64-bit numbers
  -uint64_t xorshift_plus() {
  -    uint64_t s1 = state0;
  -    uint64_t s0 = state1;
  -    state0 = s0;
  +uint64_t xorshift_plus(struct xorshift_state *st) {
  +    uint64_t s1 = st->state0;
  +    uint64_t s0 = st->state1;
  +    st->state0 = s0;
       s1 ^= s1 << 23;
       s1 ^= s1 >> 18;
       s1 ^= s0;
       s1 ^= s0 >> 5;
  -    state1 = s1;
  -    return state1 + s0;
  +    st->state1 = s1;
  +    return s1 + s0;
   }
   
   // Function to generate an 8-character random string
   void generate_random_string(char *buffer) {
  +    uint64_t timestamp = (uint64_t)time(NULL) * 1000;
  +    uint64_t state0 = timestamp ^ 0xDEADBEEF;
  +    uint64_t state1 = (timestamp << 21) ^ 0x95419C24A637B12F;
  +    struct xorshift_state st = {
  +        .state0 = state0,
  +        .state1 = state1,
  +    };
  +
       for (int i = 0; i < STRING_LENGTH; i++) {
  -        uint64_t rand_value = xorshift_plus() % 62;
  +        uint64_t rand_value = xorshift_plus(&st) % 62;
   
           if (rand_value < 10) { // 0-9
               buffer[i] = '0' + rand_value;
  @@ -113,11 +123,6 @@
   }
   
   int main() {
  -    // Initialize random seed
  -    uint64_t timestamp = (uint64_t)time(NULL) * 1000;
  -    state0 = timestamp ^ 0xDEADBEEF; // Arbitrary constant
  -    state1 = (timestamp << 21) ^ 0x95419C24A637B12F; // Arbitrary constant
  -
       double total_time = 0.0;
   
       // Run 3 times and measure execution time
  
and the benchmarks now make slightly more sense:

  $ taskset -c 1 hyperfine --warmup 1000 ./bench-c-gcc
  Benchmark 1: ./bench-c-gcc
    Time (mean ± σ):       1.1 ms ±   0.1 ms    [User: 1.1 ms, System: 0.1 ms]
    Range (min … max):     1.0 ms …   1.8 ms    1725 runs
  
  $ taskset -c 1 hyperfine --warmup 1000 ./bench-c-clang
  Benchmark 1: ./bench-c-clang
    Time (mean ± σ):       1.0 ms ±   0.1 ms    [User: 0.9 ms, System: 0.1 ms]
    Range (min … max):     0.9 ms …   1.4 ms    1863 runs
But I'm going to stop trying to improve this garbage, because on re-reading the article, I saw this:

  > Yes, I absolutely used ChatGPT to polish my code. If you’re judging me for this,
  > I’m going to assume you still churn butter by hand and refuse to use calculators.
  > [...]
  > I then embarked on the linguistic equivalent of “Google Translate for code,”
Ok so it's LLM-generated bullshit, translated into other languages either by another LLM, or by a human who doesn't know those languages well enough to notice when the output doesn't make any sense.


> my eyes are immediately drawn to this weird bullshit

Gave me a good chuckle there :)

Appreciate this write up; I'd even say your comment deserves its own article, tbh. Reading your thought process and how you addressed the issues was interesting. A lot of people don't know how to identify or investigate weird bullshit like this.


So glad I had read the 2nd agreement by Don Miguel Ruiz lol.


The "dsb nsh; isb" sequence after "svc 0" is part of OpenBSD's mitigations for Spectre.

https://github.com/openbsd/src/commit/bbeaada4689520859307d5...

https://github.com/openbsd/src/commit/0c401ffc2a2550c32105ce...

https://github.com/openbsd/src/commit/5ecc9681133f1894e81c38...

If I'm reading the commits correctly, the OpenBSD kernel will skip two instructions after a "svc 0" when returning to userspace, on the assumption that any syscall comes from libc and therefore has "dsb nsh; isb" after it.


  > This is a very good example of how C is not "close to the machine" or
  > "portable assembly",
C is very much "portable assembly" from the perspective of other systems programming languages of the 80s-90s era. The C expression `a += 1` can be trusted to increment a numeric value, but the same expression in C++ might allocate memory or unwind the call stack or do who knows what. Similarly, `a = "a"` is a simple pointer assignment in C, but in C++ it might allocate memory or [... etc].

The phrase "C is portable assembly" isn't a claim that each statement gets compiled directly to equivalent machine code.


When the code has hit the IR in clang or gcc, there is no 'a' (we know that with certainty, since SSA form doesn't mutate but assigns to fresh variables). We don't know if there will be an increment of 1, the additions could be coalesced (or elided if the result can be inferred another way). The number can even decrease, say if things have been handled in chunks of 16, and needs to be adjusted down in the last chunk. Or the code may be auto-vectorized and completely rewritten, so that none of the variables at the C level are reflected on the assembler level.


From a high-level academic view, yes, the compiler is allowed to perform any legal transformation. But in practice C compilers are pretty conservative about what they emit, especially when code is compiled without -march= .

You don't have to take my word for it. Go find a moderately complex open-source library written in C, compile it, then open up the result in Hexrays/Ghidra/radare2/whatever. Compare the compiled functions with their original source and you'll see there's not that much magic going on.


-O3 does autovectorization: turning your loops into a bunch of SIMD instructions, sometimes even drastically changing performance profile.

If autovectorization is "not that much magic" then idk what else it is.


Any optimization you are familiar with is trivial and expected. Everything else is broken compilers optimizing UB to win benchmarks.


Nowadays it's -O2. I was also surprised when I first learned this.


They are as aggressive as they can be.

Here's an example of a C compiler completely eliminating a loop because it has figure out how to transform the loop into a constant calculation.

https://godbolt.org/z/cfndqMj4j

The place where C compilers are conservative is when dealing with arrays and pointers. That's because it's impossible for C to know if a pointer is to an element of an array or something completely different. Pointer math further complicates what a pointer could actually reference.


Saying that something "is like XY" when you really mean "is like XY, at least in comparison to C++" isn't what most people mean.

C is not a portable assembler.

In C, "a += 1" could overflow, and signed overflow is undefined behavior--even though every individual ISA has completely defined semantics for overflow, and nearly all of them these days do two's complement wraparound arithmetic. With C's notion of undefined behavior, it doesn't even give you the same wraparound in different places in the same program. In fact, wraparound is so undefined that the program could do absolutely anything, and the compiler is not required to even tell you about it. Even without all the C++ abstraction madness, a C compiler can give you absolutely wild results due to optimizations, e.g. by evaluating "a += 1" at compile time and using a different overflow behavior than the target machine. Compile-time evaluation not matching runtime evaluation is one of a huge number of dumb things that C gives you.

Another is that "a += 1" may not even increment the variable. If this occurs as an expression, and not as a statement, e.g. "f(a += 1, a += 1)", you might only get one increment due to sequence points[1]--not to mention that the order of evaluation might be different depending on the target.

C is not a portable assembler.

C is a low-level language where vague machine-like programs get compiled to machine code that may or may not work, depending on whether it violates UB rules or not, and there are precious few diagnostics to tell if that happened, either statically or dynamically.

[1] https://en.wikipedia.org/wiki/Sequence_point


> The phrase "C is portable assembly" isn't a claim that each statement gets compiled directly to equivalent machine code.

Weasel words. Like a "self driving car" that requires a human driver with constant attention willing to take over within a few hundred milliseconds.

People advocate for C and use it in a way that implies they think it can achieve specific machine outcomes, and it usually does .. except when it doesn't. If people want a portable assembler they should build one.


As a general rule if you're reading a technical discussion and every single participant is using a particular phrase in a way that doesn't make sense to you then you should probably do a quick double-check to make sure you're on the same page.

For example, in this discussion about whether C is "portable assembly", you might be tempted to think back to the days of structured programming in assembly using macros. I no longer remember the exact syntax, but programs could be written to look like this:

  .include "some-macro-system.s"
  .include "posix-sym.s"

  .func _start(argc, argv) {
    .asciz message "Hello, world!"
    .call3 _write STDOUT message (.len message)
    .call1 _exit 0
  }
Assembly? Definitely! Portable? Eh, sort of! If you're willing to restrict yourself to DOS + POSIX and write an I/O abstraction layer then it'll probably run on i386/SPARC/Alpha/PA-RISC.

But that's not really what people are discussing, is it?

When someone says "C is portable assembly" they don't mean you can take C code and run it through a platform-specific macro expander. They don't mean it's literally a portable dialect of assembly. They expect the C compiler to perform some transformations -- maybe propagate some constants, maybe inline a small function here and there. Maybe you'd like to have named mutable local variables, which requires a register allocator. Reasonable people can disagree about exactly what transformations are legal, but at that point it's a matter of negotiation.

Anyway, now you've got a language that is more portable than assembler macros but still compiles more-or-less directly to machine code -- not completely divorced from the underlying hardware like Lisp (RIP Symbolics). How would you describe it in a few words? "Like assembly but portable" doesn't seem unreasonable.


> still compiles more-or-less directly to machine code

There's a lot hiding in "more or less". The same kind of example holds for e.g. C# : https://godbolt.org/noscript/csharp ; if you hit "Compile" it'll give you the native binary. If you write "x+1" it'll generate an add .. or be optimized away. Now does that mean it's portable assembler? Absolutely not.

Conversely there's a bunch of things that people expect to do in C, do in real code, but are not in the standard or are undefined or implementation-defined. As well as things that are present in assemblers for various platforms (things like the overflow flag) which aren't accessible from the C language.

What people actually seem to mean by "portable assembler" is "no guardrails". Memory unsafety as a feature.

> Reasonable people can disagree about exactly what transformations are legal, but at that point it's a matter of negotiation

And a matter of CVEs when you lose your negotiation with the compiler. Or less dramatic things like the performance fluctuations under discussion.


Systems languages that predated C already could do that, that is the typical myth.


> The C expression `a += 1` can be trusted to increment a numeric value, [...]

Have you heard of undefined behaviour?


Show me a C compiler that miscompiles the following code and I'll concede the point:

  uint32_t add_1(uint32_t a) {
    a += 1;
    return a;
  }


C might be low level from the perspective of other systems languages, but that is like calling Apollo 11 simple from the perspective of modern spacecraft. C as written is not all that close to what actually gets executed.

For a small example, there are many compilers who would absolutely skip incrementing 'a' in the following code:

  uint32_t add_and_subtract_1(uint32_t a) {
    a += 1;
    a -= 1;
    return a;
  }
Even though that code contains `a += 1;` clear as day, the chances of any incrementing being done are quite small IMO. It gets even worse in bigger functions where out-of-order execution starts being a thing.


If you want the compiler to treat your code as literal portable assembly turn off optimizations.


That's a property of a particular compiler, not of the C language (at least as described in the standard).


Exactly, pretty much what I said, or enable / disable the optimizations you want.


Why would you want it to increment 1 if we decrement 1 from the same variable? That would be a waste of cycles and a good compiler knows how to optimize it out, or what am I misunderstanding here? What do you expect "it" to do and what does it really do?

See: https://news.ycombinator.com/item?id=43320495


I'm pretty sure that's replying directly to the comment about how c is close to assembly and that if you add that line of code somewhere you know there's a variable getting incremented. Doesn't really matter whether or not it's useful, the point is that the behavior isn't exactly what you wrote


To reiterate, claiming that C can be described as "portable assembly" is not a claim that it is literally a package of assembler macros that emit deterministic machine code for each individual source expression.

I linked these in another comment, but here's some examples of straightforward-looking integer addition emitting more complex compiler output for other languages that compile to native code:

Haskell: https://godbolt.org/z/vdeMKMETT

C++: https://godbolt.org/z/dedcof9x5


The Haskell version deals with both laziness and also detects overflow.

Your C++ example has a lot more code than the C example, I'm not sure why you'd expect it to produce the same output?


https://godbolt.org/z/r39jK1ddv

It increments, then decrements with -O0 though.

I do not see the issue still, as the behavior is expected with -O0; increments then decrements.


There's nothing in the C standard that enforces the observed -O0 behaviour. Your compiler might change tomorrow.


How likely is that to happen, and in which languages can you either optimize or not AND where the compiler might not change tomorrow though?

David Hume said that we cannot know if the sun is going to rise tomorrow just because it has always did before. See "problem of induction", https://philosophynow.org/issues/160/Humes_Problem_of_Induct....


The C standard guarantees certain behaviours that will not change, even if your C compiler changes. That's the whole point of the standard. And it has nothing to do with the problem of induction.

But the standard does not guarantee that specific assembly instructions will be used.


Sure, but what programming language or its standard guarantees it (and its compilers in practice, of course), then?

You said "Your compiler might change tomorrow.", but does it not apply to EVERY programming language's compiler?


> You said "Your compiler might change tomorrow.", but does it not apply to EVERY programming language's compiler?

Yes. I wasn't the one trying to argue that C is special in this regard. Just the opposite.


That’s a contrived example but in a serious program there would often be code in between or some level of indirection (e.g. one of those values is a lookup, a macro express, or the result of another function).

Nothing about that is cheating, it just says that even C programmers cannot expect to look at the compiled code and see a direct mapping from their source code. Your ability to reason about what’s actually executing requires you to internalize how the compiler works in addition to your understanding of the underlying hardware and your application.


What optimizer would remove the increment/decrement if the value was accessed in between? That seems like something that would be really easy to detect.

I’ve never studied compilers though.


It would be very normal for a compiler to do an increment (or merge it into a later instruction), but never do the decrement, and instead use the old copy of the value.


Then in the next step, it would see that the result of the increment is never used and thus the increment instruction is dead code and can also be removed.


In what languages can you do that that is not assembly though? The higher level the language is, the "worse" or difficult it gets, perhaps I am not following the thread right.


Yes, this is normal for languages. The only pushback here is against the term “portable assembler” being applied to C, where it’s incomplete often enough that many people feel it’s no longer a helpful label.

I think it’s also reflecting the maturity and growth of the industry. A turn of the century programmer could relatively easily find areas where dropping down to assembly was useful, but over the subsequent decades that’s become not only uncommon but often actively harmful: your code hand-optimized for a particular processor is likely slower on newer processors than what a modern compiler emits and is definitely a barrier to portability in an era where not only are ARM and potentially RISC-V of interest but also where code is being run on SIMD units or GPUs. This makes the low-level “portable assembler” idea less useful because there’s less code written in that middle ground when you want either a higher-level representation which gives compilers more flexibility or precise control. For example, cryptography implementers want not just high performance but also rigid control of the emitted code to avoid a compiler optimizing their careful constant-time implementation into a vulnerability.


I'm not an embedded expert but a friend of mine has complained about compiler optimizations breaking things in his programs. I could see incrementing by one being used to set some bits in a memory location for a cycle that may mean something to some peripheral and then decrementing by one to set some other bits that may mean something else. In that case, the compiler removing those two lines would cause a very hard to debug issue.


I understand compiler optimizations having unintended consequences, e.g. https://godbolt.org/z/r39jK1ddv but there are a lot of options he may use to enable or disable optimizations (assuming GCC here): https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html

This is even more difficult to do with higher-level languages.


It is unlikely as is, but it frequently arises from macro expansions and inlining.


> It gets even worse in bigger functions where out-of-order execution starts being a thing.

In addition, add that your processor isn't actually executing x86 (nor ARM etc) instructions, but interprets/compiles them to something more fundamental.

So there's an additional layer of out-of-order instructions and general shenanigans happening. Especially with branch prediction in the mix.


If my misocompile, you mean that it fails the test that a "C expression `a += 1` can be trusted to increment a numeric value", then it is trivial: https://godbolt.org/z/G5dP9dM5q


Here's another one, just for fun https://godbolt.org/z/TM1Ke4d5E


I was being somewhat terse.

The (implied) claim is that the C standard has enough sources of undefined behavior that even a simple integer addition can't be relied upon to actually perform integer addition.

But the sources of undefined behavior for integer addition in C are well-known and very clear, and any instruction set that isn't an insane science project is going to have an instruction to add integers.

Thus my comment. Show me a C compiler that takes that code and miscompiles it. I don't care if it returns a constant, spits out an infinite loop, jumps to 0x0000, calls malloc, whatever. Show me a C compiler that takes those four lines of C code and emits something other than an integer addition instruction.


Why are you talking about miscompilation? While the LLVM regression in the featured article makes the code slower, it is not a miscompilation. It is "correct" according to the contract of the C language.


You show one example where C doesn't have problems, but that's a much weaker claim than it sounds. "Here's one situation where this here gun won't blow your foot off!"

For what it's worth, C++ also passes your test here. You picked an example so simple that it's not very interesting.


'eru implied `a += 1` has undefined behavior; I provided a trivial counter-example. If you'd like longer examples of C code that performs unsigned integer addition then the internet has many on offer.

I'm not claiming that C (or C++) is without problems. I wrote code in them for ~20 years and that was more than enough; there's a reason I use Rust for all my new low-level projects. In this case, writing C without undefined behavior requires lots of third-party static analysis tooling that is unnecessary for Rust (due to being built in to the compiler).

But if you're going to be writing C as "portable assembly", then the competition isn't Rust (or Zig, or Fortran), it's actual assembly. And it's silly to object to C having undefined behavior for signed integer addition, when the alternative is to write your VM loop (or whatever) five or six times in platform-specific assembly.


Yes, 'a += 1' can have undefined behaviour in C when you use signed integers. (And perhaps also with floats? I don't remember.)

Your original comment didn't specify that you want to talk about unsigned integers only.


Forth might be a better competition for 'portable assembly', though.


Actually even here, C has some problems (and C++), too:

I don't think the standard says much about how to handle stack overflows?


I know that for 'int a' the statement 'a += 1' can give rather surprising results.

And you made a universal statement that 'a += 1' can be trusted. Not just that it can sometimes be trusted. In C++ the code you gave above can also be trusted as far as I can tell. At least as much as the C version.


I'll expand my point to be clearer.

In C there is no operator overloading, so an expression like `a += 1` is easy to understand as incrementing a numeric value by 1, where that value's type is one of a small set of built-in types.

You'd need to look further up in the function (and maybe chase down some typedefs) to see what that type is, but the set of possible types generally boils down to "signed int, unsigned int, float, pointer". Each of those types has well-defined rules for what `+= 1` means.

That means if you see `int a = some_fn(); assert(a < 100); a += 1` in the C code, you can expect something like `ADD EAX,1` somewhere in the compiler output for that function. Or going the other direction, when you're in a GDB prompt and you disassemble the current EIP and you see `ADD EAX,1` then you can pretty much just look at the C code and figure out where you are.

---

Neither of those is true in C++. The combination of completely ad-hoc operator overloading, function overloading, and implicit type conversion via constructors means that it can be really difficult to map between the original source and the machine code.

You'll have a core dump where EIP is somewhere in the middle of a function like this:

  std::string some_fn() {
    some_ns::unsigned<int> a = 1;
    helper_fn(a, "hello");
    a += 1;
    return true;
  }
and the disassembly is just dozens of function calls for no reason you can discern, and you're staring at the return type of `std::string` and the returned value of `true`, and in that moment you'll long for the happy days when undefined behavior on signed integer overflow was the worst you had to worry about.


> That means if you see `int a = some_fn(); assert(a < 100); a += 1` in the C code, you can expect something like `ADD EAX,1` somewhere in the compiler output for that function.

I completely agree that C++ is orders of magnitude worse but I’ve seen at least a couple counter-examples with code almost that simple. A researcher I used to support compared each release against a set of reference results, and got a surprise when they didn’t match but his program was working. This turned out to be a new compiler release being smart enough to inline and reorder his code to use a fused multiply-add instruction, which had greater internal precision and so the result was very slightly different from his saved referenced set. GCC has -fexcess-precision=standard for this but you have to understand the problem first.


    error: could not convert 'true' from 'bool' to 'std::string' {aka 'std::__cxx11::basic_string<char>'}
I don't think anyone's claiming C nor C++'s dumpster fires have signed integer overflow at the top of the pile of problems, but when the optimizer starts deleting security or bounds checks and other fine things - because of signed integer overflow, or one of the million other causes of undefined behavior - I will pray for something as straightforward as a core dump, no matter where EIP has gone.

Signed integer overflow UB is the kind of UB that has a nasty habit of causing subtle heisenbugfuckery when triggered. The kind you might, hopefully, make shallow with ubsan and good test suite coverage. In other words, the kind you won't make shallow.


For context, I did not pick that type signature at random. It was in actual code that was shipping to customers. If I remember correctly there was some sort of bool -> int -> char -> std::string path via `operator()` conversions and constructors that allowed it to compile, though I can't remember what the value was (probably "\x01").

---

My experience with the C/C++ optimizer is that it's fairly timid, and only misbehaves when the input code is really bad. Pretty much all of the (many, many) bugs I've encountered and/or written in C would have also existed if I'd written directly in assembly.

I know there are libraries out there with build instructions like "compile with -O0 or the results will be wrong", but aside from the Linux kernel I've never encountered developers who put the blame on the compiler.


> but aside from the Linux kernel I've never encountered developers who put the blame on the compiler.

I encounter them frequently.

99.99% of the time it's undefined behavior and they're "wrong".

Frequently novices who have been failed by their teachers and documentation (see previous rant using atoi as an example of the poor quality of documentation about UB: https://news.ycombinator.com/item?id=14861917 .)

Less frequently, it's experienced devs half joking out of a need for catharsis.

Rarely, experienced devs finally getting to the end of their rope, and are finally beginning to seriously consider if they've got a codegen bug. They don't, but they're considering it. They know they were wrong the last 10 times they considered it, but they're considering it again damnit!

The linux kernel devs aren't quite unique in "just because you can, doesn't mean you should"ing their way into blaming the compiler for what could be argued to be defects in the standard or fundamental design of the language (the defect being making UB so common), but that's probably among the rarest slice of the pie of people blaming the compiler for UB. Few have the will to tilt at that windmill and voice their opinions when the compiler devs can easily just blame the standard - better to keep such unproductive rants close to heart instead, or switch to another language. Something actually productive.

0.01% of the time, it's a legitimate codegen bug on well-defined behavior code. Last one I tracked down to a bug tracker, was MSVC miscompiling 4x4 matrix multiplications by failing to spill a 17th value to stack when it only had 16 SSE register to work with. Caught by unit tests, but not by CI, since people updated compiler versions at their own random pace, and who runs `math_tests` on their personal machines when they're not touching `math`?


I heartily agree that C++ is a lot more annoying here than C, yes.

I'm just saying that C is already plenty annoying enough by itself, thanks eg to undefined behaviour.

> That means if you see `int a = some_fn(); assert(a < 100); a += 1` in the C code, you can expect something like `ADD EAX,1` somewhere in the compiler output for that function. Or going the other direction, when you're in a GDB prompt and you disassemble the current EIP and you see `ADD EAX,1` then you can pretty much just look at the C code and figure out where you are.

No, there's no guarantee of that. C compilers are allowed to do all kinds of interesting things. However you are often right enough in practice, especially if you run with -O0, ie turn off the optimiser.

See eg https://godbolt.org/z/YY69Ezxnv and tell me where the ADD instruction shows up in the compiler output.



I'm not sure that's a counter-example -- what assembly do you think should be emitted for floating-point math on an AVR microcontroller?


It means that "a += 1` is easy to understand as incrementing a numeric value by 1" is not true and instead "it can be really difficult to map between the original source and the machine code".

More examples of non-trivial mapping from C code to generated code: https://godbolt.org/z/jab6vh6dM


All of those look pretty straightforward to me -- again, what assembly would you expect to be emitted in those cases?

For contrast, here's the assembly generated for Haskell for integer addition: https://godbolt.org/z/vdeMKMETT

And here's assembly for C++: https://godbolt.org/z/dedcof9x5


> All of those look pretty straightforward to me -- again, what assembly would you expect to be emitted in those cases?

It is very straightforward indeed, but it is still not mapping primitive operations to direct machine code, but it is forwarding to out-of-line code. Same as operator overloading in other languages.

> And here's assembly for C++: https://godbolt.org/z/dedcof9x5

That's just a symptom of allowing the compiler to inline the add code, otherwise the generated code is as straightforward:

   addOne(Int):
    push   rax
    mov    esi,0x1
    call   4010c0 <add_safe(int, int)>
Ref: https://godbolt.org/z/xo1es9TcW


  > It is very straightforward indeed, but it is still not mapping primitive
  > operations to direct machine code, but it is forwarding to out-of-line code.
  > Same as operator overloading in other languages.
I am not claiming that C is a collection of assembler macros. There is no expectation that a C compiler emit machine code that has exact 1:1 correspondence with the input source code.

  > Same as operator overloading in other languages.
The lack of operator overloading, and other hidden complex control flow, is the reason that someone can read C code and have a pretty good idea of what it compiles to.

  > That's just a symptom of allowing the compiler to inline the add code,
  > otherwise the generated code is as straightforward:
No, that's just moving the instructions around. You've still got dynamic allocation and stack-unwinding being generated for a line that doesn't have any sign of entering a complex control flow graph.


> ... and other hidden complex control flow,....

Until someone calls longjmp() or a signal() is triggered. Extra bonus of fun if it happens to be multithreaded application, or in the middle of a non-rentrant call.


Or your emulated float or atomic relies on non-local state, like a control word or a spin-lock pool.


a+=1 will not produce any surprising results, signed integer overflow is well defined on all platforms that matter.

And we all know about the looping behavior, it isn't surprising.

The only surprising part would be if the compiler decides to use inc vs add, not that it really matters to the result.


> a+=1 will not produce any surprising results, signed integer overflow is well defined on all platforms that matter.

I'm not sure what you are talking about?

There's a difference between how your processor behaves when given some specific instructions, and what shenanigans your C compiler gets up to.

See eg https://godbolt.org/z/YY69Ezxnv and tell me where the ADD instruction shows up in the compiler output. Feel free to pick a different compiler target than Risc-V.


I don't think "dead-code elimination removes dead code" adds much to the discussion.

If you change the code so that the value of `a` is used, then the output is as expected: https://godbolt.org/z/78eYx37WG


The parent example can be made clearer like this: https://godbolt.org/z/MKWbz9W16

Dead code elimination only works here because integer overflow is UB.


Take a closer look at 'eru's example and my follow-up.

He wrote an example where the result of `a+1` isn't necessary, so the compiler doesn't emit an ADDI even though the literal text of the C source contains the substring "a += 1".

Your version has the same issue:

  unsigned int square2(unsigned int num) {
      unsigned int a = num;
      a += 1;
      if (num < a) return num * num;
      return num;
  }
The return value doesn't depend on `a+1`, so the compiler can optimize it to just a comparison.

If you change it to this:

  unsigned int square2(unsigned int num) {
      unsigned int a = num;
      a += 1;
      if (num < a) return num * a;
      return num;
  }
then the result of `a+1` is required to compute the result in the first branch, and therefore the ADDI instruction is emitted.

The (implied) disagreement is whether a language can be considered to be "portable assembly" if its compiler elides unnecessary operations from the output. I think that sort of optimization is allowed, but 'eru (presumably) thinks that it's diverging too far from the C source code.


In what world the return value doesn't depends on 'a' in this code?

  if (num < a) return num * num;
  /*else*/    return num;
A control dependency is still a dependency


`a = num; a += 1; if (num < a)` is the same as `if (num < (num + 1))`, which for unsigned integer addition can be rewritten as `if (num != UINT_MAX)`. So there's no need to actually compute `a+1`, the comparison is against a constant.

If the code returns `num * a` then the value of `a` is now necessary, and must be computed before the function returns.

For signed integer addition the compiler is allowed to assume that `(num < (num + 1))` is true, so the comparison can be removed entirely.


> For signed integer addition the compiler is allowed to assume that `(num < (num + 1))` is true, so the comparison can be removed entirely.

That's not directly what the compiler assumes. The direct problem is in 'a + 1' having undefined behaviour, and that transitively allows the assumption on the comparison that you mentioned.

This was an example where 'a + 1' doesn't compile to an add instruction.


C compilers are just too smart IMO to make C portable assembly. Your example doesn’t always ADDI either, for example if it is inlined

https://godbolt.org/z/vv9rvKsxn

This isn’t qualitatively different from what the JVM JIT would do, but Java isn’t considered portable assembly.

I guess if you compile with optimizations completely off, you get something that is assembly-like, but I’ve never seen that in prod code.


> He wrote an example where the result of `a+1` isn't necessary, so the compiler doesn't emit an ADDI even though the literal text of the C source contains the substring "a += 1".

No, the result of the 'a+1' is necessary in my version. And if you change the type from 'int' to 'unsigned' you will see that the compiler no longer just omits the addition.


That will be inline by any C compiler and then pretty much anything can happen to the 'a += 1'.


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

Search: