Memory management
Autofree is still WIP. Until it stabilises and becomes the default, please avoid using it. Right now allocations are handled by a minimal and well performing GC until V's autofree engine is production ready.
V avoids doing unnecessary allocations in the first place by using value types, string buffers, promoting a simple abstraction-free code style.
Most objects (~90-100%) are freed by V's autofree engine: the compiler inserts necessary free calls automatically during compilation. Remaining small percentage of objects is freed via reference counting.
The developer does not need to change anything in their code. "It just works", like in Python, Go, or Java, except there's no heavy GC tracing everything or expensive RC for each object.
Control
You can take advantage of V's autofree engine and define a free()
method on custom
data types:
Just as the compiler frees C data types with C's free()
, it will statically insert
free()
calls for your data type at the end of each variable's lifetime.
Autofree can be enabled with an -autofree
flag.
For developers willing to have more low-level control, autofree can be disabled with
-manualfree
, or by adding a [manualfree]
on each function that wants to manage its
memory manually.
See attributes for more information.
For example:
The strings do not escape draw_text
, so they are cleaned up when
the function exits.
In fact, with the -prealloc
flag, the first two calls will not result in any allocations at all.
These two strings are small, so V will use a preallocated buffer for them.
Stack and Heap
Stack and Heap Basics
Like with most other programming languages, there are two locations where data can be stored:
- The stack allows fast allocations with almost zero administrative overhead. The stack grows and shrinks with the function call depth – so every called function has its stack segment that remains valid until the function returns. No freeing is necessary; however, this also means that a reference to a stack object becomes invalid on function return. Furthermore, stack space is limited (typically to a few Megabytes per thread).
- The heap is a large memory area (typically some Gigabytes) that is administrated by the operating system. Heap objects are allocated and freed by special function calls that delegate the administrative tasks to the OS. This means that they can remain valid across several function calls; however, the administration is expensive.
V's default approach
Due to performance considerations, V tries to put objects on the stack if possible but allocates them on the heap when obviously necessary. Example:
Here a
is stored on the stack since it's address never leaves the function f()
.
However, a reference to b
is part of e
which is returned. Also, a reference to
c
is returned. For this reason b
and c
will be heaped allocated.
Things become less obvious when a reference to an object is passed as function argument:
Here the call q.f(&w)
passes references to q
and w
because a
is
mut
and b
is of type &MyStruct
in f()
's declaration, so technically
these references are leaving main()
. However, the lifetime of these
references lies inside the scope of main()
so q
and w
are allocated
on the stack.
Manual Control for Stack and Heap
In the last example the V compiler could put q
and w
on the stack
because it assumed that in the call q.f(&w)
these references were only
used for reading and modifying the referred values – and not to pass the
references themselves somewhere else. This can be seen in a way that the
references to q
and w
are only borrowed to f()
.
Things become different if f()
is doing something with a reference itself:
Here f()
looks quite innocent but is doing nasty things – it inserts a
reference to s
into r
. The problem with this is that s
lives only as long
as g()
is running but r
is used in main()
after that. For this reason
the compiler would complain about the assignment in f()
because s
"might
refer to an object stored on stack". The assumption made in g()
that the call
r.f(&s)
would only borrow the reference to s
is wrong.
A solution to this dilemma is the [heap]
attribute at the declaration
of struct MyStruct
. It instructs the compiler to always allocate MyStruct
-objects
on the heap. This way the reference to s
remains valid even after g()
returns.
The compiler takes into consideration that MyStruct
objects are always heap
allocated when checking f()
and allows assigning the reference to s
to the
r.r
field.
There is a pattern often seen in other programming languages:
Here f()
is passed a reference a
as receiver that is passed back to the caller and returned
as a result at the same time.
The intention behind such a declaration is method chaining likey = x.f().g()
.
However, the problem with this approach is that a second reference
to a
is created – so it is not only borrowed and MyStruct
has to be
declared as [heap]
.
In V, the better approach is:
This way the [heap]
attribute can be avoided – resulting in better performance.
However, stack space is very limited as mentioned above. For this reason the [heap]
attribute might be suitable for very large structures even if not required by use cases
like those mentioned above.
There is an alternative way to manually control allocation on a case-to-case basis. This approach is not recommended but shown here for the sake of completeness:
Here the compiler check is suppressed by the unsafe
block. To make s
be heap
allocated even without [heap]
attribute the struct
literal is prefixed with
an ampersand: &MyStruct{...}
.
This last step would not be required by the compiler but without it the reference
inside r
becomes invalid (the memory area pointed to will be overwritten by
use_stack()
) and the program might crash (or at least produce an unpredictable
final output). That's why this approach is unsafe and should be avoided!