Memory management

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 doesn’t 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:

  1. struct MyType {}
  2. [unsafe]
  3. fn (data &MyType) free() {
  4. // ...
  5. }

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 manage its memory manually. (See attributes).

Note 2: 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.

Examples

  1. import strings
  2. fn draw_text(s string, x int, y int) {
  3. // ...
  4. }
  5. fn draw_scene() {
  6. // ...
  7. name1 := 'abc'
  8. name2 := 'def ghi'
  9. draw_text('hello $name1', 10, 10)
  10. draw_text('hello $name2', 100, 10)
  11. draw_text(strings.repeat(`X`, 10000), 10, 50)
  12. // ...
  13. }

The strings don’t escape draw_text, so they are cleaned up when the function exits.

In fact, with the -prealloc flag, the first two calls won’t result in any allocations at all. These two strings are small, so V will use a preallocated buffer for them.

  1. struct User {
  2. name string
  3. }
  4. fn test() []int {
  5. number := 7 // stack variable
  6. user := User{} // struct allocated on stack
  7. numbers := [1, 2, 3] // array allocated on heap, will be freed as the function exits
  8. println(number)
  9. println(user)
  10. println(numbers)
  11. numbers2 := [4, 5, 6] // array that's being returned, won't be freed here
  12. return numbers2
  13. }

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:

  1. struct MyStruct {
  2. n int
  3. }
  4. struct RefStruct {
  5. r &MyStruct
  6. }
  7. fn main() {
  8. q, w := f()
  9. println('q: $q.r.n, w: $w.n')
  10. }
  11. fn f() (RefStruct, &MyStruct) {
  12. a := MyStruct{
  13. n: 1
  14. }
  15. b := MyStruct{
  16. n: 2
  17. }
  18. c := MyStruct{
  19. n: 3
  20. }
  21. e := RefStruct{
  22. r: &b
  23. }
  24. x := a.n + c.n
  25. println('x: $x')
  26. return e, &c
  27. }

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 heap allocated.

Things become less obvious when a reference to an object is passed as function argument:

  1. struct MyStruct {
  2. mut:
  3. n int
  4. }
  5. fn main() {
  6. mut q := MyStruct{
  7. n: 7
  8. }
  9. w := MyStruct{
  10. n: 13
  11. }
  12. x := q.f(&w) // references of `q` and `w` are passed
  13. println('q: $q\nx: $x')
  14. }
  15. fn (mut a MyStruct) f(b &MyStruct) int {
  16. a.n += b.n
  17. x := a.n * b.n
  18. return x
  19. }

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:

  1. struct RefStruct {
  2. mut:
  3. r &MyStruct
  4. }
  5. // see discussion below
  6. [heap]
  7. struct MyStruct {
  8. n int
  9. }
  10. fn main() {
  11. m := MyStruct{}
  12. mut r := RefStruct{
  13. r: &m
  14. }
  15. r.g()
  16. println('r: $r')
  17. }
  18. fn (mut r RefStruct) g() {
  19. s := MyStruct{
  20. n: 7
  21. }
  22. r.f(&s) // reference to `s` inside `r` is passed back to `main() `
  23. }
  24. fn (mut r RefStruct) f(s &MyStruct) {
  25. r.r = s // would trigger error without `[heap]`
  26. }

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:

  1. // failcompile
  2. fn (mut a MyStruct) f() &MyStruct {
  3. // do something with a
  4. return &a // would return address of borrowed object
  5. }

Here f() is passed a reference a as receiver that is passed back to the caller and returned as result at the same time. The intention behind such a declaration is method chaining like y = 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:

  1. struct MyStruct {
  2. mut:
  3. n int
  4. }
  5. fn (mut a MyStruct) f() {
  6. // do something with `a`
  7. }
  8. fn (mut a MyStruct) g() {
  9. // do something else with `a`
  10. }
  11. fn main() {
  12. x := MyStruct{} // stack allocated
  13. mut y := x
  14. y.f()
  15. y.g()
  16. // instead of `mut y := x.f().g()
  17. }

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:

  1. struct MyStruct {
  2. n int
  3. }
  4. struct RefStruct {
  5. mut:
  6. r &MyStruct
  7. }
  8. // simple function - just to overwrite stack segment previously used by `g()`
  9. fn use_stack() {
  10. x := 7.5
  11. y := 3.25
  12. z := x + y
  13. println('$x $y $z')
  14. }
  15. fn main() {
  16. m := MyStruct{}
  17. mut r := RefStruct{
  18. r: &m
  19. }
  20. r.g()
  21. use_stack() // to erase invalid stack contents
  22. println('r: $r')
  23. }
  24. fn (mut r RefStruct) g() {
  25. s := &MyStruct{ // `s` explicitly refers to a heap object
  26. n: 7
  27. }
  28. // change `&MyStruct` -> `MyStruct` above and `r.f(s)` -> `r.f(&s)` below
  29. // to see data in stack segment being overwritten
  30. r.f(s)
  31. }
  32. fn (mut r RefStruct) f(s &MyStruct) {
  33. r.r = unsafe { s } // override compiler check
  34. }

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!