At the end of the day, in a general purpose language, it's great to have machinery to avoid needing GC when possible, but you really do want to have GC to fall back on.
At the end of the day, in a general purpose language, it's great to have machinery to avoid needing GC when possible, but you really do want to have GC to fall back on.
This is not true. The counter itself can have its count updated atomically with a compare & exchange. I think this is what actually happens in most cpp std libs, but haven't checked in a while. (This doesn't mean the object they point to is thread safe though!)
But here's my question: Why do C++ programmers always refer to the overhead of "reference counting", when the overhead is not in the counting, but in the memory management: the use of doug lea's malloc or its derivatives such as ptmalloc3. This is what "new" wraps, and the STL objects all use it to resize themselves too. These routines are written by geniuses, but if you actually read up on how they work, they are doing a lot of (completely necessary) stuff, and one of the longest-running std lib calls you can make. (Way longer than an exp(), for example).
Presumably they're looking at reference counting as an alternative to unique_ptr, and in that context the memory management overhead is already being paid.
I have been under the impression that glibc implements both unique_ptr and shared_ptr (that's the one with arbitrary refcounts) using locks, but ok, if it's done with something like CMPXCHG that's still a heck of a lot slower than ordinary copying.
As for overhead, well, if an object lives for a long time and lots of references are updated or moved around, then that itself can be expensive, besides the malloc/free costs. Copying garbage collectors by comparison are simple and fast, at the expense of making your program gobble a lot more memory, limiting the language so that the gc can know where all the pointers are, and having to stop the program during gc.
Why would unique_ptr ever need a lock?
> As for overhead, well, if an object lives for a long time and lots of references are updated or moved around
If performance is important you only pass a shared_ptr if the callee needs to keep it around, otherwise just pass a plain (const) reference. You can also pass a (const) reference to the shared_ptr and then the callee can decide if they actually need to keep it. Moving shared_ptr is free, because the refcount stays the same.
But I agree that GC has higher thruput than malloc/free as most gc allocations are a single op, at the cost of higher initial memory usage and worst-case latency. That's why "scripting" languages like python / perl use refcounting as they need to start quickly and might not run for very long, whereas java / c# programs tend to be longer running.
eh, not that much.
use.cpp:
double rand_double() { return 16.; }
int rand_int() { return 16; }
void use(void*) { }
main.cpp: #include <iostream>
#include <chrono>
void run();
int main()
{
auto t0 = std::chrono::high_resolution_clock::now();
for(int i = 0; i < 10000000; i++) {
run();
}
auto t1 = std::chrono::high_resolution_clock::now();
std::cout << std::chrono::duration_cast<std::chrono::milliseconds>(t1 - t0).count() << std::endl;
}
alloc.cpp: #include <cstdlib>
int rand_int();
void use(void*);
void run()
{
auto res = malloc(rand_int());
use(res);
free(res);
}
exp.cpp: #include <cmath>
double rand_double();
void use(void*);
void run()
{
auto res = exp(rand_double());
use(&res);
}
I get ~100ms for the exp, ~130ms for the malloc/free at -O3 / -Ofast with both clang and gcc (and checking with perf ensures that the time is indeed spent in these functions). So it's a bit faster, but not exactly fast (or rather, the math functions are really fucking slow) double rand_double() { return 16.; }
int rand_int() { return 16; }
here are profiling traces:Not only that, but the original doug lea's malloc has a bin specifically for 16 byte allocations: http://gee.cs.oswego.edu/dl/html/malloc.html (the smallest size), so you've specifically chosen a very fast example.
In order to properly test malloc() you have to pre-generate some pseudo random numbers for your allocation sizes, and allocate and free unpredictably. But it's more complicated than that, as in the very occasional case when malloc() has to ask the OS for more heap, that system call may not "finish" until your program first writes to that region of memory... (ie malloc() returns but hasn't finished!)
I agree that malloc/new is comparatively expensive, but that ends up as a cost borne by all heap-allocated variables in every language. Unless people use special techniques like pool or slab allocators.
The real gains of managing memory manually is not in the speed of malloc(), but in the fact that in practice you can perform far fewer heap allocations.
At some point you have to consider how that hardware implements atomic variables and cmpxchg, because it is certainly not free.
You’re mistaken. In a multi core or multi processor system, atomic operations necessary for shared RC are relatively expensive. Heap allocation cost is paid for any dynamic object no matter the type of GC/RC used, so that is not what is being compared.
Moving objects between processes itself doesn't lead to fragmentation, as processes don't have their own heap in the data structure sense. That is, the structures representing a process don't have some sort of "heap" field. Instead, the OS threads running processes maintain the heaps (each thread has its own heap). This brings several benefits:
1. Since the heap is physically detached from processes, we can move without copying
2. Since the heap is still thread-local, allocations don't need synchronisation
3. Since the heap is detached from the processes, each process is smaller, allowing for more processes to run concurrently
4. Since objects (and their children) can only be owned by one process, we also don't have to worry about multiple processes trying to write into the same object
This is only possible because we statically guarantee that a value `T` can't be _shared_ between processes, and because we'll disallow sending references between processes.