13 ms·
Some important things I think people should note before blindly commenting: * The example code is obviously contrived. The real gist is that massive deallocat
by fpgaminer 6y ago
Some important things I think people should note before blindly commenting:
* The example code is obviously contrived. The real gist is that massive deallocations in the UI thread cause lag, which the example code proves. That very thing can easily happen in the real world.
* I didn't see any difference on my machine between a debug build and a release build.
* The example is preforming 1 _million_ deallocations. That's why it's so pathological. It's not just a "large" vector. It's a vector of 1 million vectors. While that may seem contrived, consider a vector of 1 million strings, something that's not too uncommon, and which would likely suffer the same performance penalty.
* Rust is not copying anything, nor duplicating the structures here. In the example code the structures would be moved, not copied, which costs nothing. The deallocation is taking up 99% of the time.
* As an aside, compilers have used the trick of not free-ing data structures before, because it provides a significant performance boost. Instead of calling free on all those billions of tiny data structures a compiler would generate during its lifetime, they just let them leak. Since a compiler is short lived its not a problem, they get a free lunch (pun unintended), and the OS takes care of cleaning up after all is said and done. My point is that this post isn't theoretical, we do deallocation trickery in the real world.
- papaf 6y agoThis deallocation trick is neat but in C and C++ you could use a memory pool to do this. In theory, you could also use a memory pool in Rust but I think the standard library uses malloc without some way of overriding this behaviour.
- deleted 6y ago[deleted]
- orf 6y agoYou can change the global allocator in any rust project. You can write your own easy enough, or use one like jemalloc
- bauerd 6y agoThink with LD_PRELOAD it is always possible to override the allocator?
- dan-robertson 6y agoOnly if it is dynamically linked
- jfkebwjsbx 6y agoThat wouldn’t help. The point is to use local allocators which have extra information about the data structures and usage of the memory. This is routinely done in medium to large C++ programs for different reasons (performance, debuggability...).
- francasso 6y agoI don't get this. Usually I either don't care about how stuff is allocated, or when I care I want to be specific about what allocator should be used where. In the second situation just setting a global allocator wouldn't cut it most of the time, I would want support for customization when I instantiate a data structure.
- josephg 6y agoSure; but when you’re using arenas and things like that you usually you will want different objects allocated into different pools (or with different lifetime properties). Rust only lets you pick one allocator for the entire process, so you can’t specify “all the children of this data structure go in arena A, and this other allocation goes into a traditional heap”. It’s more awkward, but I much prefer Zig’s approach here where everything that allocates takes an allocator as a parameter. Usually the allocator is specified at compile time - in which case zig can generate identical code to the rust compiler. But when you want the flexibility, it’s there. Aside from compilers, this is heavily used in video games where there’s often a lot of objects that get allocated per frame, and can be discarded all together. And in that case rust’s lifetime tracking would be a huge asset. The dovecot email server (C) also makes superb use of a mix of memory containers for different tasks. Given how messy email parsing is, dovecot is an absolute pleasure to read.
- pjmlp 6y agoIn C++/WinRT the same approach is taken, because you cannot just use a memory pool for COM.
- vvanders 6y agoYou can also use the typed-arena crate[0] or roll your own if you're feeling like cracking open unsafe. [0] https://crates.io/crates/typed-arena https://crates.io/crates/typed-arena
- all-fakes 6y agoTyped-arena is a bit limited, because all the allocated values must be of the same type. bumpalo[0] removes that restriction, but it also doesn't run destructors, which can lead to memory and other resource leaks. Another worry I have with bumpalo is that I don't think it's been reviewed thoroughly for unsoundness. Still, bumpalo has been used to great effect in dodrio[1], a React-like library for Rust with really good performance. [0]: https://crates.io/crates/bumpalo https://crates.io/crates/bumpalo [1]: https://github.com/fitzgen/dodrio https://github.com/fitzgen/dodrio
- CoolGuySteve 6y agoEven just keeping a free list and deallocating it’s elements at an idle time is probably cheaper and faster than spawning a thread.
- estebank 6y agoIn Rust you could just call `mem::forget` on whatever heavy thing that you're no longer using is before it would get dropped, but then the programmer is effectively responsible for that memory leak not becoming a problematic leak during refactors. Edit: this will also break any code that relies on Drop being called for clean up, but that is already a "suspect"/incorrect pattern because there are no assurances that it will ever run.
- mcguire 6y agoIsn't that a problem for the RAII approach?
- kd5bjo 6y ago> There are no assurances that it will ever run. Yes and no. Whenever control leaves a code block, Rust automatically calls the drop() method of all values still owned by that block. There is no guarantee that control will exit every block (cf. Turing), but a moderately exceptional circumstance needs to occur for this not to happen, like an infinite loop.
- rcxdude 6y agoThere's also no guarantee the object won't be moved elsewhere, including into a context which never gets freed (for example, you can construct mem::forget using entirely safe code by forming a cycle of reference-counted boxes). That said, Rust has support for such a concept through the Pin trait (which essentially guarantees an object will not get moved until it is 'Unpinned').
- projektfu 6y agoYeah, I like Apple’s (Next’s) approach of pool allocation for each run through the event loop. Defer dealloc, drop pool at the end.
- saurik 6y agoApple's pools are for helping manage reference counts of returned objects (via autorelease) but aren't doing pool allocation (as any of those objects could escape, so you can't do the fast thing of just deallocating the pool). The normal memory allocator is used while in the scope of an autorelease pool.
- projektfu 6y agoTrue, I was thinking the same after I posted. Maybe I’m thinking more of Apache’s approach and how it gets some of the benefits of a fork-per-request server without the overhead.
- andrekandre 6y agoright, so if you do @autoreleasePool { [[[SomeObject alloc] init] autorelease]; }//someobject is release after exiting this scope //inside main runloop, not inside @autorelease{} [[[AnotherObject alloc] init] autorelease]; //AnotherObject will be release at end of main runloop [[NotAutoreleased alloc] init]; //will leak past runloop iteration is that understanding correct?
- mcguire 6y agoUnless your destructors do more than deallocation, in which case you will leak whatever other resource you're managing.
- MaxBarraclough 6y agoA pool can invoke destructors when it is cleared. Might take a bit of overhead (if the pool is to support arbitrary classes), but you could retain the fast pointer-bump allocation.
- wongarsu 6y agoIn a toy raytracer I once wrote in C++, switching from malloc to custom memory pools for small fixed-size objects was a big performance boost. Making free() a noop was another big performance boost, both for deallocation and allocation. Turns out sequentially handing out memory from a big chunk of memory is much easier than keeping track of and reusing empty slots, and it keeps sequentially allocated objects in sequential memory locations.
- folmar 6y agojemalloc is the answer to the question you did not ask
- jomohke 6y agoYou can easily use a custom global allocator in Rust: #[global_allocator] static GLOBAL: MyAllocator = MyAllocator;
- rubber_duck 6y ago>As an aside, compilers have used the trick of not free-ing data structures before, because it provides a significant performance boost. Instead of calling free on all those billions of tiny data structures a compiler would generate during its lifetime, they just let them leak. Since a compiler is short lived its not a problem, they get a free lunch (pun unintended), and the OS takes care of cleaning up after all is said and done. My point is that this post isn't theoretical, we do deallocation trickery in the real world. And then someone tries to use your compiler as a service (code analysis, change triggered compiler) and it's a dead end
- yrro 6y agoAs distasteful as leaky code is, is it that bad to run it in a separate process? You get a bit more robustness against crashes as well.
- rubber_duck 6y agoYou want to be able to incrementally update state to get performance out of incremental code analysis (eg. language server implemention for IDE)
- folmar 6y agoThere are more ways to go then that: AST fragment caching, intermediate representation fragment caching and so on. Incremental updates fit some languages better than the others.
- qzw 6y agoWell, then that’s not the original use case anymore, and it’ll have to be re-engineered. In the meantime it may have been used for years and the perf difference may have saved many developer-years collectively across its user base. Surely you’re not suggesting that the compiler developers should be prematurely optimizing for future use cases that they may not even have envisioned.
- 6y ago
- hinkley 6y agoOne of my “favorite” snags in perf analysis is that periodicity in allocations can misattribute the cost of allocations to the wrong function. If I allocate just enough memory, but not too much, then pauses for defragmentation of free space may be costed to the code that calls me. A solution to this that I’ve seen in soft real time systems is to amortize cleanups across all allocations. Every allocation performs n steps of a cleanup process prior to receiving a block of memory. In which case most of the bad actors have to pay part of the cost of memory overhead. Might be good for Rust to try something in that general realm, or in the cleanup side may be easier to tack on. On free, set a ceiling for operations and queue what is left. That would at least peak shave.
- rini17 6y agoDoing unrelated cleanup sounds like flushing CPU cache per every allocation.
- hinkley 6y agoThis post is already about unrelated cleanup, so I'm not sure what other escape hatch you imagine people taking. Do you have a suggestion? You can tell that it's unrelated cleanup because if it were related, then the cost of freeing wouldn't be noteworthy. It would be cache hot because you would be visiting it for the second time. In which case we'd be talking about why you are scanning a giant object on an event loop in the first place. That's not what's at issue. What's at issue is that you've been handed this great bomb of uncached data from someone else and now you're stuck doing the janitorial work. Freeing an object of arbitrary size is effectively an unbounded operation. Cache invalidation has a very high cost, sure, but it's still bounded. Putting a limit on the amount of work you do, you could stop before purging the entire cache. You could use a smaller limit on doing work for someone else and control invalidation there, too.
- indemnity 6y agoIsn’t a Rust “move” implemented as a bit wise copy (e.g. memcpy call)? I see people claiming move has no cost but I’m not sure that is true.
- justinpombrio 6y agoEDIT: See kevincox's reply. Rust will bitwise copy the containing type, which is typically very cheap. For example, it you move a String, it will copy the String struct, which contains a couple pointers and a length (or something along those lines). Importantly, it will not copy the underlying char array. I was thinking of the following code, where I believe the assignment to y is actually free. Though apparently this isn't called a "move". let x = <<large owned type like [char; 1000]>>; let y = x; More info: https://doc.rust-lang.org/rust-by-example/scope/move.html https://doc.rust-lang.org/rust-by-example/scope/move.html
- kevincox 6y agoSemantically it is a bitwise copy. In the example from the article it is probably actually a copy because the value was originally on the parent thread's stack, which will be reused after the function returns, so the value will need to be copied to the new thread's stack. However it is important to not that it isn't a deep/recursive bitwise copy. It just needs to copy the HashMap itself (which is probably a handful of words). So yes, it is doing a bitwise copy, but this is also very cheap. It will be much, much cheaper than spawning the thread.
- ahupp 6y agoThat's not exactly true. In C++ terms, the example code is moving a value: std::map<...> foo; someFunction(std::move(foo)); And not moving a pointer like: std::unique_ptr<std::map<...>> foo = ...; someFunction(std::move(foo)); So it copies sizeof(std::map<...>), not a pointer.
- pjmlp 6y agoNot necessary, std::map::map(&&) is free to move just the internal pointers and bookkeeping data, only dumb implementations would move the memory blocks where the map data is actually stored, as per ISO C++ requirements.
- ckcheng 6y ago>As an aside, compilers have used the trick of not free-ing data structures before, because it provides a significant performance boost. Instead of calling free on all those billions of tiny data structures a compiler would generate during its lifetime, they just let them leak. Since a compiler is short lived its not a problem, they get a free lunch (pun unintended), and the OS takes care of cleaning up after all is said and done. My point is that this post isn't theoretical, we do deallocation trickery in the real world. This reminds me of the exploding ultimate GC technique [1]: > on-board software for a missile...chief software engineer said "Of course it leaks". ... They added this much additional memory to the hardware to "support" the leaks. Since the missile will explode when it hits its target or at the end of its flight, the ultimate in garbage collection is performed without programmer intervention. [1]: https://devblogs.microsoft.com/oldnewthing/20180228-00/?p=98125 https://devblogs.microsoft.com/oldnewthing/20180228-00/?p=98...
- all-fakes 6y agoYes, Java also has a garbage collector that does nothing, called the Epsilon GC, intended for short-lived programs and references for garbage collector benchmarks.[0] [0]: https://blogs.oracle.com/javamagazine/epsilon-the-jdks-do-nothing-garbage-collector https://blogs.oracle.com/javamagazine/epsilon-the-jdks-do-no...
- inopinatus 6y agoThe bumper sticker is “Ada programmers prove the algorithm terminates.”
- blub 6y agoI guess the bigger question is why they use dynamic memory allocation in the first place.
- ndesaulniers 6y ago> As an aside, compilers have used the trick of not free-ing data structures before In Clang, this flag is `-Xclang -disable-free`. Not from a Jedi...
- jfkebwjsbx 6y ago> That very thing can easily happen in the real world. Only if badly designed. That is why it is contrived! > While that may seem contrived, consider a vector of 1 million strings, something that's not too uncommon A program dealing with a million elements of any kind should not be performing naive allocations to begin with. > we do deallocation trickery in the real world Skipping deallocations is an optimization, not a design pattern. In other words, the code needs to keep the ability to perform the deallocation for debugging, testing, usage as a library, etc.
- GuB-42 6y agoI don't know if Rust can do it (unsafe?) but in C and C++, I sometimes end up writing a custom allocator. It is often one of the most significant optimizations. For example, I had the "million strings" problem once, literally millions. The solution was to put every string into a single large buffer and the pointers in another buffer. Not only I could deallocate everything at once but I also saved a bit of RAM by not aligning (not needed for strings).