Not yet that I know of, though perhaps LLVM might be able to infer simple cases (when loading from a known constant for example).
Pattern types could potentially maybe in the future allow for the compiler to know more details though. They are a nightly feature, and afaik only for enums, integers and pointers so far. The idea would be that you can define a custom type such as "an integer between 7 and 45" and everything else become niches for niche optimisation (e.g. for `Option<MyFunkyInt>` some of those impossible values would be used to represent the None case of the wrapping Option).
But I could envisage a future in which you could say "f64 without NaN" which would both make those available for niches and potentially tell LLVM about this. However, we are very far from any of that currently. And it might not be what you want, since you would need to add checks when you perform operations to ensure the value doesn't suddenly become a NaN. Which is way more complicated than ensuring integers don't become, say, zero. It will likely be much harder to optimise away the checks.
LLVM has range flags and things like nnan that could in theory be used to replace a minimum intrinsic into a minimumnum (iirc x86 has a instruction for the latter but not the former) https://llvm.org/docs/LangRef.html#floating-point-min-max-in... I'm not sure if the optimizations use this but in theory they could
That is a big maybe though. It is very experimental (didn't even have non-placeholder syntax last I looked), and as far as I know nobody has yet even discussed it for floats.
Those tank controls are not great, and going to 3D made the graphics noticeably worse. It would have been a better game had it stayed 2D but with the same story. A lot of early 3D has aged poorly.
Yes the first 3 games are really good. But I also have a soft spot for Myst. Maybe it is nostalgia, but I really enjoyed replaying it from the original CD of the Mac release a few years ago (using my original iBook, which is slightly later vintage than the game of course).
Gnome 2 was really good for it's time. Gnome 3 is a dumbed down disaster, removing settings I care about (such as font antialiasing settings, which I need to change to not get literal headaches, subpixel AA is awful as is anything blurry).
And that is just the tip of the iceberg when it comes to Gnome. I went to Xfce, MATE and then Cinnamon for some years, but I'm now back on KDE and it been good for several years (I left KDE when KDE 4 came out and went to Gnome 2).
But each to their own, use what makes you happy. But Gnome has a history of messing with your desktop, KDE less so in recent years.
> Gnome 3 [...] removing settings I care about (such as font antialiasing settings, which I need to change to not get literal headaches, subpixel AA is awful as is anything blurry).
1) That setting still exists, it's just less prominent.
2) Subpixel AA is no longer the default; the default is grayscale, because it works well on all screens in all orientations.
Last I checked I couldn't set full hinting. And for GTK4 programs I even had to set a hidden variable in a ini file[1] to make it respect my fontconfig setting for hinting.
Apparently it is supposed to be on for low DPI screens. I would count mine as low DPI (even my laptop is only around 150 DPI) but I still need to set that manually.
So no, GTK didn't keep the font rendering settings I care about, and is actively hostile in trying to ignore them even.
Last I checked (a few years ago admittedly) Gnome Tweaks only configured the fontconfig setting, not the ini setting to make GTK4 programs care.
Also Gnome Tweaks is an add-on, not part of the default install on any Linux distro I have tried. In KDE Plasma this is in the standard settings.
And if you use any desktop environment other than GNOME, their preferences certainly won't set the GTK4 hidden ini setting. Rather they expect programs to follow best interoperability practises, such as respecting the fontconfig settings. Most people won't limit themselves to only first-party apps from their DE of choice, so interoperability is king.
That or you hit some UB elsewhere that caused the compiler to assume things that you didn't follow.
Trying UBSAN and ASAN might have been worth it. I don't think comparing debug and release builds would have helped: could be buggy optimisations or UB in your code regardless of what the outcome of that test was.
In this case I was reading memory with a debugger and could see that it literally was the same object at the same memory address, but what I did not know was whether or not different parts of the code had different views of that field for some reason, or any number of other things that could have been causing the issue. Either way, it was super weird and frustrating.
The right tool for that occasion would be a watchpoint.
1. You check in both places in code in the debugger that the address of the field matches. (Maybe the object is at the same address, but due to some build system level ifdef mismatch, you effectively get different struct definitions in different places or something like that.)
2. You add a breakpoint inside the procedure and outside it.
3. When the breakpoint inside is hit, you add a watchpoint at the address of the field. Continue. See if the watchpoint gets hit before reaching the second breakpoint.
Having been involved in the WG14 discussions on this topic:
The issue is that there is a contingent of users who complains about cases where the return dynamically can't be hit but that isn't obvious statically. Consider something like this:
int do_something(enum meow koala) {
switch (koala) {
case enum_val_1: return 5;
case enum_val_2: return 3;
/* etc., covering all the enum values */
}
}
Should this be required to diagnose? That's the sticking point.
There may be differences between C and C++ here (and I worked with C++ more recently than C), but: if that enum doesn't specify an underlying type it would be UB to have a value that isn't in the "member list" of the enum. In that case it should be possible to determine if all cases are covered.
If an underlying type is specified, then it should error since it is legal to have those values (unless the whole range of the underlying type is covered by the cases.
Again, that is what would be sensible from a C++ perspective, I don't know if C differs here.
EDIT: Also, and now I'm talking with my Rust user hat on: it is better to not have pointless UB. Yes some is needed to practically allow for optimisation. But C and C++ had a lot of UB that doesn't really help with making your code faster, such as this.
I don't think your C++ comment is true, otherwise you wouldn't be allowed to OR together C enums. I think the restriction is on values wider than the underlying type, where the underlying type is always wide enough to support any representable bit pattern.
It seems like something that could have been trapped right in k&r.
Was it ever even theoretically under any circumstances for any reason intended to be able to write a stack of functions with no returns that just fall into each other like assembly? I can't believe it.
So it seems like something even the very first compiler could have cought right in an early parser pass or stage.
But I also decline to believe I have a better idea about something than K or R, so there must be a non-triviality I don't see. I mean goto() exists in the language so ?
... I guess simply detecting the end of a function, or detecting that the process reached the end of a function, isn't a good enough definition of the problem. You can have any number of returns or gotos in the middle that you are always supposed to hit, and intentionally no return at the end because instead you have an assert or a goto.
assert you should never get here, goto error, goto not error but just next step, etc. They might or might not be error conditions that the process reached that spot, but it's not an error that the code doesn't end with a return.
> I completely agree. My point isn't that Big-Oh is low level, but rather that Big-Oh is often enough for most programming problems.
For what I work on, the hidden constant is often more important than big O. For example, a hash map has better complexity than just searching through a vector. But if the vector is small enough it will best the hash nap for actual time. Just plain searching until you find the element will even beat binary search on a sorted vector for small enough vectors. The reasons are complex, to do with cache, prefetch, branch prediction and also just how many instructions your tight inner loop has. (And the specific reasons will vary between desktop class CPUs and microcontrollers. But both exhibit this pattern.)
You could argue that at that point why bother optimising at all (there aren't a lot of elements in the collection after all). But there are two distinct cases I have come across over the years where it still matters (and for what I work with, they represent the common cases):
* You need to look up in a small collection a lot (either lots of lookups into a few small collections or a few lookups each into lots of different small collections, I have seen both cases).
* Hard realtime code where predictable latency matters. Hashmap has a bad worst case, binary trees and binary searching has badly predictable memory access patterns. And in this case the collections are usually small anyway (there are only so many actuators and sensors your equipment has, and/or the embedded microcontroller doesn't have a lot of memory anyway).
> But if the vector is small enough it will best the hash nap for actual time.
This will all depend on the size and type of object stored in a vector.
If you have a relatively small and flat struct that you are storing, then sure that will likely win. But if you are working with a collection of pointers, then the hash map will (almost) always win.
> Hard realtime code where predictable latency matters. Hashmap has a bad worst case
The hash map worst case is a linear search. It will be a lookup + the search. This also depends on the implementation. You could, for example, use a robinhood hash map which trades insertion times for lookup times.
A bad hashmap implementation will store collisions in linked lists. A better one will try and put them in a b-tree. An even better one will use a vector for collisions. And the most cache friendly version stores everything in the table and does probing on collisions.
There are specific cases where vectors are better, but those are the exception and not the rule in my experience.
It all depends. I was thinking mainly of small keys that are at most a machine word. But yes I conceded that for large keys (extremely unusual for what I do, thus I didn't think of that case before) a hashmap wins.
Also, while on a desktop class CPU any pointer chasing you do tends to dominate, that is not the case on small and medium microcontrollers (which I work with a lot). They have very short pipelines and their memory is all internal SRAM most of the time. Here classic cycle counting is still king, so you need to ask yourself if it is quicker to compare the key than to calculate the hash and compare hashes.
Code size in general matters a lot on embedded. Even a simple hashmap will always have more code than a simple vector. And a state of the art implementation with probing and tombstones (such as swisstable in C++ and hashbrown in Rust) will be way too large to be usable.
So, where do you draw the line? Do you accept having an OS? Because that is a huge dependency. So I assume you run directly on BIOS or UEFI? But even those are fairly sizable on modern systems.
>So, where do you draw the line? Do you accept having an OS?
Yes I accept using an OS usually, I guess there's much more than could fit on a single catchy sentence, but there's a clear policy.
Operating System is the biggest exception, for Windows it's pretty simple to carve out everything that is manufactured by Microsoft itself. But for the main Linux OS (Debian/RHEL), I include everything that is distributed by the main package manager (apt/yum) as allowed by the OS policy exception. (On Windows, this is equivalent to adding software packaged and signed by microsoft, like Git).
Alternative package managers like flatpak or snap are against my personal policy, not only are they very bloaty, but they kind of break the OS monopoly and push towards less safe supply chains, if it's not in apt/yum, then I don't use it.
> So I assume you run directly on BIOS or UEFI?
I have gone that route, but only experimentally, it's not very hard to get C compiled binaries to run and interface with keyboard and display through BIOS, but there's a lot of extra work that needs to be done incrementally, in order to use features in the sequence that they have historically been available, like 16 bit, 32 bit, 64 bits, 4GB memory. If you think of Wirth's law, this might actually be an effective long-term pacing strategy.
But I'm not that hardcore personally, not for lack of want, in a professional setting I'm pulled towards the pragmatic side and start conceding to stuff like using an OS, maybe using one or two packages. I would probably revisit booting directly to binary if any startup I work with hits a home run and needs to upgrade to at least 10k+ concurrent users. It's like one step removed from an ASIC, which is a stage almost no company enters, but I would have definitely have passed the baton at that stage, custom hardware is another discipline.
>But even those are fairly sizable on modern systems.
Well not BIOS, but UEFI and device drives certainly are. BIOS would just be some (mostly unwritten) standards on how to initialize, then it dissapears. Of course hardware itself is a dependency, and I'm definitely not going to be summoning computing from heat, sand, and electricity, but my line is definitely at the OS and above.
One final exception that wasn't mentioned is the programming language and its 'built in libraries'. I use the programming language along with its standard distribution. For Python (my main language), that means I don't use pip, but I might use 'import sockets' (it's almost the same as using cffi and glibc anyways). There's an analogue in almost all languages, node with npm, java with maven, php with composer, I just don't add those kinds of dependencies if I have control over it. I chmod ugo-rwx requirements.txt to avoid other engineers from adding leftpadisms.
That's not to say that it never happens, maybe even I imported Flask to meet a deadline, and maybe there's that perfect library from a good source that someone else suggests and it gets accepted, but it doesn't hurt to add some friction, it catches a lot of trash packages from being added to the foundation of a startup, which give almost no benefits at great expense over the lifecycle of the core.
The end result has a few advantages if I may explicitly sell myself and my strategy:
- Basal stack: Instead of having to hire yarn,yum,pnpm,react,tailwind,buzzword,jev,shadcn, developers (developers of very specific modern deep tech stacks that will only be used and popular during a very narrow timeline), if the startup grows, they have a much broader space to search talent from, which means that hiring can occur across other search parameters, MOST importantly business domain knowledge. I think it's much more productive to hire devs that are interested in dentistry if you are a dentistry startup, rather than devs that know, say, the intersection of Kubernetes, FastAPI, Kafka and some AWS Branded tech.
- Long term Foundation: Usually the simpler basal tech takes a bit longer to get started, as opposed to exclusively prioritizing delivery speed, but it allows for reasonable development speeds in later stages.
We sometimes call it tech debt, in this case the mechanics create debt by: increasing the Lines of Code count (you should count the lines of the dependencies you import), and by reducing the personnel system knowledge.
It is a tradeoff yes, but it isn't really a huge advantage to be able to get the first version up in 2 hours, as opposed to 1 month. Much less getting a first version up in 15 seconds with a prompt. I'd rather just work with someone that can spend like 5K in a 1 month prototype, and thinks about pacing for the long term winner-takes-all condition, rather than trying to save 5K and having a prototype in 2 hours, (or in 15 seconds). Nothing wrong with that, but it just doesn't seem like I would fit in there. If someone wants to setup their prototype in 2 hours, they don't need me, and if they ever call me, we are going to be building the 1-month prototype from scratch, and using the vibecoded or Framework prototype as a Proof of Concept, not an architectural base.
- Proprietary advantage/Moat: Haven't seen this mentioned elsewhere, but code is the main asset of a company, if you import a lot of code to achieve your goal, sure you accomplish that, but you have not accomplished a very distinctive or defensible advantage. Strategically it's more profitable to pursue the kinds of challenges you need to write code for, than the ones you need to import and glue together components for.
- Strategic conditioning: Similar to the above, but you want to think and write code at the same time, if you use someone else's framework you think according to their terms, whether that is to confer them a strategic advantage, or whether the goal of thinking is to reduce development costs. The kind of thinking that occurs in software development should ideally be as free as possible from the influences of actors that are not goal-aligned with the company's mission.
Opinionated software is great, but ideally other software should be incorporated with a commercial relationship in the middle, carved out by some other exception. And even then, their goals should be acknowledged strategically, if you are building, say: a DRM or surveillance system on top of Debian, that's bound to have some sort of conflict. Similarly using Microsoft for a public hospital project is another conflict that is worth at least taking note of.
- Personnel knowledge: I pointed to this, but the knowledge of personnel of the system AND the business domain is relevant. The objective of making software is not JUST to achieve the end-goal, the knowledge acquired in the process of creating that software is a valuable byproduct for both operational and strategic reasons. Similar to writing an essay, it's not just that we want to have the essay in order to slap it into someone else (well maybe sloppers do), but that we actually gain insights and develop our own thoughts while we write it.
- User/Producer separation: For the cases where the product is about LLMs or DevTools. If you both consume dev tools and build devtools, or consume LLMs to code and are building LLM features, I find that people tend to get lost in an unhealthy spiral more often, it's a dangerous task to endeavor, if there's ever a time not to use LLMs to code, it's when your product actually includes LLMs as a feature for users. Which is even a more important goal, right? What's more important, putting this super technology at the hands of the user? Or consuming this super technology to accelerate development?
---
Regarding costs this is not particularly expensive either, it's not like you are missing out on a particularly valuable revolution if you miss out on abusing Open Source supply chains or on abusing vibecoding thingies. An idea-stage startup that hires me to build an MVP would cost like 5K and a month, maybe if they really want to polish it, that's 6 months which is like 30K, plus the (co)founder's salary, whatever that is, as it's not hands-off work.
I'm sure to some the idea of saving 29K and spending 1K in AI subscriptions is enticing, maybe they can even attempt vibecoding before hiring talent, for sure, that's valid. It even helps refining and conveying the idea, and getting more accurate cost and feasibility estimations. But it's also a perfectly skippable step, that bootstrapped investment route has been used for decades now.
Depends on your use case. If i want to export some graph data structure from a program to visualise it for debugging purposes, graphviz is way more relevant than doing it by hand in drawio.
But if I'm making a presentation for a conference I would choose differently. And in between you get a whole spectrum of tradeoffs.
An alternative way to think about it would be a padlock and a key. Anyone can close the padlock (assuming the common spring loaded design), but only the key holder can open it.
While this works for encryption, it doesn't really work for explaining signatures or however. Maybe someone can come up with a good analogy for that case.
That's the analogy I was taught in college. The public key is really a padlock that had infinite number of copies, but there was only one key that could open all the padlocks. If someone wanted to send a secret message to the person who owned the key, the would obtain a copy of the padlock, put their message in a box and lock it with the padlock, then send the box to the key owner. The key owner was the only person who could unlock the box and read the message.
Pattern types could potentially maybe in the future allow for the compiler to know more details though. They are a nightly feature, and afaik only for enums, integers and pointers so far. The idea would be that you can define a custom type such as "an integer between 7 and 45" and everything else become niches for niche optimisation (e.g. for `Option<MyFunkyInt>` some of those impossible values would be used to represent the None case of the wrapping Option).
But I could envisage a future in which you could say "f64 without NaN" which would both make those available for niches and potentially tell LLVM about this. However, we are very far from any of that currently. And it might not be what you want, since you would need to add checks when you perform operations to ensure the value doesn't suddenly become a NaN. Which is way more complicated than ensuring integers don't become, say, zero. It will likely be much harder to optimise away the checks.
reply