Most active commenters
  • cycloptic(5)
  • mwkaufma(5)
  • voldacar(3)
  • nyanpasu64(3)
  • qppo(3)

←back to thread

200 points jorangreef | 25 comments | | HN request time: 2.475s | source | bottom
Show context
logicchains ◴[] No.24293046[source]
I work in HFT, and one of the key concerns when writing low-latency code is "is this code allocating memory, and if so, how can I stop it?" Zig is the perfect language for this use case as none of the standard library implicitly allocates, rather for anything that allocates, the caller must pass in an allocator. The stdlib also provides a handy arena allocator, which is often the best choice.

This is a huge advantage over C++ and Rust, because it makes it much harder for e.g. the intern to write code that repeatedly creates a vector or dynamically allocated string in a loop. Or to use something like std::unordered_map or std::deque that allocates wantonly.

replies(8): >>24293328 #>>24293382 #>>24293469 #>>24293919 #>>24293952 #>>24294403 #>>24294507 #>>24298257 #
1. voldacar ◴[] No.24293469[source]
Yeah when I heard about this I instantly thought of game engines, but it makes total sense for HFT too. "Modern C++", with all its constant little mallocs and frees is so awful for anything that requires ultra low latency
replies(3): >>24293652 #>>24299417 #>>24300117 #
2. cycloptic ◴[] No.24293652[source]
Can you explain how this is a problem in modern C++? I was under the impression that all the STL containers (string, vector, list, map, etc.) worked the same and have an allocator parameter. Are there other areas where these are missing? Or is the issue that STL implementations almost always default to an allocator that uses malloc? I'm not trying to dog on Zig here (it's a nice little language) but this just doesn't seem to be something that only Zig can do.
replies(3): >>24294240 #>>24294749 #>>24299554 #
3. voldacar ◴[] No.24294240[source]
It isn't really a matter of can do/cannot do. It's more about the default patterns promoted by the idiomatic way of writing code. Yeah you could write C++ code that constantly passes around allocators while also using STL heavily, but it will be verbose, unnatural, and ugly.

As well as having nicer syntax in general and real metaprogramming instead of the brain damage that is templates, zig promotes this kind of allocator-aware programming style in a way that's clean and idiomatic

replies(1): >>24294275 #
4. cycloptic ◴[] No.24294275{3}[source]
I'm not sure what you mean that it's verbose, unnatural and ugly. To me it looks the same.

In C++, you have to pass around an allocator to your templates. You can typedef this away if you want.

In Zig, you have to pass around an allocator as a function argument or a struct member. You can comptime this away if you want.

Is there some fundamental way that I missed that Zig changes this? If your actual complaint is that C++ templates are bad and you're saying Zig comptime is better, that's different than having woes about allocators.

replies(2): >>24298526 #>>24299764 #
5. logicchains ◴[] No.24294749[source]
A concrete example is std::stable_sort. As far as I'm aware there's no way to pass it a custom allocator/buffer to avoid it allocating memory.
replies(1): >>24300103 #
6. fsociety ◴[] No.24298526{4}[source]
The difference is you have to pass an allocator to the standard library functions in Zig. That’s why it is idiomatic compared to C++.
7. nyanpasu64 ◴[] No.24299417[source]
std::span is a modern C++ class designed to act like an array or vector, by viewing the memory of an existing array/vector without allocating anything. In my experience writing audio code, C++'s implicit copy constructors are what makes it too easy to accidentally allocate memory.
replies(1): >>24304570 #
8. mwkaufma ◴[] No.24299554[source]
Many core modern C++ types don't permit customizing the allocator. E.g. std::function
replies(2): >>24299572 #>>24301268 #
9. mwkaufma ◴[] No.24299572{3}[source]
Furthermore, C++ dependencies commonly instantiate types like std::vector with the default allocator internally, rather than exposing it to the host application.
replies(1): >>24300100 #
10. voldacar ◴[] No.24299764{4}[source]
have fun trying to globally override new and delete in C++

(hint: there is no way to do this)

replies(1): >>24299945 #
11. cycloptic ◴[] No.24299945{5}[source]
Are you sure? https://en.cppreference.com/w/cpp/memory/new/operator_new#Gl...

You don't need to do this if you're using allocators.

12. cycloptic ◴[] No.24300100{4}[source]
Thank you for the examples. I'm not sure std::function is a good comparison. After some research it seems this used to be in the spec, but it was removed because nobody supported it correctly and it seems it was too difficult to do it in a type-safe manner anyway: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p030...

The other thing is that Zig doesn't seem to have any real plans to support C++-style closures right now. If they ever find a type-safe way to do it while supporting custom allocators, then that would be interesting, but at the moment I wouldn't say it's any better than C++ in this regard.

I actually have seen some C/C++ libraries that do allow changing the default allocator although it's usually only low-level libraries that bother to do this.

replies(1): >>24359229 #
13. cycloptic ◴[] No.24300103{3}[source]
Thank you for the example.
14. fluffy87 ◴[] No.24300117[source]
The actual Modern C++ design book from Alexander’s ci is about custom allocators and performance.

The irony.

