"Ark is not a garbage collected language, therefore when you allocate memory, you must free it after you are no longer using it. We felt that, as unsafe as it is to rely on the user to manage the memory being allocated, performance takes a higher precedence. Although garbage collection makes things fool-proof and removes a significant amount of workload from the user, it inhibits the performance we were going for."
https://github.com/ark-lang/ark-docs/blob/master/REFERENCE.m...
This is the usual false dichotomy that languages subscribe to, where they presume that manual and unsafe memory management is the only alternative to dynamic and safe memory management. But Rust's secret sauce is that memory management is static and safe, thanks to linear types and the borrow checker. Rust is still the only competitor in the space of zero-overhead memory-safe languages.
Unfortunately, it has lots of warts that have sent me crawling back to C++. Addressing the particular item you're talking about here, manually specifying lifetimes of objects is a cure that's worse than the disease. It's great when the compiler infers everything for you, but I'm never going to be able to explain the syntax or semantics of those ugly 'a marks to my coworkers who aren't interested in programming language theory.
Anyways, I've been tempted to write a full blog post listing all of my Rust complaints, but I figured it's better to just quietly let you guys enjoy your thing. However, whenever I see these advocacy posts from you and the other Rust honchos, I can't help but scream a little bit inside. It's really not as good as it could've been.
To be really specific: it's great that you got "zero-overhead memory-safety", but I can't even implement fundamental data structures without using unsafe blocks and lifetime annotations.
Please do. I'd love to hear your thoughts on issues.
As for the lifetime annotations, we could be extending lifetime elision to more places, including to struct definitions, if people come up with rules that are easy enough to understand. I'd probably be for it, but there are others who think that if you go too far toward removing lifetime annotations then you actually make programs more difficult to understand and the language harder to teach. But that was also the argument against our current lifetime elision rules, which are pretty fantastic in retrospect, so I'm not particularly swayed.
It sounds like you don't want memory safety without garbage collection, based on your last paragraph, and that's totally fine! My use cases aren't necessarily your use cases, and I don't want to claim that everyone who truly doesn't care about zero-overhead memory safety is wrong. But I think it's defensible that memory safety is an important feature for many projects (for example, the ones I work on) in 2015, even if it results in an additional learning curve. The disease we're trying to cure is a never-ending stream of security vulnerabilities and crashes in systems code, and none of the attempts to solve the problem that have existed so far have worked.
Though I do feel a bit let out after reading http://cglab.ca/~abeinges/blah/turpl/_book/vec.html which implements Vec from scratch, as it's not super simple.
But really, that would be impossible in Go, just as tricky in C++ (if you make it stl compatible), and impossible in implement generically without lots of void* stuff in C, so I guess I wasn't sure what I was expecting. It's a pain to implement fundamental data-structures I guess.
Still, if you've got anything new to say, go ahead and say it.
Do you think when the standard library is totally GC free it would be good for your use cases?
But some GC schemes could be fast enough even from an IRQ handler, if memory allocation is just something simple like an atomic add to top of heap pointer. As long as non-interrupt level routines, potentially running on an another core, have enough time to clean up the garbage.
Manual memory management seems to be at the end of its road when it comes to high core count systems, with tens or hundreds of CPU cores. Allocation and application side object lifetime synchronization and management will simply saturate any inter-core communication mechanism, limiting scalability. GC should be able to get around that limitation. At that scale, you could already dedicate one or more cores just for cleaning garbage.
Suddenly your memory allocation routine is a pointer addition, and you don't need garbage collection either. Deallocation is just as fast as well, it just happens in one block.
It requires a bit of care in the code using it to not grow memory in an unbounded fashion, but this isn't really hard once you get used to it.
That's one of the techniques I apply. I have also some other tricks with different trade-offs up in my sleeve.
This is primitive garbage collection. Because compaction / sweeping phase is simply discard all, no marking phase is required.
Sometimes you get a lot of interrupts in a sequence, and if the lower priority code didn't have a chance to clean up the arena, you lose data.
Interrupts can also occur at any point, unless you disable them. But you can't keep them disabled for very long. Maybe long enough that you check IRQ handler is not currently running on another core. If not, swap an arena pointer IRQ routine uses, enable interrupts again and start to process the previously pointed arena buffer.
> It requires a bit of care in the code using it to not grow memory in an unbounded fashion, but this isn't really hard once you get used to it.
This. The babysitting code and care you need for this technique.
https://fossies.org/dox/glibc-2.21/malloc_8c_source.html#l02...
Also see this benchmark between different, faster allocators: http://locklessinc.com/benchmarks_allocator.shtml
The extra work doesn't end at allocation. When you actually implement a concurrent system, you'll usually end up having corner cases at object lifetime changes, which you need to synchronize. If the memory is only claimed when there are no more references to it, this extra synchronization step can be avoided. If you can't rely on this, you'll probably end up doing synchronization, such as (atomic) reference counting.
Synchronization is very expensive and it can quickly become the performance bottleneck for the whole system. On modern X86, you can do 5-20k floating point operations during one contended atomic sync op. Reference count increase or decrease is one sync op. A simple mutex needs two of those.
The more you have CPU cores, the more there will be synchronization (cache coherence) traffic broadcasted to all cores.
Words like "JIT" and "GC" seem to cause knee-jerk reactions in some developers. Likewise for manual memory management. It's not so black and white. There'll always be trade-offs. I usually write low level (firmware and kernel driver) and high performance code. C/C++/SIMD. Code that might need to react under a microsecond.
My message is just please be more open minded.
Analyze where your code spends its execution time. You might be surprised how much of it is spent in things like C++ streams, xprintfs and memory allocation. Unfortunately inter-core synchronization is more insidious. It's not visible in benchmarks on small systems. Often you only start to see hints of this problem when actually running on more cores. Enough of them, and that's all your code is doing.
the only mainstream competitor, perhaps; ATS does a good job of it too [http://www.ats-lang.org/]
(It doesn't seem like the language in the OP is interested in memory safety though.)