Live data from Hacker News

Taming Go’s memory usage, or how we avoided rewriting our client in Rust

akitasoftware.com

211–220 of 231 posts

Re: Taming Go’s memory usage, or how we avoided rewriting our client in Rust

#211

Earlier quoted context omitted.

You might be interested in this: https://rust-unofficial.github.io/too-many-lists/

Thanks. Saw that before, but the credibility/length ratio wasn't high enough to read it more carefully. It appears that we do have to Box/Rc/Arc nodes in a recursive datastructure. Doable, but a bit on the inconvenient side. struct Node { elem: i32, next: Option >, }

All explanations start with why without any indirection, Node would be a recursive, infinitely large type. Therefore the Node must be a pointer. Ok. But then, Rust then forces you to answer this question: who will own the data referred to by the pointer? Consequently, who will be responsible for freeing it?

If you use a &mut Node as your pointer, you are attempting to answer those questions with "not me". Someone else has to own the Nodes. They've got to be somewhere on the stack or on the heap. There's nothing stopping you from defining the next pointer as Option. The problem is actually constructing a list.

Not many answers tell you why you can't do this in practice. I agree that this is not explained well enough in general, because new Rustaceans don't intuitively reach for references so it probably doesn't come up much and they're hard to use for this. But it's not that hard to see why:

Imagine you try to allocate all the Nodes at once (e.g. an array), and then use &mut references into the pre-allocated array. In order to set one &mut Node's next pointer, you will have to hold another &mut Node to set it to. This means you need to acquire mutable references to array elements, in the order that you wish them to appear in the linked list. This is actually really tricky to do: slice::get_mut(index)'s returned reference borrows the entire slice, so it doesn't let you have a &mut reference to two nodes at the same time. You need smaller &mut [Node] slices, somehow.

slice::split_first_mut is one way (in order), but if you have an array and can only create a linked list in the order nodes appear in the array, what's the point? Just use the array! Any other compiler-checked access order scheme will also be so limiting that you should just use a data structure of that exact shape anyway. To use an arbitrary order, you're going to need unsafe, so you'd basically be writing C.

To be fair, there is basically one application of this, and it's to have a sparsely populated constant size array that needs to be iterated in order. I made a demo:

https://play.rust-lang.org/?version=stable&mode=debug&editio...

The other problem is that you can't resize the backing array: your &mut Node references would be invalidated.

For this reason, pre-allocated lists like these are usually done with indices instead of references. The overhead is one pointer + offset and then a bounds check when dereferencing.

---

The other solutions answer the ownership question like so:

- You can use Box, so that each Node (acting as a list head) owns the entire tail of the list, uniquely, such that no other list can also refer to it. The tail is freed when the head is, unless of course you detach it first (let tail = node.next.take();).

- You can Arc/Rc the nodes, so that each node has a pointer, but not a unique pointer, to the next node. These can be duplicated, so lists can exhibit structural sharing if you are comfortable with that. Because of the sharing, freeing the head does not necessarily free any/all of the tail.

Re: Taming Go’s memory usage, or how we avoided rewriting our client in Rust

#212
post #27

Earlier quoted context omitted.

> Additionally, Rust is pretty in-your-face when it comes to concurrency and sharing memory across thread/task boundaries. Use channels whenever possible.