15. tlb ◴[] No.24301268{3}[source]
However, most std::functions have a small built-in buffer for captured variables, like 4 pointers worth. If you limit yourself to only capturing that many, there's no allocation.
replies(1): >>24354807 #
16. qppo ◴[] No.24304570[source]
I'm not saying C++ is the best language out there but that smells like inexperience writing real-time safe code. The problem isn't implicit copy constructors but implicit copies in your code.
replies(1): >>24308234 #
17. nyanpasu64 ◴[] No.24308234{3}[source]
> The problem isn't implicit copy constructors but implicit copies in your code.

I don't understand what's the difference.

I'm not an expert in writing real-time safe code, but I've spent close to a year working on allocation-free programming. Implicit copies of structs (value types) are as fast as implicit copies of integers, but implicit copies of types with owned heap memory invoke the copy constructor, which calls into the allocator. Or did I misunderstand your comment or get anything wrong?

replies(1): >>24308972 #
18. qppo ◴[] No.24308972{4}[source]
Sorry I worded that poorly, what I meant is that you shouldn't be writing code where the fact an implicit copy constructor allocates is a concern at all, because C++ has very clear copy semantics.

With a handful of exceptions you essentially will never need to worry about this if you restrict assignment and argument passing of non-POD types to references within the critical code blocks. And in the case that you do need to worry about it, the copy constructor should be explicitly deleted anyway.

It's a footgun to be sure, but it's not a serious one with a bit of discipline. If you're used to doing real-time safe programming, you'll get paranoid about code that could invoke an allocator (or write your own).

replies(1): >>24309210 #
19. nyanpasu64 ◴[] No.24309210{5}[source]
> if you restrict assignment and argument passing of non-POD types to references within the critical code blocks.

It's a working strategy, but if you forget the `&` in `auto & x = document.foo; auto & y = x.field;` a single time, it might silently invoke the allocator. What actually happened to me was that it crashed because I returned a reference to a stack variable copied by mistake, when I meant to type a `&`. Pointers are probably less prone to accidental copying, but they have uglier dereference syntax and are nullable (excluding custom types).

Ever since that incident, I've been paranoid that I accidentally forgot the reference in another spot in the code. A few days ago, I debugged the code and set a breakpoint on malloc in the audio thread (LLDB crashes when listing threads, Visual Studio works) and found out my current codebase doesn't allocate on the audio thread. I hope I don't introduce any allocations.

To avoid this footgun, objects could be only copyable through an explicit `clone()` method like in Rust (which breaks std::vector<explicit_clone>), or by marking copy constructors as explicit (which you can't do to a std::vector).

replies(1): >>24309905 #
20. qppo ◴[] No.24309905{6}[source]
Most of this is solved by code review, unit testing, custom allocators, and if you really want rust-like guarantees, type traits. In this case std::is_trivially_copyable and std::is_trivially_destructible. Like you don't need an explicit .clone() method, you static_assert that all real-time-safe code only touches structs that are trivially copyable/destructible and write a custom allocator with optional checks to see if it's invoked in a real time context, and write a unit test to stress it. There are some places where this doesn't work, but they're obvious and pretty straightforward to handle. Ideally you'd have a slab allocator with constant time alloc/free while locked in the real time context that cleans itself up for real on resetting the system.

Really though, you shouldn't have an owned STL instance like std::vector near your real time code to begin with. You're seeing one of the reasons people writing performant code don't use the STL at all, even if it has gotten up to par with handrolled solutions in certain benchmarks.

21. mwkaufma ◴[] No.24354807{4}[source]
I'm not saying that the default allocation strategy isn't good (in the general case), just that it's not customizable (for special needs).
22. int_19h ◴[] No.24359229{5}[source]
C++-style closures are unrelated to custom allocators, since they're not heap-allocated.
replies(1): >>24401211 #
23. mwkaufma ◴[] No.24401211{6}[source]
You are correct that the type the compiler creates for a lambda is allocated in-place, usually on the stack, and perform no heap-allocs. However if you pass it to a std:: function it will be _boxed_ and std::function _will_ heap alloc the space for it. This alloc is what's not customizable.
replies(1): >>24404323 #
24. int_19h ◴[] No.24404323{7}[source]
That's fair, but std::function is not specifically about lambdas (As Boost.Function, it predates them, in fact) - it's about wrapping an arbitrary callable in a way that allows erasing its type. Idiomatic C++ rarely uses that class - I don't think it's used anywhere else in the standard library, even though it has plenty of higher-order functions etc. Turns out that closures that can only be passed in and not returned are still plenty useful.
replies(1): >>24435168 #
25. mwkaufma ◴[] No.24435168{8}[source]
In my dayjob as a code-reviewer, I see it in code-bases a lot. Between the generic name and elevated status in the std namespace, it's a natural tool for developers to reach for, across experience-levels. I speculate that the boxing side-effects are not well understood, given how many times I have to lift them out of hot-loops (despite the many unverified claims that "oh, LLVM will inline and optimize that away, no worries, teehee").

In general, I have not observed a consensus for 'idiomatic' C++, even within a single project. I say this as someone who wishes there was, because my job would be a lot easier if dependencies were less heterogeneous :)