The removal of std.Thread.mutex and std.Thread.futex from the synchronization primitives saddens me

This is obviously valuable for library authors. It will be valuable sometimes for application authors, but less obviously and less often.

I think for a “replaces C” language, these are legitimate concerns. One of the things C really got right is exactly an abstraction (not much of one! but an abstraction) across operating system primitives.

I’m on team “let’s see how this works out”, around Io generally. As far as the kitchen-sink effect is concerned, it’s tracked and that’s enough for me.

As a sanity check, I built a little CLI tool which uses some zg data, under ReleaseSmall, before and after Zig 0.16, using juicy main. Before is 273KiB, after is 375KiB: a 102KiB ‘penalty’, if you want to call it that. Before, I had a pretty small program, after, I have a pretty small program, just, not quite as small. About 150KiB of both is just data tables, but I would expect that hundred K to be a constant: a bigger chunk of very small programs, and a smaller piece of bigger ones.

Something I could translate as “removing the cross-platform primitives makes Zig less ‘C-like’, and I would like the option to continue programming that way”: I think that’s worth bearing in mind. Maybe it fades, maybe it doesn’t.

You got some good answers to “why not comptime known”, but I hope, at least, that the “secret third thing” option will be seriously considered.

It’s possible that “we’ll add restricted function types and that will get the dispatch penalty back” is going to work. It’s not clear to me that this is the solution which really cuts the problem along the joints. Using a “pattern” for runtime polymorphism is maximally flexible, but it has one obvious downside: you can’t actually tell the compiler what you’re up to, so it has to optimize in ignorance of that intention.

Finding a way to introduce the type system to what we’re doing has, in the past, proven elusive: many proposals, none good enough. But we’ve gained more experience with how to do it by hand, and what the consequences are, and maybe the question should be revisited using that experience.

2 Likes

I’m skeptical, especially if the goal is to move most operation to Io.Operation which adds another layer of indirection, that needs a different mechanism for dead code elimination.

2 Likes

I share the skepticism, but the core team is also very good at what they do.

It seems simpler to me to tell the compiler: “this is a definition of common behaviors with consistent function signatures, which a struct may implement: how this is translated to code is up to you”. Rather than try to tackle that one signature at a time, especially (as you mention) through an arbitrary number of indirections through a literal struct of function pointers.

But we’ll see. Losing the @fieldParentPtr trick isn’t an option here, as I see it. So the solution would need to be sophisticated.

4 Likes

It is being actively implemented, they seem to have been expanded to restricted pointers[1], that already have about 76% less[2] ‘penalty’ with just the basic implementation.



  1. That might just be for development, or could change for other reasons IDK. ↩︎

  2. Definitely doesn’t represent all cases, only one test case was shared and only the numbers, it’ll vary a lot depending on how much of the API you’d use too. ↩︎

3 Likes

Awesome! I still think, maybe, this calls for… syntax. A type-of-type, perhaps.

But that question can stand for as long as it needs to. It’s good to hear that implementing restricted pointers is panning out.

1 Like

I second that.

fn GoodIo(comptime Data: type, comptime vtable: Vtable);
const RuntimeIo = GoodIo(*anyopaque, old_io_vtable)

RuntimeIo is exactly the Io we have today. If code bloat is a problem for you, just use RuntimeIo, solving both your concerns. My guess is that most people will have a single Io, in which case, instantiating GoodIo a single time would have the same cost as compiling RuntimeIo, and you would benefit from one less layer of indirection. If you need more than one Io, you can decide for yourself whether you want runtime dispatch or compile time dispatch, each with their pros and cons.

I hate to be the nay-sayer, but here goes:
Many people believed this about parameter reference optimization (PRO). Turns out, the reason why other languages have not implemented that optimization yet is because it is unsolvable, at least with the parameter semantics we have in C (and Zig imherited). After all that time being one of the really cool selling points of Zig, we’re back to passing parameters like we did in C.
The C++ community has invested a lot of energy in devirtualization, and it is still far from great. I trust Andrew and the Core team are really smart people, but so are the people writing C++ compilers. If they haven’t solved that issue yet, trust me, it’ll take more than year for Zig to solve it, if ever.

2 Likes

If there was a comptime interface with “anytype”, you could still pass a vTable to it. In fact the more I think about it I realize that the set of programs that can be expressed with a comptime interface is a direct superset to the set of programs we can express with the vTable interface. You could also store vTable versions in structs and pass those around to functions, or even a tagged union.

In regards to binary size - the comptime suggestion would solve the binary bloat regression because unused functions wouldn’t be generated.
We compare that to a binary size increase by function duplication (which we already have accepted when we use generics). If that was measured to be significant in a project, you could pass a Vtable / tagged union version.

1 Like

