odin-lang / Odin

Odin Programming Language
https://odin-lang.org
BSD 3-Clause "New" or "Revised" License
6.1k stars 550 forks source link

Consistent dynamic array default capacity and avoid unnecessary dynamic array allocations #3833

Closed karl-zylinski closed 3 days ago

karl-zylinski commented 3 days ago

Behavior before

Before these changes, if you did:

arr: [dynamic]int

and then append to arr, then arr will have capacity 8.

But if you did

arr := make([dynamic]int, context.temp_allocator)

then arr would have capacity 16.

Behavior now

Now both arr: [dynamic]int and arr := make([dynamic]int, context.temp_allocator) will result in arr having cap 0.

The only reason to use make without an explicit len or cap now is because you want to set it up for a non-default allocator. After the first call to append it will now in both cases have capacity 8.

There was a hard-coded 8 in append and then there was a default reserve capacity constant set to 16. The constant has been changed to 8 and is used in append now, while make uses capacity 0.

I have updated the documentation in the strings builder to reflect this. There were also some inaccuracies in there that weren't true even before these changes (such as len being max(16, len) when you specify len for a builder).

Positive side effects of this

Say you have a proc like this, that fetches all trees that are near a video game player within a certain distance:

find_all_nearby_trees :: proc(max_distance: f32, allocator := context.allocator) -> []^Tree {
    trees := make([dynamic]^Tree, allocator)

    for &t in world.trees {
        if linalg.length(t.pos - player.pos) < max_distance {
            append(&trees, &t)
        }
    }

    return trees[:]
}

Then if there are no trees nearby, then with the new code no allocation at all will happen. With the old code there would always be an allocation of capacity 16.

Tests done

I tested this using this program

package dyn_arr_cap

import "core:fmt"
import "core:strings"

test :: proc() {
    fmt.println("Testing dynamic array")
    dyn_arr := make([dynamic]int, context.temp_allocator)
    fmt.println(cap(dyn_arr)) // 0
    append(&dyn_arr, 7)
    append(&dyn_arr, 8, 9, 10)
    fmt.println(cap(dyn_arr)) // 8

    fmt.println(dyn_arr) // [7, 8, 9, 10]
}

test_soa :: proc() {
    fmt.println("Testing soa dynamic array")
    My_Struct :: struct {
        v1: int,
        v2: int,
        v3: int,
        v4: int,
        v5: int,
        v6: int,
    }

    dyn_arr := make_soa(#soa[dynamic]My_Struct, context.temp_allocator)
    fmt.println(cap(dyn_arr)) // 0
    append_soa(&dyn_arr, My_Struct{1,2,3,4,5,6})
    append_soa(&dyn_arr, My_Struct{1,2,3,4,5,6}, My_Struct{1,2,3,4,5,6}, My_Struct{1,2,3,4,5,6})
    fmt.println(cap(dyn_arr)) // 8

    fmt.println(dyn_arr) // [My_Struct{v1 = 1, v2 = 2, v3 = 3, v4 = 4, v5 = 5, v6 = 6}, My_Struct{v1 = 1, v2 = 2, v3 = 3, v4 = 4, v5 = 5, v6 = 6}, My_Struct{v1 = 1, v2 = 2, v3 = 3, v4 = 4, v5 = 5, v6 = 6}, My_Struct{v1 = 1, v2 = 2, v3 = 3, v4 = 4, v5 = 5, v6 = 6}]
}

test_builder :: proc() {
    fmt.println("Testing builder")
    b := strings.builder_make(context.temp_allocator)
    fmt.println(cap(b.buf)) // 0
    strings.write_string(&b, "Hellope!")
    fmt.println(cap(b.buf)) // 8
    strings.write_string(&b, " Bye!")
    fmt.println(cap(b.buf)) // 24 (8 * 2 + 8)
    fmt.println(strings.to_string(b)) // "Hellope! Bye!"
}

main :: proc() {
    test()
    test_soa()
    test_builder()
}

Output:

Testing dynamic array
0
8
[7, 8, 9, 10]
Testing soa dynamic array
0
8
[My_Struct{v1 = 1, v2 = 2, v3 = 3, v4 = 4, v5 = 5, v6 = 6}, My_Struct{v1 = 1, v2 = 2, v3 = 3, v4 = 4, v5 = 5, v6 = 6}, My_Struct{v1 = 1, v2 = 2, v3 = 3, v4 = 4, v5 = 5, v6 = 6}, My_Struct{v1 = 1, v2 = 2, v3 = 3, v4 = 4, v5 = 5, v6 = 6}]
Testing builder
0
8
24
Hellope! Bye!

I also tested this with my current game prototype, it all worked fine.