Channels are not always the best solution (unless you're referring to Rust channels?) https://www.jtolio.com/2016/03/go-channels-are-bad-and-you-s...

Yeah, Rust's crossbeam channels are actually really good.

Re: Taming Go’s memory usage, or how we avoided rewriting our client in Rust

#214
post #160

Earlier quoted context omitted.

That's because the Python regex module caches the regexes it compiles, so it only happens once. It's proper and good usage to specify the regex string inline, even in a hot path. I'd only use a variable when I'm using the same regex multiple times in code, and even then I could still just have the variable be the string.

Yeah, it's been a while since I've benchmarked that, I'll try it out

Gotta agree with the sibling comments here. The performance difference is definitely smaller than it used to be, but there's still good reasons to keep compiled regexes in a module scope.

Caveat: I write libraries, not "production" code; my requirements are significantly more strict. One thing I can't do is make assumptions about where my code will run. If you're using my library, and you compile a whole bunch of regexes, they'll evict my regexes from the cache. I don't want the performance of my library to suffer, so I'll keep them in the module scope.

Re: Taming Go’s memory usage, or how we avoided rewriting our client in Rust

#215

Earlier quoted context omitted.

Thanks. Saw that before, but the credibility/length ratio wasn't high enough to read it more carefully. It appears that we do have to Box/Rc/Arc nodes in a recursive datastructure. Doable, but a bit on the inconvenient side. struct Node { elem: i32, next: Option >, }

All explanations start with why without any indirection, Node would be a recursive, infinitely large type. Therefore the Node must be a pointer. Ok. But then, Rust then forces you to answer this question: who will own the data referred to by the pointer? Consequently, who will be responsible for freeing it? If you use a &mut Node as your pointer, you are attempting to answer those questions with "not me". Someone els…

An improvement, using the Node struct and apparently pushing the limits of borrowck: https://play.rust-lang.org/?version=stable&mode=debug&editio...

If you actually want this intrusive linked list functionality, consider using a real-world implementation like this: https://lib.rs/intrusive_collections

Re: Taming Go’s memory usage, or how we avoided rewriting our client in Rust

#216

Earlier quoted context omitted.

The usual C# reflection APIs that devs turn to allocate a lot, but there are ways to make them almost performant by (re)using delegates and expressions. There are a number of good libraries to use reflection faster, as well.

IIRC, dynamically compiled expression trees in C# have the overhead of a single virtual call (on the resulting delegate) when executing - and cover all object factory and member access scenarios. But if you need to discover metadata, you still have to resort to Reflection APIs.

The main corner case that still causes me problems is that if you want to construct a delegate at runtime, this often forces you to go through the reflection APIs to actually grab the method even if you know its full signature, etc. My current project has a JIT compiler for scripts that has this problem (I ended up finding a workaround involving getting LINQ to generate method tokens in an assembly, but .NET Core / .NET 5 deprecated LINQ compilation...)

Re: Taming Go’s memory usage, or how we avoided rewriting our client in Rust

#217

Earlier quoted context omitted.

Reflection APIs seem to be pretty messy and slow in every runtime I've ever used, perhaps because the idea of optimizing them might encourage more use. The C# reflection APIs also allocate a lot.

They're pretty simple in many dynamic languages, eg you can just do "import os; dir(os)" in Python.

yeah, but that doesn't mean they're fast or don't make a mess of the gc heap

Re: Taming Go’s memory usage, or how we avoided rewriting our client in Rust

#218
post #209

Earlier quoted context omitted.

I’m not really sure if that 2x figure is accurate. I’ve seen charts on both sides of this and a lot here depends on your programming language and the things it can optimize: with Linear/Affine types, I’m fairly sure Haskell could, in theory, eliminate GC deterministically from the critical sections of your code-base without forcing you to adopt manual memory management universally. But, there’s just the fact that peo…

> … tasks like implementing lock-free hash maps… Please be specific. You pointed to spectral-norm, what does that have to do with lock-free hash maps? The 2.java program seems to be 4x slower than the 7.rs program !

Look at 2.cl, though: the lisp solution is faster than everything except one c++ solution. (And, aside from the SIMD intrinsics, the lisp solution is fairly idiomatic)

I was referring to this with the lock-free hash maps: https://twitter.com/nodefunallowed/status/137196906733924761...

Re: Taming Go’s memory usage, or how we avoided rewriting our client in Rust

#219
post #207

Earlier quoted context omitted.

Two examples: LockGuard and Box. Control is primarily exerted over consumers of your API rather than the actual resources. This can be enforced through a combination of Drop implementations, and closures / lifetimes; the classic example is Mutex's LockGuard. In a GC language (eg Go) they give you defer or finally blocks that can accomplish the same thing, but that is always optional and up to other programmers to rem…

Ah, so it's more about library writer control then about library consumer control? Since for example in Common Lisp, the latter can still be accomplished through declarations, such as DYNAMIC-EXTENT ( http://clhs.lisp.se/Body/d_dynami.htm ). (Not sure if the former is necessarily related to memory usage control, but you'd probably achieve that type of resource control by exposing only WITH-* macros in your API.) Mayb…

Edit: yes. Library consumers don't get to change much, except where you have generic functions that abstract over a trait like `T: Borrow`, and then you can pass in any kind of owned or borrowed pointer to T2.

Dynamic-extent appears to be more similar to the "register" hint in C than to anything in Rust, in that it's an implementation-defined-behaviour hint. Rust has no such thing as hinting at storage class. Your variables are either T (stack) or Box (heap) or any other box-like construct involving T. You maintain complete control at all times, nothing is implementation-defined, and it's explicit. You can implement (and people have implemented) dynamic switching between stack and heap storage in a Rust library.

https://lib.rs/smallvec (stack to heap), https://lib.rs/tinyvec (smallvec with no unsafe code), https://lib.rs/arrayvec (stack only)

As you can see, these three library authors get to control very precisely how their types allocate and deallocate, and you basically mix and match these and the stdlib's smart pointers (and Vec) + other libraries like arenas, slot maps, etc to allocate the way you want.

> you'd probably achieve that type of resource control by exposing only WITH- macros in your API*

Yes, this and similarly using with_* closures both work, but both are more limited than destructors that run when something goes out of scope. A type that implements Drop can be stored in any other type, and the wrapper will automatically drop it. You can put LockGuard in a struct and build an abstraction around it.

Re: Taming Go’s memory usage, or how we avoided rewriting our client in Rust

#220

"How we avoided rewriting in Rust" feels like clickbait given that the answer is "our problems were algorithmic, not language-specific"

I would call this a language issue as you need to understand the various abstractions and how they interact which is endemic to almost all languages, ideally a language would type system to express resource usage
Post reply on HN