Maybe C++ has defaults and features that make devirtualization and related dead code elimination very difficult? For example (pardon me if I’m wrong here, I’m not a C++ expert), because of multiple translation units and open class hierarchies, when an object is emitted some virtual function implementations might not have been analyzed yet. My understanding is that this is more or less what LTO and things like whole-program-vtables and force-emit-vtables try to address.

With single translation unit and a way to say “an assignment to this function pointer must be a known function” (some kind of “sealed” pointer type), and by tracking the set of such functions during analysis, it seems like this becomes doable. If the resulting set contains only one candidate, then full devirtualization can happen (and that in turn makes it much easier to see that the function assigned to this pointer is never called and could be eliminated). If it contains two or more, a backend could still use this information to improve codegen (for example, maybe LLVM !callees could be used?).

Anyway, we’ll see where zig is in a year :slight_smile:

1 Like

If anything, C++ has features that should make devirtualization easier. It has language level support for virtual functions. The open class hierarchy was supposed to be addressed by the final keyword. And, like you pointed out, LTO should be a really good tool for this. And yet, compilers can’t reliably devirtualize.

Let me provide a non-Io counterpoint.

I do both microcontrollers and Vulkan (thankfully, normally not both at the same time). I found the Zig abstractions for Mutex/Condition/Futex annoying in a lot of ways. I wouldn’t say “broken”, but definitely surprising sometimes. And it’s really annoying to paper over the differences between Windows threads and *nix threads and microcontroller threads. And trying to sync with a GPU and not completely waste your performance is … interesting.

As was pointed out, it was work. It turns out that it is tricky work. And it still had weird bugs.

Don’t get me wrong. I’m annoyed that I have to fully pick up the work for Mutex/Futex/Condition, but I way already leaning that way anyhow.

However, nothing is stopping anybody from extracting the old code, writing this as a library, sharing it and having other people use it. (See: BoundedArray)

Mostly for sophisticated users who were the most likely to have to take the reins and beat this stuff into submission anyway. We’re systems programmers; we’re expected to sometimes have to do savage, unspeakable things that normal programmers don’t have to think about.

1 Like

Not everyone using Zig is a systems programmer.

And Zig wants to be a general-purpose programming language.

For example, I developed an OS-independent distributed reporting architecture which uses nulti-threading and mutexes, controls several OS processes, and ofc IPC and networking, in Python. Developing sth like this wouldn’t be possible (with reasonable effort) without Python’s excellent standard library.

I would like to see a similar “batteries included” for Zig sooner or later.

Ofc, on MS windows I can use the windows APIs, and in rare cases my code does it (or Linux fcntl for example).

But if a language ecosystem wants to be general purpose, it needs those abstractions.

2 Likes

This is a strange statement. I’m not aware of any bugs in the mutex/condition/futex wrappers in Zig 0.15. Do you know of any?

Your post seems to go in the direction that if something is not universally usable, it shouldn’t be in stdlib. Then perhaps std.Thread should also be removed? After all, the code could be inlined into std.Io.Threaded.

3 Likes

Probably? But isolating them would be difficult. Multi-threaded Vulkan code is never ending pain from multiple directions. All I know is that my code got more reliable when I removed those abstractions and took on the burden myself. However, I’m very reluctant to throw an axe solely at Zig given that writing multi-threaded Vulkan code is living in a target-rich glass house.

I’m probably more sympathetic to that position than most people here would be comfortable with. I consider a Thread abstraction very similar to a String abstraction; every one is its own special little snowflake with no two ever alike.

I’m having trouble coming up with a scenario where I would have a Zig-level Thread abstraction but not also need a Zig-level Io abstraction (and conversely where I can’t implement one where I could implement the other). Unfortunately, Threading and Io at bottom varies greatly between Windows, macOS/iOS, Linux, and Android. Any “abstraction” over those either winds up very anemic due to being least common denominator or the abstraction leaks like a sieve and you wonder why you abstracted things in the first place.

Like most programmers, I like having a “default” implementation for certain abstractions (and obviously the Zig compiler needs these abstractions). However, unlike most non-systems programmers, I’m not afraid to discard the default, wield a machete through the underbrush, and forge my own path through the jungle.

3 Likes

I can completely agree, and I think IO in general will only be beneficial, First because not 100% of a program needs to have “perfect” performance, that’s a certainty, and while IO might never become THE optimal solution everywhere, it still gives you close enough in most cases, and especially at scale.

The true benefit of IO in my opinion is not optimality, but code reuse, I can most certainly attest because I basically rewrote a subset of libcamera exactly because there was no practical way to make it fit the IO model that was optimal for the project i’m working on at work, which is a bloody shame, because I couldn’t reuse all the work and sophistication that went into it, simply because it made my IO really sub optimal, and my scheduling 10x more complex than it needed to be.

So yeah, people shouldn’t be affraid to go as low as need be, and I don’t believe it goes against a general purpose abstraction such as IO, because the strongest benefit is to be able to pick any library, and just stick in your own IO or the one that makes the most sense to you, and you get close enough to really good code everywhere.