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

Marketing has always been the toughest part. Doesn't matter what you invent in private if no one sees it.

Nice job!

UB according to the standard committee is "we didn't think of it". It's not literal UB it's well known what it compiles down to, every time. (.loop: jmp .loop)

It's not "we didn't think of it", it's literally "the standard has nothing to say about it", which means that any standard-conforming implementation is free to do whatever it wants, meaning that different implementations may handle it differently.

> It's not literal UB it's well known what it compiles down to, every time. (.loop: jmp .loop)

That might be true for a particular version of a particular compiler, but if you assume that it's true for all standard-conforming compilers (now and in the future) then you're making an assumption that is not supported by the standard.


> It's not literal UB it's well known what it compiles down to, every time. (.loop: jmp .loop)

...Uh, the example shown at the literal top of the blog demonstrates precisely the opposite?


It's catastrophic actually. Like disastrously catastrophic. It started with C++20 mostly, and has only kept getting worse from then. See zero initializing variables by default (WHY?) compare/meta including half the STL and HARDCODING those symbols, std::initializer_list being in the std namespace (if you don't include <initializer_list> you literally can't use it, and there is no such thing as a __initializer_list or some internal symbol), the entire coroutine library where you MUST provide coroutine_handle, noop_coroutine, suspends et al (coroutines aren't that bad because they're not necessarily spaghetti).

<meta> is the single WORST OFFENDER, where they hardcode std::vector (literally std::vector in the std namespace) std::ranges std::allocator.


I can't find anything saying variables are zero initialized by default in C++20. But the reason to do so is obvious: many bugs are caused by the lack of this, and as long as you can opt out with "= void" or something, it's not violating C++ core principles.

They were saying the problematic philosophy started in C++ 20, not the variable initialization rule.

Yes the reason is obvious, but it’s neither simple nor black and white. One huge problem is that this can cause serious performance regressions, and you have to change your code to opt out, e.g. add “[[indeterminate]]”. There are many, many cases in high performance computing where the intended & desired behavior is don’t touch my variables until I fill them.

This is changing C++ core principles, there’s a new designation for the state of a variable: erroneous. It’s also subtle and weird, because you can still have well-defined behavior even with erroneous state. It does seem like this might be an experiment though, I don’t think this is the end of the story. (It seems they’re already talking some redesign of this idea.)


What I'm most annoyed at with the variable initialization change is that:

  - It's potentially a performance change in every single function, especially ones that have sizable fixed-size buffers
  - If you have regressions you have to spray [[indeterminate]] everywhere, because there is no coarser way of suppressing it.
  - While the language says unrecognized attributes are ignored, compilers frequently warn on unrecognized attributes. Clang, for instance, currently warns on [[indeterminate]].
  - There is no defined macro name for backwards compatibility.
Which means that libraries are going have to all declare their own macros for [[indeterminate]] and pepper their code with it.

Uninitialized variables were already UB to read, because some architectures have trap representations, even for integers. Every register on Itanium has one.

That's assuming you were reading it without writing to it. There are three common cases when that isn't true.

The first is that you have a fixed buffer large enough for the maximum message size even though the typical ones aren't that big. You most often write 1% of the buffer and read it back, the other 99% is never accessed.

The second is that you always write the entire contents before reading it but the compiler may not be able to see that.

And the third is that you have a code path where that variable is simply not used.

You would then have the compiler emitting instructions to write zeros that are either overwritten before being read or are never read at all.

Moreover, zero initializing the data doesn't actually remove the bugs when that isn't the case. Consider the first case when you mess up. You have a fixed buffer used to store variable length messages. For the first message the buffer is now zeros instead of uninitialized, but for every subsequent message the remainder of the buffer still contains the remainder of the previous message and subjects you to information disclosure or data modification if you're reading back a different amount than was written in the associated call.

Now consider the second or third case. You unintentionally read from a variable before assigning to it. You get zeros instead of uninitialized memory, but if you weren't expecting zeros, well, the UID field is now 0.


If you are writing to it before reading from it then it's not uninitialised, and in 99% of cases the compiler can see that and will not set it to 0 at its declaration because that's a dead store.

Consider this (quite common) code:

  char buffer[LARGE_SIZE];
  if(maybe_fill_buffer(buffer, sizeof(buffer))) {
      use_result(buffer);
  }
The function maybe_fill_buffer() is an external library function that either fills the buffer and returns true or doesn't access it and returns false. Or maybe it unconditionally fills it, or unconditionally returns false without reading from it. The compiler can't see any of that though because it's in an external library. For all it knows that function is going to read from it instead of writing to it.

