ручное управление памятью - ponyatov/nimbook GitHub Wiki

ручное управление памятью

Manual dispose

A GC was added to Nim because back then this seemed like the best solution to ensure memory safety. In the meantime programming language research advanced and there are solutions that can give us memory safety without a GC.

Rust-like borrowing extensions are not the only mechanism to accomplish this, there are many different solutions to explore.

So let's consider manual dispose calls. Can we have them and memory safety at the same time? Yes! And a couple of experimental programming languages (Cockoo and some dialect of C#) implemented this solution. One key insight is that new/dispose need to provide a type-safe interface, the memory is served by type-specific memory allocators. That means that the memory used up for type T will only be reused for other instances of type T.

Here is an example that shows what this means in practice:

type
  Node = ref object
    data: int

var x = Node(data: 3)
let dangling = x
assert dangling.data == 3
dispose(x)
x = Node(data: 4)
assert dangling.data in {3, 4}

Usually accessing dangling.data would be a "use after free" bug but since dispose returns the memory to a type-safe memory pool we know that x = Node(data: 4) will allocate memory from the same pool; either by re-using the object that previously had the value 3 (then we know that dangling.data == 4) or by creating a fresh object (then we know dangling.data == 3).

Type-specific allocation turns every "use after free" bug into a logical bug but no memory corruption can happen. So ... we have already accomplished "memory safety without a GC". It didn't require a borrow checker nor an advanced type system. It is interesting to compare this to an example that uses array indexing instead of pointers:

type
  Node = object
    data: int

var nodes: array[4, Node]

var x = 1
nodes[x] = Node(data: 3)
let dangling = x
assert nodes[dangling].data == 3
nodes[x] = Node(data: 4)
assert nodes[dangling].data == 4

So if the allocator re-uses dispose'd memory as quickly as possible we can reproduce the same results as the array version. However, this mechanism produces different results than the GC version:

type
  Node = ref object
    data: int

var x = Node(data: 3)
let dangling = x
assert dangling.data == 3
x = Node(data: 4)
# note: the 'dangling' pointer keeps the object alive
# and so the value is still 3:
assert dangling.data == 3

The GC transforms the use-after-free bug into hopefully correct behaviour -- or into logical memory leaks as liveness is approximated by reachability. Programmers are encouraged to not think about memory and resource management, but in my experience thinking a little about these is required for writing robust software.

Philosophy aside, porting code that uses garbage collection over to code that has to use manual dispose calls everywhere which can then produce subtle changes in behaviour is not a good solution. However, we will keep in mind that type-safe memory reuse is all that it takes for memory safety.

This is not "cheating" either, for example https://www.usenix.org/legacy/event/sec10/tech/full_papers/Akritidis.pdf also tries to mitigate memory handling bugs with this idea.