What's new in purely functional data structures since Okasaki? (2010)
cstheory.stackexchange.com
cstheory.stackexchange.com
The other discovery is that some languages, like Swift, are introducing lots of surface-level functional idioms that resemble their counterparts in truly functional languages, but without the persistent structures. Since Swift is implemented in C++, no doubt that's why it's not a reality in that new language either.
If someone could cleverly invent an implementation of Okasaki that worked in a language like C++ (and some smart people have really tried), I think it would be a major game changer to the language's possibilities. This handicap is a rare example of something that is simply not so possible in C++.
This is new to me, but based on the HN link, isn't Okasaki a reference to a book with multiple different algorithms? What are the names of the Okasaki algorithms that are hard to implement in C++?
(1) I'm pretty sure that everything that can be done by a GC can be done by `shared_ptr` and `weak_ptr`. If not, then you can use Boehm GC if you don't do anything weird with pointers.
(2) If you mean "tail recursion optimization", it's a compiler thing, not a language thing in this case, and GCC and MSVC in fact support it, and I'm sure Clang does too. So since "either one" is sufficient, as you say, it looks like you can go implement Okasaki's stuff in C++ now!
That doesn't help so much. If it is not a standard language feature as per the C++ spec, then it's not as useful as you'd think.
edit: Not understanding the downvotes here. In Scheme, for example, tail calls are part of the semantics of the language. We know that when we place a recursive call in tail position that we're really describing a linear process. If such a thing is just an optional optimization, programmers can't rely on it in the same way.
> The list implementation is simple/nice but it imposes a limit on number of stored elements because of "additional effect" of recursive destruction.
EDIT: I also don't see how destruction of a list is any harder than iteration of a list, as far as writing it tail recursively. You would need to make sure your node destructors are trivial, but you probably need to do this anyway if you intend to support using a custom Allocator (would instead have template <typename Allocator> destroy(node*, Allocator&); that calls Allocator::destroy on the contained value and Allocator::deallocate on the node). The implementation would then look something along the lines of:
decltype(head) next; for (auto ptr = head; ptr; ptr = next) { next = ptr->next; if (ptr->unique()) { destroy(ptr, allocator); } }
(unique() is a method on node - intrusive ref count.)
Except only slower and subject to thread races.
The problem with RC based solutions is that to beat the top of top GC implementations, they need compiler support and so many tricks that in the end one gets RC + mini-GC anyway.
For example, Cedar at Xerox PARC used RC with local GC for cycle collection, instead of trying to use lots of tricks for RC performance fine tuning.
Some functional languages leverage this sort of restriction to their advantage. It can simplify garbage collection and serialization protocols don't need to track visited nodes for example. It's certainly nice to be able to step out of this mode from time to time and many functional languages provide controlled effects of sorts. I think an interesting area to study would be the balance between minimal effects and otherwise immutable structures. I wouldn't however, use this to sacrifice persistence, which it seems many implementations do.
When you compare benchmarks in GC languages to benchmarks in non-GC languages, you're usually comparing mostly-static allocation to dynamic allocation, because non-GC languages tend to be oriented to the former. With functional data structures, however, lots of dynamic allocation is typically unavoidable.
For an excellent paper on state-of-the-art memory allocation techniques (which also has a good overview of prior techniques, and how their works builds on that), see "Fast, Multicore-Scalable, Low-Fragmentation Memory Allocation through Large Virtual Memory and Global Data Structures" from OOSPLA 2015: http://2015.splashcon.org/event/oopsla2015-fast-multicore-sc...
If you're aware of published works which show garbage collection consistently outperforming manual memory management for the reasons you explained, I'm very interested in reading them.
There may be a crossover point where the GC algorithms are better (say, a pathological case where you keep forcing the manual memory allocator to hit its slow paths by allocating just one more than the size of a slab, freeing all of them, then repeat). But I would also not be surprised if there never is a crossover point.
No, by live objects I don't mean the objects that currently exist in the heap, I mean the objects currently reachable by the program, which is the same whether or not you are using GC. But you are right that GC will use much more heap on average than manual allocation.
I agree that you can probably always beat GC using manual allocation if you are sufficiently smart about it. For instance, if you have some data structure you know is going to allocate a bunch of small objects, and none of the small objects can outlive the data structure, you can have that data structure create a special slab just for its objects, so when you free the data structure you just need to call one free() on the slab instead of a bunch of free() calls on every object.
for (size_t i = 0; i < 100000000; ++i) {
size_t* val = malloc(sizeof(size_t));
*val = i;
free(val);
}
Decent memory allocators will just keep reusing the same memory location over-and-over. All 100 million calls to malloc and free will be fast-path calls. GC-based schemes will most likely not be 100 million fast-path allocations. Yes, this is a contrived example, but freeing memory as you no longer need it is common in languages with manual memory allocation. It is also harder, more error prone, and quite often leads to nasty bugs, but it's usually faster.I think you're reasoning about just the cost of the frees versus garbage collection cost. But by ignoring the extra memory required on the heap, you're also ignoring the extra slow-path allocations.
Calling free is not what is expensive. It's what we do when we call free. GC has to do more work because it has to figure things out. Manual memory allocation does not need to spend any instructions figuring anything out because the programmer has already done that. As I have said before, decent memory allocators will keep returning the exact same address for each malloc call in this situation. That is the ideal situation for this case.
Also keep in mind that larger heap sizes don't come for free - that means more page faults, which will slow you down.
Allocation is a mere pointer bump.
The language the compiler is implemented in doesn't matter, so I guess you mean that it's not possible because Swift is too heavily influenced by C++? I don't think that this is really the case, it's more influenced by Objective-C
The link doesn't really explain why though. I mean, I'm willing to believe this is true until proven otherwise - it's safe to assume Milewski knows what he's talking about - but I'd love to see a proper explanation.