Notice that if it could actually figure it out 99% of the time then it could also emit a warning the 1% of the time that it can't and encourage you to make an explicit choice, which would have been a better option if that was actually the rate.


That's not reason enough to have the compiler initialize.

Zero initializing also hides bugs.

Say you have some code that should not be reading the initial state and is buggy if it does. Without zero-init, valgrind and msan will give you an immediate and false positive message that your code is wrong-- or forget dynamic analysis: the compiler can often statically tell you that the code will use an uninitialized variable. Zero initialize it and you lose that signal.


> See zero initializing variables by default

Strictly speaking the standard only requires some pattern that is not tied to program state. Zero works for that, but so do other static patterns like 0xABAB... or the like.

> (WHY?)

The motivation section of the corresponding paper [0] might be interesting. tl;dr: it lets wrong code be wrong without suffering from (all) the consequences of full-blown UB.

[0]: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p27...


In other words, it's a sane default that you can opt out of on a case by case basis which is the way it should have been all along.

I’d guess the concern is performance, not what initializer value is used. And performance is a valid concern that is discussed in the proposal, and a reason there’s an escape hatch. Still, it might cause some confusion.

AFAIK zero-init is unfortunately the default compiler use (but you can change that)

D initializes floating point variables to NaN by default. And chars to 0xFF. Yes it's controversial!

The language is already littered with these "the compiler shall insert" and then a reference to the STANDARD LIBRARY FEATURE N.X. Which means if you're compiling in a freestanding environment half the time you'll get linker errors such as "couldn't find symbol whatever". And what's worse the compiler inserts a call to a function that is LITERALLY STD NAMESPACED. Meaning you have to provide that signature yourself. See how std vector is hardcoded into compare/meta and I can't remember what else.

This then forces developers to create undefined behaviour because according to the standard you can't namespace std your own functions even though it's required to get it to work.


Freestanding environments specifically don't have to do this, they get a carve-out. (I dunno if this applies elsewhere though.)

Sounds like a compiler bug that a standard feature implementable in freestanding doesn't work in freestanding.

This is all publicly available anyways, who cares? Also you're practically consenting to it when you run an agent locally

Why? Writing a memory allocator is quite simple, and I'd argue that _everyone_ should write one from scratch for any kind of high performance application. It's also trivial to outperform general purpose allocators that have to satisfy countless constraints. I've written numerous special purpose mallocs that are a) both provably (formally) safer than the standard armada and b) significantly faster (>10x throughput).

Your experience writing memory allocators is irrelevant. The point is that jemalloc is widely used and that’s why it makes sense to be aware of it.

Writing an allocator is simple, you’re correct, but writing an allocator that doesn’t suck is not simple.

I'd happily see performance, latency and stability of your allocators in massively multithreaded, long-living programs with workloads where hundreds or thousands of parallel threads continuously create and destroy short-lived small and medium objects.

Writing allocators for domain-specific access patterns is easy. Writing a general-purpose high performing, stable allocator with bounded P99 latency is hard.

Give your friend, Dunning–Kruger, some better pills to keep him from speaking through you.


You're correct, but his point is that you don't need to solve the generic problem. Solving the generic problem is very hard. Grug doesn't like solving hard problem. What does grug do? Solve five easy problems. Make an arena for the short-lived objects, reuse the objects, use generic multithreaded malloc for the rest. Grug happy.

I guess the point was that before you consider using a different allocator you should rule out a custom one.

And that's rather hard, because a general purpose allocator makes all decisions based only on the requested size. This is a very simple interface and such a tool is worth having. But a custom allocator can both bake in a specific scenario and provide more nuanced interaction.


>massively multithreaded, long-living programs with workloads where hundreds or thousands of parallel threads continuously create and destroy short-lived small and medium objects.

My first thought would be to use per thread pool allocators.


>where hundreds or thousands of parallel threads continuously create and destroy short-lived small and medium objects.

Should one even want a global, general purpose heap allocator for that? Seems like a crazy idea to even consider.


Not everything needs to be general purpose. Allocation can be as easy as bumping a pointer, and it's hard to beat that.

> Will these models eventually replace all knowledge work, leaving lawyers, doctors, product managers, software developers, and others out of a job?

Effectively yes, in the current forms. Those professions will likely evolve, but the traditional forms (ie writing code by hand, writing law filings by hand etc) are all dead.


> Those professions will likely evolve, but the traditional forms (ie writing code by hand, writing law filings by hand etc)

there will still be writing initial and incremental prompts by hands, until and if LLMs surpass humans in all intellectual functions.


