Syscall marker

There’s the Allocator rule that we wish to maintain in our code. One aim, among others, is to have a clear view and control of some performance characteristics of our programs. In other words, avoid hiding costly procedures. I wonder if, for the same end, procedures that make syscalls should not also be marked?..
Allocator pattern is not enforced by the language, I know.
But do you strongly believe that dynamic memory allocation tactics strongly differ in importance from management tactics of all other resources? After all, a syscall is essentially different from a function call, but it is impossible to tell from the code.

What patterns could we use to maintain the same level of clarity for all syscalls?

1 Like

This is the Rust way - you have to use unsafe blocks to do a syscall.
But wait… if a syscall is unsafe in some sense, to whom we are going to tRust? :slight_smile:

I’m not so much concerned about their safety. Zig is quite liberal in this regard. I’m considering the overhead of such functions. And maybe, consequently, what’s the actual essence of the allocator pattern that has become one of the bulwarks in Zig community.

According to this, malloc is far from being the most costly operation. While it is, I presume, very susceptible to abuse, it might be that it amounts to more than 20 mallocs to have the same impact as one write call. Yet in Zig, it is not marked in the same way allocations are.
Even worse – it is actually actively hidden, as a side-effect of encouragement to use the writer pattern.

There’s a tension here that I can’t quite resolve.

While the argument that “well, a comment should suffice” or “skill issue” might arise, I fail to see how it is not entirely, if not more, applicable to the allocator pattern. If we consider the abstraction encouraged and provided by writer, these arguments do not apply at all.

(bear in mind, I’m basing this more on theory and googled-out benchmarks and I would implore a regard on this question that’s more based on actual practice.)

Yes, syscalls are interesting as they relate to allocation. The issue here though is not every call to an allocator will make a syscall - page_allocator is a very direct path to that route while the FixedBufferAllocator may not make any syscalls at all if you give it stack memory.

Interfaces have this problem - they hide implementation details (by design). So at an Allocator level, getting syscall information externally is by definition not possible in my view.

You may be already aware of this, but malloc doesn’t always make syscalls either. In the following implementation, there’s memory caching and free-listing going on behind the scenes… it’s actually quite complicated: glibc/malloc/malloc.c at master · lattera/glibc · GitHub

To achieve the level of control you are looking for, you’ll probably have to abandon interfaces entirely.

1 Like

Then just do not use “writer pattern”, it’s interface is muggy (it’s my personal observation, nothing more). :slight_smile:

1 Like

allocations at OS side are always syscalls (brk, sbrk), not matter what malloc or any other more kinda advanced allocators is doing inside themselves.

Correct, they are. My point is that if someone is using malloc as a baseline for understanding/measuring syscalls, they need to reconsider their approach. As an aside, I’m not saying that OP is directly doing this (again, they could be well aware of what goes on inside of malloc), but this is a typical confusion people have if they’ve never looked into the implementation of malloc.

I think OP meant that we pay a lot of attention to allocations, but don’t pay nearly as much to syscalls, while syscalls are actually the most costly of the two. I agree with OP.
One other very important thing that goes agaisn’t Zig’s “no hidden control flow” mantra is abstraction. Any abstraction will necessarily make some decisions and hide some things for you. In a way, this works for Zig. I believe most of us here agree that modern software suffers from too much abstraction. But at a certain point, you need some abstraction. Even if you’re not using a library or anything, there’s got to a piece of you code that you whish was “hermatically closed”, so others wouldn’t touch it willy-nilly, but only through the interface that you defined. This is usually considered a cool development, like the code is “maturing”. But, in so doing, the creators have to enforce users to interact with the code in a particular way, that probably won’t be the most efficient at every corner.
Personally, I think we should focus on letting people write the fastest code possible. This means that libraries should expose few things, and we should be encorauged to touch the library’s inner parts. A sort of “loose” abstraction. This would preserve the performance gains from using libraries, but when the library inevatably doensn’t fit well with the code, we adjust by touching it’s private parts.
Back to the problem OP suggested. I think the best way to handle would be to make a doc comment saying if a function “is guaranteed to syscall”, “has a chance to syscall” or “no syscall”. We could take this one step further. Consider a function that reads some data from a file. IF we gave them a parameter called “Scheduler”. Every time the function had to block (like reading a file), they would signal the manager, The manager can switch the task with something else while we wait for the response to message to arrive. Now we have transformed the ideia of “may block” into something that have specifics actions, by the scheduler. In so doing, we get back the “no hidden control flow” and keep all of the perfomance of just doing things manually with no abstraction.

2 Likes

There’s a lot of things here we totally agree on.

A lot of code these days is bubble wrapped and punishes any off-roading. I personally like when the code exposes the internals through some conduit that you can use directly.

The scheduler parameter idea is pretty cool and parametrization usually increases flexibility.

In general here, I’m with you on your philosophy about letting people figure out what is a fork and a toaster and how they relate to each other. The alternative is to live in a world where making toast would incur an immense amount of machinery to protect people from what’s obvious (call it a toaster abstraction, if you will).


Here’s where we disagree. In my mind, Allocator is already a maybe syscall. Everything that uses an allocator carries that label by extension. I don’t see how adding on an extra label further clarifies the situation. Like, ArrayList for instance - does append need that label? But we already have that - appendAssumeCapacity takes care of this issue.

I’ll agree on your point about “guaranteed to syscall”. If something is absolutely going to make a syscall, that helps me know where to play defense.

1 Like

So off the top of my head, I see this:

fn foo(allocator: Allocator, kernel: Kernel) ...

where Kernel is akin to Allocator but for making syscalls instead of allocating. But then this list could keep growing with other things that hide control flow. So maybe:

const System = struct {
    allocator: Allocator,
    kernel: Kernel,
   ...
};

fn foo(system: System) ...

Kind of reminds me of Roc’s concept of a platform to handle all things related to external stuff and side effects in their programming model.

3 Likes

I believe Odin does something like this too? A Context which is automatically passed around.

3 Likes

Yeah I remember something about that too. But I imagine that the context would then be heap allocated behind the scenes somewhere.

There’s important qualitative difference between dynamic allocations and syscalls:

With the allocator, it’s not about avoiding using memory at all — you still need to allocate something somewhere, it’s just that you often can make these allocation decisions in a more static way, not needing the full power of a dynamic malloc. Instead of passing an Allocator in, it’s often enough to pass an output buffer.

With syscalls, you can’t generally avoid them — gotta read that file one way or another.

That being said, yes, it absolutely is helpful to be explicit about where and which syscalls happen. So, passing something like io: *IO throughout the relevant parts of the program might be a good idea.

Though, often you’d like to abstract on way above syscalls. In TigerBeetle, what we pass is not raw IO, but rather Storage and MessageBus – the two components that internally use syscalls to access the disk and the network.

A related idea is that the standard blocking syscall interface is just a poor abstraction:

Hardware doesn’t work that way, everything is evented throughout the stack. Which is another reason to eschew traditional ambient authority blocking IO in favor of explicit passing of an IO parameter which separates submitting IO work from waiting to completion.

8 Likes

it’s always exciting to come upon your posts.

could you expand on what you mean with “blocking interface is a poor abstraction” in relation to concurrency? I find it difficult to connect to what’s said in the link you provided. these terms are not as deeply established in me yet.

edit: oh, it seems Andrew is of a similar opinion. and it was posted on the same day haha, I feel exalted.

1 Like

Same here! I’ve read that post like five times already, and only on that last reading last week I actually got what “ The sequential lie: our dumb history” section means.

Let me try to explain this. If you think in data oriented way, in what actually happens in the software and in the kernel, you’ll realize that there’s no such thing as “blocking IO”. Nothing ever blocks! If you program does a “blocking” read syscall, the OS swaps the thread and the CPU continues to happily execute something else.

Physically, all IO is a combination of:

  • submitting an io request
  • checking if a request is done
  • getting notified through an event that the io is completed

“Submit io & wait until it is done” is an abstraction provided by an OS. And, as is the case with many abstractions, it often is just a wrong abstraction. Very often you want to do things like “write two files concurrently”, but it is hard to do simply because OS APIs get in the way. In reality, you can not not do it concurrently. In practice, only the blocking API is usually available.

6 Likes

Ohh, that’s spicy :hot_pepper: ! Love all of that, thanks for the link!

I don’t think this is entirely true. See the old UART and TCP head of line blocking. Sometimes you have to block.

When they say “blocking” they probably mean that your program can not continue until returning from a syscall, i.e. it is blocked in some sense. When operation can not be performed immediately, a process puts itself into SLEEP/WAIT/BLOCKED state, this happens inside OS code which executes on behalf of a process. Some time later (or never :-)) OS puts this process into READY state, then it chooses it for execution - return from syscall.