Why are we both-sidesing this and blaming the poll aggregators when they are the ones that successfully rooted out the fake poll data?
I think a big problem is that, as a poll aggregator, the more you pick and choose which polls are allowed to be aggregated, the more risk there is that you are inserting bias into the result through selective exclusion. Erring on the side of broadly accepting poll data means less room for bias. But they do talk about the need to raise their standards due to the increasing ease of producing fake data in this podcast with FiftyPlusOne: https://www.youtube.com/watch?v=Neuot3aMkrY
Why does everyone love being able to control the allocator in Zig? I rarely want this low level control over the allocator, I would rather have an easy to use interface to malloc/free (such as RAII) or would prefer the language push me to avoiding allocations when unnecessary. C and Zig users are always talking about how arena allocators are easier to use than the alternative but I disagree because the alternative is extremely easy to use when the language makes it so.
Arenas have a lot of footguns like growing out of control if you don't have space in your logic to delete them (e.g. suppose every new HTTP connection required obtaining a new allocation from the primary arena -- now you have a memory leak). They also aren't necessarily faster than malloc: I remember hearing a case where mimalloc was faster than an arena for a one-shot program like a linker because reclaiming space improved cache locality. Plus it's difficult to implement things like growable vectors or hashmaps using an arena without wasting space.
I just don't see how "futz about with the allocator constantly" and "use an arena" is actually a good default.
It is a good default for lib code because it makes that code truly re-usable. The same is true for great C libraries, they let you provide a block of memory instead of calling malloc inside. The library should not get to decide if/when allocations should be done.
In Zig app code you don't have to pass allocators all over if you don't want to, that's your code so do what you want. But for libraries it's the right thing to do because it puts the caller in control.
The reason you have to pass allocators instead of making this implicit like in e.g. Odin, is because Zig just takes a firm stance against implicit logic, firmer than any other language I've ever used. The end result is that people who like Zig tend to like passing allocators around even in their own code.
The library developer doesn't know what allocator will be passed, and the library user doesn't necessarily know how it will be used. They share an interface, so it will [probably] work, but it might not behave (mem/perf wise) as either expects. ArenaAllocators in particular don't play great with ArrayLists or nested ArenaAllocators.
I don't think it's a huge problem, but it's more subtle than an explicit allocator is a better because it provides more flexibility.
Zig seems to be laser focused on systems programming domains where there is value in keeping unusually tight control over everything the program does. Aside from tinkering with Arduino, never been me. If it were, though, then I would probably not want to use a library that wants to be able to arbitrarily grow ArrayLists. I would probably want to structure my program so that it acquires pages of memory (actual memory, not just overcommit) in as few places as possible - ideally one - and from a location very close to the entry point. And a library that doesn’t play nice with that wouldn’t be much more palatable than a library that wants to bring its own memory management runtime along for the ride.
I don’t think that’s totally unreasonable? It’s more restrictive than what most of us are used to, sure. But it’s also quite a bit less restrictive than completely banning dynamic heap allocation as is common in aerospace applications.
> Zig just takes a firm stance against implicit logic
I bought that as Zig's M.O., but then came the automatic "async/IO" colorless functions which is a huge chunk of implicit and hidden logic.
That's more implicit logic than what's in say JavaScript async AFAICT. Arbitrary int sizes with implicit casting rules, etc. More recently was the attempts of memory safety via an some sort of (optional) implicit GC mode. Personally I don't get it.
The async design changed multiple times, and the current approach is that of requiring passing an I/O interface that encapsulates the logic, same approach as for passing around an allocator.
This effectively makes all I/O bound code "quasi-async", and it's all explicit and the caller is in control.
As far as I understand it, the idea is that the IO interface is "magic" in the sense that it affects which version of a function gets called: either the synchronous, linear version, or the asynchronous, stackless, suspensible state machine version.
This has a lot of implications, especially for function pointers and other forms of dynamic dispatch.
My personal take is that async code is fundamentally different from sync code, because the weights of various tradeoffs change dramatically. In my view, "colorless" functions are not really a worthwhile goal in the first place. But I've been wrong before, so it may turn out to be great, we'll see.
There is no magic about the I/O interface. It's a regular dynamic dispatch, the same as if you use virtual functions in C++. Currently, and in the foreseeable future, there are only two options how it can run, blocking mode where each task occupies a dedicated thread, or using stackful coroutines. The interface implements blocking functions, there is no async waiting anywhere in there. But because it's dynamic dispatch, it allows the async implementation to do the "prepare operation, suspend until completed, wake up" dance, where the threaded implementation would just run the blocking syscall directly. There are plans to add stackless coroutines, but that will require many language restrictions to happen first, so it's not going to happen any time soon and when it does, the interface will likely change to accomodate it.
> it allows the async implementation to do the "prepare operation, suspend until completed, wake up" dance, where the threaded implementation would just run the blocking syscall directly. There are plans to add stackless coroutines
Yeah so that's a lot of hidden magic and implicit logic. ¯\_(ツ)_/¯
"It's actually explicit if you look at how it's implemented" is true for everything.
If you look at how it's implemented you can see that the classic "XOR a register with itself to zero that register" trick is in fact basically free in a modern CPU rather than using up the ALU because of how register renaming is implemented. But hopefully we're not pretending that "You could buy the CPU vendor's technical manuals" makes this now explicit.
In most languages, async is implemented as a special feature in the compiler, not as userspace code. You can't just jump-to-definition of async/await keywords in languages that use them. async/await are not keywords in Zig, they are regular function calls. There is obviously a categorically different level of insight between jumping to library code and digging into compiler internals.
While you could say that, all the operations are documented behavior. You need to assume they will block. If the blocking happens in userspace runtime or in the kernel doesn't matter. And you, as the application author, are the one choosing which runtime to use. This just allows the libraries to be generic.
Doesn't that assume there are no intrinsic differences between single-threaded, multi-threaded and async execution? Plenty of code works fine single-threaded but breaks once it's run multi-threaded or async, and some works fine multi-threaded but breaks under async. With explicit async you have precise control over where you yield back to the event loop. With Zig's Io you don't get that unless you deliberately design for it, and at that point you've just got an implicitly coloured function.
There is a difference between single-threaded and multi-threaded, but if you use `io.concurrent` or `io.async`, you are expected to have your code assume multi-threaded enviroment. And there is no visible behavior change between blocking and non-blocking backend, assuming you are not calling external libraries that do their own I/O. So code written and tested on blocking threaded I/O, will work just fine on async implementation. All I/O operations are the yield point.
If you don't like arenas then that will push you towards avoiding allocations :)
I don't love malloc / free, managing frees one-to-one with allocations is a nightmare. An HTTP server is actually a great example of a good fit for arenas... you can have an arena created per connection, and then free the arena when the connection closes. Now all of your allocations during that connection are guaranteed to be freed instead of having to keep track of them individually. Need something to live across connections? Use the general purpose allocator for those particular things.
For one shot programs, why bother freeing anything at all? The gpa or malloc or whatever else is fine there.
Personally I mostly lean on fixed-size buffers, but if I'm writing Zig it's just little toy programs where blowing up when the buffer runs out of room is fine. But for big non-toy programs it might be good to use fixed-size buffers anyway and define your limits in terms of things actually relevant to your domain.
RAII and smart pointers are fine too, but C++ has you covered if you want those. Zig is specifically avoiding that kind of invisible code that destructors bring you.
> Now all of your allocations during that connection are guaranteed to be freed instead of having to keep track of them individually
And how do you size the arena?
> keep track of them individually
I think this is an exaggeration of the problem. This is what RAII semantics in C++ are for.
> avoiding that kind of invisible code
The invisible code is traded for defer statements right after initialization that are obvious and therefore mostly just noise, and also a problem if you forget to call them. This isn't an issue with RAII. I get the point of not wanting invisible code, but defers don't seem much of an improvement either. A trade-off at best.
Two options: you either say "eh 20MB per request ought to be big enough" and resize it later if you need to. Or you take time to figure out reasonable limits in terms of whatever domain you're dealing with, and return descriptive errors when those limits are exceeded. Maybe you're saying your service supports up to N HTTP headers and M query parameters in the URL and so on. For almost anything there's a limit you can place that's reasonable. Having no limits at all just means the limit is when you run out of memory / disk / whatever.
If you like RAII and C++ just use that though, Zig is an alternative not an upgrade, imo.
The problem is not finding limits, although that does take decent engineering for anything non-trivial. The problem is that a hard limit assumes your requests have a relatively uniform distribution of memory requirements. Otherwise, you are wasting memory. Or you have N limits instead, which just complicates the problem.
Resize: realloc invalidates pointers. If you're saying allocate more chunks and link them, that is technically no longer an arena?
Requesting virtual memory and committing lazily seems a good trade-off except on Windows, which does not allow to over-commit. Somehow I don't find that very satisfying because it relies on the OS and on the hardware to provide virtual memory support. But maybe it's just what it is.
Yup! In C you can mmap out a big anonymous region and then it will only take up actual resident space for pages you use. I'm not sure if Zig exposes this in the stdlib somewhere or if you'd just do it through libc.
If you're doing something like per-request or per-frame arenas you can clear the arena without having to reallocate the whole mapping. And depending how fancy you want to get you can mark the pages unused when you clear the arena so that the OS can reclaim them if needed.
As opposed to allocating a bunch of memory you won't need?
Allocating virtual memory up front and committing lazily might be a good trade-off, until it isn't. So I'm not actually sure and hence why I asked the question.
Not to take away from your point, but client_max_body_size "Sets the maximum allowed size of the client request body." That is different from how much the server/service is allowed to allocate internally to service the request.
Yes, my bad, replied in a hurry. Technically, the arena size will be function involving both the request body size, the response body size, along with the sizes of any intermediate results of processing.
> If you don't like arenas then that will push you towards avoiding allocations
I mean push in the sense of a pit-of-success, not making me do things because the alternative is unpalatable.
> managing frees one-to-one with allocations is a nightmare
It really isn't a nightmare if your language has tools to help you. I'm never particularly concerned about frees in Rust outside of thinking about room to optimize.
> Need something to live across connections? Use the general purpose allocator for those particular things.
Well, this is a determination you have to make very carefully and if you screw it up (like in the situation I was trying to outline) you have a subtle bug.
> Personally I mostly lean on fixed-size buffers, but if I'm writing Zig it's just little toy programs where blowing up when the buffer runs out of room is fine.
What's also fine is using malloc, which will have perfectly reasonable performance for your toy programs.
This is reinforcing my belief that Zig programmers actually just have a sort of aesthetic attachment to the idea of managing their allocations themselves and this has very little to do with the actual practical concerns of writing software that works. I'm a lot more sympathetic to the other reply that talked about library code improving flexibility for those relying on the library, than this comment that only thinks about the case where everything goes perfectly and using a fixed size buffer for no reason.
> I'm never particularly concerned about frees in Rust outside of thinking about room to optimize.
Zig's position, love it or hate it, is that you should be thinking about it. Dropping a Vec in Rust is O(n). Have you ever clicked the X on an app and it takes 10 seconds to disappear? Stuff like this is part of the problem. You shouldn't have to traverse your process's whole memory space just to call exit(0).
If you have to write that for loop, the O(n) will jump out at you and push you towards an arena or FixedBufferAllocator or std.process.cleanExit, or any other solution to avoid the loop.
So often when people insist that a thing they don't like is "part of the problem" they haven't even thought about how much that "part" would be, although I suppose it's worse when they have thought and decided to go ahead anyway because the weasel wording might convince the hard of thinking.
A guy who rounded up from $14.86 to $15 on some grant paperwork and got 14 cents he wasn't entitled to out of a government programme, and Ken Paxton inexplicably becoming very wealthy from a government job are both "part of the problem" but hopefully it's apparent which of these is worth fixing.
Just to let you know, `std::process::exit(...)` in Rust does not call any destructors. C++ also has `std::quick_exit(...)`. Applications exiting slowly is a problem caused by app developers.
(There are cases where it also takes time because the OS does have to flush all open files during process teardown, etc., but this shouldn't generally be user-visible.)
Dropping vecs that contain strings does not make any Rust code in the real world slow. You would need to have a vec containing billions of strings, occupying several gigs of memory, before dropping such a structure took any perceptible amount of time. And for extreme cases like that, you can simply use arenas. The bumpalo crate is approximately as old as Zig is. The reason that people so rarely turn to arenas in Rust isn't because you can't do them, because you've always been able to, but rather because arenas are a very specific solution to a specific sort of memory access pattern, and going out of your way to strictly adhere to that pattern doesn't produce measurable differences for most software.
Disclaimer at the start: Rust is my favorite language, this very much isn't an attack on Rust.
> Dropping vecs that contain strings does not make any Rust code in the real world slow. You would need to have a vec containing billions of strings, occupying several gigs of memory
I actually had a hobby project where that became very noticeable/too slow for my requirements at around 800k Strings, so several orders of magnitude less than your estimate. Switching to a solution that freed all the memory at once predictably made it run effectively instantly.
> And for extreme cases like that, you can simply use arenas
I think this is a fundamentally flawed way of thinking about things. Yes, each individual string allocation and drop isn't going to cost much, but in combinations with all the other similar decisions you're likely making if you think this way, plus the decisions of library authors, etc, it can add up even if strings specifically aren't a major problem. Avoiding pessimization by default instead of only when you can't ignore them any more is a good habit to be in.
Arenas aren't a free lunch either. You need a specific sort of memory access pattern, where large, conceptually related chunks of memory are being rapidly and predictably reclaimed, and if you don't have such a pattern then arenas are ceremony for no benefit. And the performance tradeoff is that you end up using more total memory than RAII because you're no longer eagerly reclaiming memory the moment it stops being used, but purposefully delaying your frees so that you can batch-reclaim it all at once. There are many cases where this makes sense, which, to reiterate, is why crates like bumpalo exist, which work gloriously well with Rust's lifetime system.
I agree that arenas aren't always the best choice, but I think you're underestimating how often they are. Not only does a lot of software have elements that are well suited to arenas (request handling for web servers, frames for games and UI, etc), but a lot of allocations are for short lived intermediary values anyway (the stack is basically an arena with a bit less flexibility).
(Also, you do not need to sell me on bumpalo, I use it heavily already)
> This is reinforcing my belief that Zig programmers actually just have a sort of aesthetic attachment to the idea of managing their allocations themselves and this has very little to do with the actual practical concerns of writing software that works.
Indeed if you don't want to manage everything yourself I don't think there's a need to use Zig. The whole pitch is making everything explicit and not hiding control flow. And I do think that limits its usefulness to smaller-scale codebases.
Writing quality software doesn't require much from the programming language until your codebase gets very large and you're collaborating with a ton of people. If I'm expecting a codebase to grow to a large scale I'm not reaching for Zig for that. I don't trust _myself_ to keep that much Zig code tidy and correct let alone a big mob of developers and agents :)
With a small program, it's rarely difficult to work out what's going on. If your small program develops a performance problem, it's usually fairly tractable to work out what's going on, and modify it. In extremis, you can just rewrite the whole thing.
Large programs are another matter. Hidden control flows in C++ (for example) can make pinning down performance problems extremely difficult. And then widespread refactoring can be extremely challenging.
Zig forces everything to be an explicit interface, which should make maintenance work on large programs much more tractable.
1. When I pick a low-level language, that's precisely the kind of control I'm after (otherwise, why go for a low-level language?).
2. Heap allocation is a very common cause of serious performance problems in low-level programs because malloc/free overhead is so high. Some languages have a moving GC to reduce the high overhead of heap allocation, but low-level languages typically can't use that optimisation, so you have to resort to arenas (which offer a similar optimisation to that of moving collectors). This isn't always faster than malloc/free, but it usually is, and if you want malloc/free you can pick that.
Anyway, I wouldn't say that the default in Zig is to use an arena, rather it's to give you the control that's the whole purpose of low-level languages.
Of the general-purpose methods it does; that's is exactly what these collectors were invented for - not to carry all that overhead of malloc/free. I guess you could argue over the frequency of situations where it yields a significant boost. It's also true that malloc/free have also improved, especially in the very large runtimes (like TCMalloc and jemalloc), but they can't do all the optimisations moving collectors can.
It’s great that they were created for this purpose, but I have a faster allocator in non-moving GC engine in C++. And I don't have to moving gigabytes of data.
Generational moving allocators don't move objects much, and so the total amount of work they need to do is substantially less than malloc/free allocators -- that's the whole idea of these algorithms and the reason why they're used (that's not under dispute; it's just the mathematics of the algorithms [1]). Still, there might be some situations where allocators in C++ could be faster, but if that were the case for my purposes, I would have stuck with C++. We needed very good performance for highly concurrent workloads, and C++ just doesn't come close in many of those (maybe at 10x the budget).
[1]: More precisely, the work they need to do doesn't scale linearly with the allocation rate as is the case with non-moving approaches. Of course, arenas are sort of degenerate moving collectors, so they also display the same effect, but they can't always be used.
The way this works is that you allocate in the young generation with a pointer bump, and to free memory you compact (move) the surviving objects, which make up a small minority, a few times and then promote (move) them to the old generation (where you either manage memory with the same technique or a different one). And so the total amount of moving is not high (objects either never move or rarely move), and what you end up with is essentially two heaps, one where the allocation rate may be high but you can completely negate all the extra work by increasing the size of that heap, and one where the allocation rate is low. This ability to, optionally, not double the amount of work when the allocation rate doubles is what gives moving collectors their efficiency (and that you can bump-allocate in virtually all cases).
Of course, it's possible to come up with programs where objects do move a lot (or there's some other common work), e.g. programs that don't exhibit the behaviour of the generational hypothesis, but the point is that in a very large class of programs, this technique is hard to compete with.
Passing allocator pointers around has the advantage of allowing you to customize the allocator when you want, and to ignore it when you want (which is most of the time), i.e. just use the allocator you were given and pass it around like some piece of syntax or ctx in Go.
The trouble comes when you have data structures that manage memory, and you want to associate the allocation strategy with the particular instance of the data structure. Or, worse, you don't store the allocator pointer with the data and you require that all functions operating on the data structure happen to use compatible allocators.
At its simplest, though, passing allocators around is just removing an implicit global variable that is sometimes helpful to customize locally, but nearly impossible to customize locally if left as a global.
Allocators help solve the problem of "this code I want to use is almost good enough; how do I actually use it?"
I distinctly remember one of my first "real" jobs, somebody had written a C# library for GraphQL. It had some sort of bug or lack of functionality I had to fix. The author had fallen hook, line, and sinker for sealed classes and all that nonsense, so the shortest path to using the rest of their code was to fork the library and re-write the thousand offending lines and all references thereto. Luckily it wasn't a deep dependency.
Contrast that with similar issues in typical Python. The library likely accepts my favourite duck-typed solution, if it doesn't then I can likely work around it with an appropriate choice of dunder methods in the code I have interacting with the library, and if it still doesn't yield then I can just rewrite (monkey-patch) the offending code at runtime.
Not that either of those scenarios are objectively optimal, but they illustrate one of the principles people have in mind when they use an interface like allocators (not the only one, I'm sure other commentors will talk your ear off), that when push comes to shove you'd prefer to be able to use your existing code to solve your new problems. And that does empirically mostly work here; you can use nearly the entire Zig stdlib (especially when ignoring things like the posix namespace) and most of the rest of the ecosystem as well on embedded devices, in WASM, in bounded-memory applications, in generic application code, in fast-running CLI apps not even bothering to free, etc.
I agree with much of your discussion about arenas by the way. They're a moderately poor fit for the current interface in Zig, largely because the free functionality is questionable at best. IME, most uses of arenas actually know they need such a thing. Functions taking allocator arguments and expecting the allocator to be an arena should take an arena argument. When using arenas for lifetime management, something should probably own that arena rather than implicitly assuming whatever gets passed through the stack is correct. Passing arenas to arbitrary functions expecting general-purpose allocators is prone to leaks, and you'd be better off using the arena (or a buffer created therefrom) as a backing allocator for a data structure capable of actually freeing if you can't trust the implementation you're calling into (and if you can trust it, they should expose a better API allowing you to detect that trustability at the callsite).
They're orthogonal concepts. Zig hasn't done this, but you could absolutely combine injected allocators and automatic destructors in the same language if you wanted.
The bigger design point is who's responsible for knowing where the object lives. Zig has a balance of power more in favour of the caller than the callee, a point I tend to agree with since it's quite easy for a caller to know exactly what they want done and quite hard for a callee to know what all possible callers might want done. That principle is applied pretty liberally through the rest of the language.
With some notable exceptions, I would argue that most of the time, the caller simply doesn't care. Maybe the "right" approach is to provide two functions, one that takes an allocator, and one that does not. That way the caller doesn't have to care unless it wants to.
I see, we can solve the problem of excess verbosity by duplicating our API surface ;)
Jokes aside, that's not the worst idea (you often see the stdlib do something in a similar vein with managed vs unmanaged data structures), but I bet it doesn't survive contact with real-world programmers well enough to actually provide the portability that normal Zig code has. For transitive "style" impositions, it only takes a low rate of non-compliance for the perceived benefit to disappear.
If that's a goal you have (and IME most code is lightning fast to write, with all the heavy lifting going to impedence mismatches like the one being described, so I tend to be happy to write a bit of extra boilerplate (I like Zig, but it's an absurdly verbose language) if it means the hairier integration points will go more smoothly, and I therefore do have that goal) then the allocator API empirically works, the other solutions I've seen work much less, and I'd want to have a pretty good idea of why a contender would sate my needs instead.
If that's not a goal you have, you do you man. I can shovel out sealed Scala classes or unmaintainable numpy with the best of them as the situation demands. Nobody's created "the best" language yet. We're just casually discussing tradeoffs.
Suppose you are implementing an application server (backend).
You can limit the concurrent requests that can be handled at a time to 2000 and limit memory per request handler to 2MB.
So you allocate 2000 x 2MB at program start.
Then you crate an arena allocator with (2MB limit) when starting to handle a request.
It will just fail if a request uses more memory than 2MB. This means you will basically never have OOM issues or a request using unexpected amount of memory to crash the server.
You also have performance benefits like:
- Allocation operation is basically an integer increment.
- Memory for a request is laid out linearly in memory and likely is pre-populated so you won't have page fault into the kernel.
- It is very unlikely to have double free or other memory management mistakes.
You can also fuzz the system in several ways to make sure requests can really never exceed that amount of memory usage so you don't have surprises in runtime.
If a request by a user really exceeds that much memory usage, you can just fail that request and log it so it will be debugged and fixed later.
You also don't need data structures like SmallVec (rust version of the thing) that are hybrid stack/heap allocated.
There are many intricacies to this approach but it works much better on scale when you have a large program that handles a large amount of memory.
It is super difficult to debug and fix memory usage issues on global allocator only programs in my experience.
I tried to fix one such issue on a database-like program in rust and it was impossible.
Also this means you don't absolutely need ownership/lifetime safety.
I am using this pattern in a C-like C++ codebase I have and it works really well.
That just means you haven't run into the problems yet that a global allocator brings with it. Other people have and welcome the better control over allocation provided by the Zig std library. The challenge is of course to balance convenience and flexibility. Also if you write code that prefers to not allocate in the first place (which should be the default), you're not affected anyway.
if you don't want to, you can just pick the general purpose allocator and use it everywhere.
however, in languages that don't allow it, you can't pick and choose different allocators for different workflows. it may not be important to you, but it is important to many. and shoe horning support for multiple allocators is not easy for language
If you had to pick a default, an arena allocator would definitely be it. Although even better would be a fixed-size arena that can never grow, like Tigerbeetle does.
I don't think the footguns you're talking about are real by the way.
arena's are popular in part because they are a way of grouping allocations by a single lifetime, reducing the amount of use-after-free's or double-free's that occur (reducing, not eliminating them).
they make a lot less sense in languages where the compiler or runtime takes care of this for you.
I also don't get it, programming since 1986, in quite a good number of programming languages, including various kinds of systems programming alternatives.
To me this kind of stuff always feels a bit cargo cult programming.
mimalloc aggressively reclaims space and it benefits the use case in our Dip database built on rust.
This is for a long running process where consuming compute and memory should only occur on processing a request and must revert to a baseline memory consumption once the request is completed.
malloc with default allocator always gradually increases the memory consumption. This continues with enough request bursts on our testing until the process runs out of memory. mimalloc doesn't.
On mobile, we are stuck with the default allocator. On mobiles, the phenomenon of the default allocator consuming memory is reproducible even with normal Qt apps. It is what it is.
I first encountered this behaviour on Linux desktop with Qt QML apps as far back as 2015. For a long time, I thought it was Qt QML ListView's inefficiency and didn't think much of it.
But in recent years, the phenomenon was visible with Dip which was built on rust.
Dip does massive parallel fanout using green threads ('may' coroutines) with work being NUMA pinned (software hash) using a thread per core model.
A default request may span up to _chunk_count_ jobs (default 5k) configurable per request.
Every request (x 5k jobs) from the client on a stress test sees a jump of ~10-20 MB of memory. Sometimes the jumps use large swaths of memory - say 100s of MB and stabilizes for a while only to gradually climb up again later. Rinse and repeat - OOM kicks in and the process crashes.
Many times we thought we were leaking memory somewhere. But armed with the Qt experience a decade ago, we intuited maybe its the allocator - and switched to mimalloc on desktop executable.
Dip's memory, using mimalloc, stabilizes at ~60 MB post request completion no matter how many requests you throw at it.
Pity that on mobiles, more specifically Android, we have to live with the default allocator on even trivial Qt apps let alone embedded Dip. But people have lived with it for at least a decade, so it is what it is.
Thus on mobiles, we just have to be content like everybody else, unable to squeeze every last ounce of performance and efficiency out of the hardware on stress tests ensuring Dip remains "just better :(" :)
You are looking at this from the wrong perspective. If you are fine with automatic allocations and RAII like you have in C++ or Rust, or with a garbage collected language, there is pretty much no reason to even look at Zig.
Zig is a language to use when you do need the control, mostly because of performance reasons. And once you are in, you start thinking how to manage your allocations and arenas are the logical outcome, once you notice that your application has scopes that can be destroyed at once.
If you need an argument why arenas are better than RAII, then you likely don't have the performance needs to even consider using Zig, because outside of performance, arenas are not better.
It is like saying "why do C++/Rust people don't want a garbage collector?", or "why do Haskell people insist on function purity?". A GC in C++ and Rust would make things much easier, and a few impure functions would likely make Haskell more practical, and then, if Rust let go of all that "memory safety" cruft, it would be much easier to write one-off scripts and prototypes with it.
Languages have opinions, that's why there are several of them and they are popular in some applications and not others. And Zig opinion is to be explicit so RAII is out, and the language does push you into avoiding allocation when unnecessary, as a side effect of not giving you the "easy to use interface" you want.
This makes it particularly suitable for fields like embedded software where you may not have malloc/free at all, but may be less so in a highly dynamic environment, where you may be better served with a GC anyways.
To me, that's not RAII, it is static analysis, useful, but different.
To me, the name "Resource Acquisition Is Initialization" implies that doing one means the other one is done implicitly. Now, in the end of the day, the result is machine language, with proper discipline you can follow the RAII principles in any languages, including assembly, but I have always understood RAII as being it done implicitly.
Also we can argue that Zig supports a weak form of RAII through the use of "defer", there are some nuances here.
OK, so everyone in the replies is saying "yeah duh", but a few years ago people used to defend the negative externalities of LLM's by pointing to medical applications and saying "but think of how useful they would be for doing a cancer diagnosis"
There are some cases where that is still the case - medical diagnosis where you have a lot of text information that you can pass in can be pretty mindblowing with LLMs. If it's text inputs, it's going to be pretty darn good with an LLM. If not, probably not.
I think at least a few of the replies including mine are saying this is a pretty meaningless over hyped demonstration because the cnn was fine tuned on the same dataset it was tested on and the actual way the eval was structured and run is nonsensical. It's basically just slop research designed to create a fun attention grabbing headline (ironically one that trashes llms).
It's a double edged sword because without #[non_exhaustive], adding a variant becomes a major semver breaking change. It depends on your priorities. I think that there is probably a subtle difference between enums that are expected to gain variants in minor releases (a common example is error types) and enums that are expected to be closed (something like Option or SocketAddr).
Honestly what I tend to see is that most enums are not exposed publicly at all besides maybe error types and configuration options (where the user usually constructs but does not match on them). In both cases #[non_exhaustive] is valid. For all of the non-public enums you get to enjoy the compiler exhaustiveness enforcement.
Without #[non_exhaustive], adding a variant is always a semver breaking change.
With #[non_exhaustive], adding a variant is not a breaking change, but only if that variant can safely be ignored. If it can't be, then it's still a breaking change, BUT you don't get a compiler error. That's a very big drawback IMO.
This makes me think of how the PNG format handles this. For context, PNG files consist of a list of chunks of different types (image data, color profile, text comment, etc). Each chunk has some flag bits that tell you whether it can be safely ignored when decoding the image, safely retained when the image data is changed, etc. That way the decoder can ignore unknown ignorable chunks and error on unknown non-ignorable ones.
I think using refcounting can allow you to get lazy with your object graphs, throwing referencing to objects around everywhere and then losing the ability to conceptualize who owns what and who is responsible for who (so to speak). Sticking to a stricter ownership graph forces you to be more deliberate with how your types are designed and this can lead to better architecture in the long run.
The problem with lifetimes is they become a object tree, where Rc and co let you represent an object graph (well, a DAG at least). So when your problem resembles a tree, it's great, but some problems don't like being forced into that shape.
Their style is pervasive in Rust, such as in the standard library, so I would personally apply a when-in-rome philosophy. FWIW something like `.text("hello")` I would expect to see in a builder-pattern but not in a setter for an object that's already built.
That being said I don't particularly understand your conception of a method. They are both methods. The getter ought to have a shorter name since it is probably called more frequently and because it inherently has less of an impact -- this goes along with the principle that names that demand attention should be larger and easier to notice.
My use case is a TUI framework (https://www.incredible.rs/). The setters configure elements in a chain-able manner. I grappled with the rust convention and decided to go the other way. Rational as mentioned.
But this made me think...
> "The getter ought to have a shorter name since it is probably called more frequently"
They are setters. They modify an element after instantiation. Mostly as part of the initial build, but then also in the event closure. Under the hood, a struct with interiorly mutable fields.
This sounds a lot like a builder but the line can be blurry. For a Rust stdlib example, the `process::Command` type is considered a builder even though it is not named "CommandBuilder", and the final operation is spawning a subprocess, not returning some other data.
What I was saying is that I think a "flipped pattern" where anything that sets something sometime somewhere is the word and anything that reads is a get_* makes this fine distinction unnecessary... and that makes life easier... I came to Rust from JS where one can overload methods... I understand from other comments that the set_* convention may have arrived from C++... In any case, consistency I think beats specifics.
Gotcha, so in your case, users of the library will set values much more often than read them (since the reading will be done by the TUI framework for display).
The granularity of crates shouldn't really impact what kinds of abstractions you build. You can still have as many modules and types as you need, and as big of functions as you need, but its saying you should organize the modules into separate crates instead of one large crate. They even call out the trait coherence rules, saying that you can just make your library bigger instead of forcing a more awkward design in the pursuit of a smaller crate.
I think one benefit of small crates is that it gives you more granularity in linking and dependency graphs, which is especially relevant for rebuilds. At a previous job we had a monolithic "utilities" crate, and this caused problems when you tried to depend on it for the IP address utilities but this brought in the radio frequency utilities. When one of these submodules got an update the versioning would excessively propagate downstream to every consumer. If this was organized as small "net-utils" and "rf-utils" crates then maybe it would not have been as much of an issue.
I've long suspected that one of the things that makes monads notoriously difficult to explain to people outside of the Haskell ecosystem is that very few other languages even have a kind system that you are allowed to manipulate in code (and in some, like C++, it's so esoteric that you basically never see it). First explaining that you can even abstract over "type->type" kinded things gets you a lot of the way towards monads without actually describing monads.
The problem is not monads. Any programmer understands the need to represent "the world". It is the need to keep that representation modular that leads to monad transformers, which makes regular people (like me) simply check out and go back to C++
Monads have no inherent connection to any notion of "the world"; that's just a thing for Haskell's IO monad specifically (and perhaps derivatives thereof) because what that abstracts over is effects that are external to the program.
A monad is any construct of abstraction that obeys the Monad Laws. This may seem a useless definition, but my point is that deriving a useful intuition for the concept of the monad is necessarily challenging because they cannot readily be boiled down any further than that without loss of information.
The best intuitive explanation I've developed so far is that a monad is an abstraction over structures of ordered computations. The most ready examples for non-functional programmers are probably exceptions (and their handlers) and nullable types (typically called "optional types" in languages with first-class handling of such things, unlike Java).
Interesting. I come from a math background and for me, monads capture a notion of "things that depend on a type". So for every type X you can think about Lists/Records/Trees/... of X. And if you're lucky this gadget let's you do two things: 1. You can turn every instance x of X into a primitive List/Record/Tree/... of Xs. 2. You can flatten Lists of Lists of Xs into plain Lists of Xs, etc.
This is something you see a lot once you know what you're looking for.
And if you're really lucky, these two are nicely compatible:
For example there are two ways to get from L(L(L(X))) to L(X): By either flattening first in the inside or first on the outside.
And for a monad, you get the same result either way.
And the computational part is then really more of a consequence: If you've got some function of type X->L(Y) and another one of type Y->L(Z) then you can compose them to a function of type X->L(Z).
What also makes the monad more interesting than just composition or flattening is that when executing the ordered computation, you can decide next computations based on previous results. This was what really made it click for me.
I find it easier to think of them as transforms which turn "a function which accepts/returns plain values" into a "function which accepts and returns a type which embeds plain values".
map :: (a -> b) -> (t a -> t b) -- Functor
bind :: (a -> t b) -> (t a -> t b) -- Monad
cobind :: (t a -> b) -> (t a -> t b) -- Comonad
ap :: t (a -> b) -> (t a -> t b) -- Applicative
The resulting function type is the same in all these cases - the difference is the type of the input function. There are other similar functions which return a `(t a -> t b)` too.
The "ordered computation" isn't unique to monads, it applies to all of these when you chain them. For monads particularly though, you can't begin the continuation until the previous result has been generated - whereas with a comonad, you can begin the continuation right away and extract the value afterwards (inside the continuation).
Can also think of it like a producer/consumer or push/pull thing. Monads push values into some type `t`, and comonads pull values out of `t`.
The naming of `return` is perhaps unfortunate as it gives the impression you're returning a value (done to make do-notation look like imperative code) - but monads don't return values. `return` produces a computation.
`coreturn` is usually called `extract`, which is better naming - it extracts a value from a computation.
The order of arguments is also unfortunate because it makes the above pattern less obvious - they're usually written as:
fmap :: f a -> (a -> b) -> f b
bind :: m a -> (a -> m b) -> m b
So it's not obvious that `fmap` is transforming a function `(a -> b)` into one `(f a -> f b)`, etc.
Not sure if going back to C++ is the answer, given how much Haskell like code one can write since C++20, and it has gotten even better with newer standards.
That may be true for functions which operate on a generic Monad typeclass, but typeclasses (Higher kinded types) aren't required to implement concrete Functor / Applicative / Monad instances for a particular type, e.g. Maybe:
fmap f Nothing = Nothing
fmap f (Just x) = Just (f x)
pure x = Just x
Nothing >>= f = Nothing
Just x >>= f = f x
It's about as difficult as describing vectors in math. Vectors aren't really "little arrows" or "things with magnitude and direction" (because then functions wouldn't count), I think there's a similarly cryptic saying that "a vector is just an element of a vector space" or "a tensor is anything that transforms like a tensor".
In both cases what they really are is an abstract concept defined by things that satisfy some definitions, but in practice it's easier to just look at a few concrete examples like Maybe.
I think most of the difficulty in explaining monads is the IO monad specifically. Leave that off and monads seem almost trivial. Add it back and whatever trivial mental model you just built stops working altogether.
Some OO languages have a parallel concept in "Metaclass" (https://en.wikipedia.org/wiki/Metaclass) i.e. the class of a class which is structurally similar to "Kind" though of course the mechanics are quite different.
I still have an old book Putting Metaclasses to Work - A New Dimension in Object-Oriented Programming by Forman et.al somewhere.
No, monads are largely a solution to problems introduced by pure functional programming, problems that impure languages don’t have in the first place.
I like monads as a concept, but in practice I find them frustrating and unnecessarily complicated. I worked on a large game written in Haskell and eventually gave up because dealing with monads made development so cumbersome.
I love the idea, but for real-world projects I’d rather avoid the complexity.
It's odd, the stereotype of software developers is a know-it-all attitude, but start talking about types and you get excuses that boil down to 'I'm incapable of learning to understand the thing you learned to understand'.
There is a video called A New Programming Fundamentals Course that contains some concepts that are very helpful to understand cases like these. I'd recommend you watch, it's free: https://www.youtube.com/watch?v=EV13CNrq4ZA
Namely, the section on "Mapping knowledge webs" (around 3:33). It explains people can struggle to adopt concepts that are too far outside their current knowledge, but that it can still be possible for them to pick it up if they can build a path to it from their current knowledge. (This often presents itself as the "depth-first" learning style)
A lot of commenters here, talking about how monads are so simple and how not being able to learn them is just lazy, are misunderstanding how learning fundamentally works. You have to expand the knowledge graph in the direction of monads before it will be easy to pick up monads. You have to build a path. You cannot simply teach monads right off the bat and expect someone to intuitively "get it". People who intuitively "get it" are people who already have a path.
I used to be a big Haskell fan and had a lot of fun writing and using monads. And I agree that once you get them, the basic idea is quite simple. I’ve even used monads to prove a small compiler correct in Lean.
But in practice, I think monads are a poor tool for large-scale software development. I spent a long time trying to build a large game in Haskell and eventually gave up out of frustration with the complexity they introduced.
Monads make sense as a way to manage effects in pure languages like Haskell/Lean/Idris/Agda. But outside that context, I don’t think the additional abstraction is worth the cost.
Understanding how monads work is one thing. Understanding why they’re needed in the first place, and when they’re not, is another.
OCaml isn't pure and neither is F# iirc do you see a bit less monad heavy code in large codebases there?
What sort of situations would you want to give up a little purity or error handling at the type level? Maybe at a boundary where you can just write a very ugly imperative but quarantined section to give it a simpler type?
> The thing that makes monads difficult is laziness and the anti math circle jerk.
That's quite a hateful way to describe lack of knowledge.
There are some prerequisites that make it easier to understand monads, and a lot of programmers don't even know those. That can make learning monads difficult right off the bat.
Your math explanation starts with a math concept that the vast, vast, vast majority of people will have never even heard of, even in math-adjacent fields. It's not anti-math reasoning. It's a lack of self-awareness that your background is very math heavy, which makes that particular explanation make sense to you. I've gone my entire schooling, as a student who was always one of the few who were "good at math", all the the way through my CS education, without ever having heard of a functor until I came across the Haskell monad explanation meme.
Your next step could be to blame the school systems if you want, but that doesn't help the situation at hand.
I majored in mathematics and I learned category theory before touching any serious Haskell, and I still found most of the "monad tutorials" kind of opaque and handwavy, and I think there are inherently non-trivial details unrelated to the notion of functor etc.
No there are no trivial details, and you don't need a mathematics major or category theory to understand what a functor is.
It's a pair of a type constructor F(a) and a map operation a -> b to get an F(b).
The two "non-trivial" details are the fact that the map operation has to respect identity, map(id) = id, and function composition: map(g ∘ f) = map(g) ∘ map(f)
None of those concepts is any difficult, nor requires any "math major" or non-trivial details, because the are not there.
Most of the stuff you just explained is absent from your explanation of monads. So it turns out monads are not so simple after all for people who don't already understand functors...
>> You start from a functor which is a data type that admits a map operation.
Then you add of, or pure, to make an applicative functor, and then you add flatmap to make a monad.
> Your math explanation starts with a math concept that the vast, vast, vast majority of people will have never even heard of, even in math-adjacent fields
Let me take this apart.
1. a data type that admits a map operation
Present in most languages including JavaScript. Not a mathematical issue.
2. add of, or pure, to make an applicative functor
I had to look this up. It was new content for me, maybe 5 or 10 minutes believing I would get it.
3. add flatmap
This didn’t scare me but maybe it scares others. Not a mathematical issue here, though. Being in C++ doesn’t make this concept mainstream, I guess?
FWIW I dropped out of high school to pursue programming more directly. I regret it now because of the job market crash, but it's also too late for me to go back.
You can try to blame lack of knowledge on anything you want, but at the end of the day Hacker News is for intellectual curiosity and even HN readers don't all know what monads are.
I think a big problem is that, as a poll aggregator, the more you pick and choose which polls are allowed to be aggregated, the more risk there is that you are inserting bias into the result through selective exclusion. Erring on the side of broadly accepting poll data means less room for bias. But they do talk about the need to raise their standards due to the increasing ease of producing fake data in this podcast with FiftyPlusOne: https://www.youtube.com/watch?v=Neuot3aMkrY
reply