in that case, legal cases would just come down to who has more compute lol. Many times cases win on their merits, but we've also seen evidence where overwhelming legal pressure can influence cases.

bigger spender already has a huge advantage, up to millions of dollars

this is more likely to democratize the legal system by reducing the cost of a good legal team


May I just note that there are other jurisdictions on this planet that are less money-biased than than the US one but will be disrupted by law-LLMs as well?

It seems much more probable to me that these LLMs will make good things worse than that they will make bad things better.


Everywhere a good lawyer costs a lot of money.

where does more money to pay a better legal team not help?

Dead wrong. Win32 (externally) only seems stable, but internally it changes between Windows releases. Win7 syscalls are completely different from Win11 syscalls, meaning if I want to release a binary _without relying_ on Win32 I need to provide full syscall mappings _for each and every Windows version_. This doesn't happen on Linux.

> only seems stable, but internally it changes

That's literally the definition of it being stable. Programs written against an interface keep working despite the implementation changing. The Linux kernel also constantly changes internally but programs written against syscalls keep working, so it is stable; that fact doesn't stop being a fact just because I dislike perf_event_open(2) or whatever. This is all very basic and easy to understand.


>> Win32 is the most stable abi on the Linux desktop.

> Dead wrong [...] if I want to release a binary _without relying_ on Win32

Then you are not using the Win32 ABI, are you?


Those are not a part of the API contract in case with NT kernel, though, unlike Linux.

Also, there are OS-provided shims in ntdll.dll (which, by the way, isn't a part of Win32 platform API, but a part of the NT kernel interface).


> If you're going to make apps in windows, you need to call their proprietary API somehow. Maybe you do it via a wrapper library, or via electron or something. But that's the same thing, just with more indirection.

Not even close to being true. You can invoke syscalls directly, just needs a bit of reverse engineering. I wrote a bare metal libc library, with (not a whole lot of) effort I'm fully able to interface with the kernel/open windows etc. Fully statically linked, no libc, no win32, compiled on Linux executed on Windows.

The problem is this isn't really well documented _at all_, and I even ended up attempting to get in touch with the Windows kernel dev team to give me the actual internal syscalls/endpoints, but they refuse to cooperate. Which is why writing anything for Windows is entirely pointless.


The problem is much deeper than that. Most OSes' syscall ABIs are not stable and could change without warning. What is stable is the dynamically-loaded libraries, shipped as part of the system. Linux is the notable exception here; the Linux kernel project doesn't ship a libc, and Linus is very famously opposed to "breaking userspace."

There's nothing that can stop you from using syscalls in theory, but if you want your app to be portable across different OS versions, past and future, you'd better not.

Incidentally, syscalls would also break Wine. The way Wine works is basically by shipping their own versions of Windows DLLs, which express their operations in terms of Linux APIs. Because Windows programs don't rely on syscalls, and call all system functions via the system-provided libraries, the Wine loader can just link Wine's version and let the program work normally.


If I recall correctly, the Golang team got bitten by this on MacOS.

They initially implemented the Golang runtime directly on top of MacOS syscalls (not the C runtime library), just like they did on Linux - and then those syscalls changed, breaking Golang.

They had to switch to the official stable API which on MacOS is the C runtime library, not syscalls.



That’s insane. Windows does not have a stable syscall ABI. The way you’re supposed to interact with the kernel is through the userspace library. Of course the kernel team refuses to cooperate.

Do you want to keep reverse engineering the syscall ABI for every Windows edition and update ever? Do you want to ask your users to disable Windows Update?

Regardless, I don’t even understand how that’s relevant, since you’re still introducing a dependency on a proprietary ABI.


> This isn’t even close to being true. Here’s a thing I did that made things way more complicated than is worth it for 99% of developers when there is a proprietary solution made so I do not need to worry about these things. Because it is so hard to work around it, it is entirely pointless to develop for one of the most used operating systems in the world.

Just being totally honest this is how I read this comment when I insert context that seems important to me. I respect having principles but at some point there needs to be more value in practicality over your codebase not being locked into a proprietary framework at all.


> Not even close to being true. You can invoke syscalls directly,

The windows syscall API is yet another proprietary windows API. Sure - you can call it without loading any DLLs. But you're still calling into a proprietary windows API.

If you really hate calling proprietary windows APIs that much, maybe stop developing for windows? Develop software for linux. Or make your own kernel, or whatever. But if you keep developing software for windows, stop fighting it. Unless you have a very good reason, your software should try to fit in on its host platform. It should behave well, and work like other windows software.

It's like travel. If you fly to France, try to fit in. Maybe learn a bit of French before you go. If you hate France, don't go.


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

Search: