From 002122b4d6069d0e915a10f4d0eba5225af9e497 Mon Sep 17 00:00:00 2001 From: sergeypdev Date: Sun, 10 Aug 2025 01:41:59 +0400 Subject: [PATCH] Add XARR implementation --- common/container/xarr/LICENSE | 21 ++++ common/container/xarr/README.md | 5 + common/container/xarr/xarr.odin | 153 +++++++++++++++++++++++++++ common/container/xarr/xarr_test.odin | 122 +++++++++++++++++++++ 4 files changed, 301 insertions(+) create mode 100644 common/container/xarr/LICENSE create mode 100644 common/container/xarr/README.md create mode 100644 common/container/xarr/xarr.odin create mode 100644 common/container/xarr/xarr_test.odin diff --git a/common/container/xarr/LICENSE b/common/container/xarr/LICENSE new file mode 100644 index 0000000..80b5198 --- /dev/null +++ b/common/container/xarr/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Sergei Pozniak + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/common/container/xarr/README.md b/common/container/xarr/README.md new file mode 100644 index 0000000..51514ad --- /dev/null +++ b/common/container/xarr/README.md @@ -0,0 +1,5 @@ +# XARR Implementation in Odin + +Growable dynamic array without reallocation, Arena friendly. + +Based on this awesome talk at Better Software Conference: [Andrew Reece – Assuming as Much as Possible – BSC 2025](https://www.youtube.com/watch?v=i-h95QIGchY&t=2s) diff --git a/common/container/xarr/xarr.odin b/common/container/xarr/xarr.odin new file mode 100644 index 0000000..84e25ff --- /dev/null +++ b/common/container/xarr/xarr.odin @@ -0,0 +1,153 @@ +package xarr + +import "base:builtin" +import "base:intrinsics" + +BASE_CHUNK_SIZE :: uint(64) +BASE_CHUNK_SIZE_LOG2 :: intrinsics.constant_log2(BASE_CHUNK_SIZE) +BASE_CHUNK_SHIFT :: BASE_CHUNK_SIZE_LOG2 - 1 + +Xarr :: struct($T: typeid) { + len: int, + chunks: [30][^]T, + allocated_chunks_mask: u32, +} + +UINT_BITS :: size_of(uint) * 8 + +msb :: #force_inline proc "contextless" (#any_int idx: uint) -> i8 { + return i8(UINT_BITS - intrinsics.count_leading_zeros(idx)) - 1 +} + +chunk_by_index :: #force_inline proc "contextless" (#any_int idx: uint) -> (chunk: i8) { + return max(msb(idx) - BASE_CHUNK_SHIFT, 0) +} + +chunk_size :: #force_inline proc "contextless" (chunk_idx: i8) -> uint { + return BASE_CHUNK_SIZE << u32(max(chunk_idx - 1, 0)) +} + +get_chunk_slice :: #force_inline proc "contextless" (a: $T/Xarr($E), chunk_idx: i8) -> []E { + return a.chunks[chunk_idx][:chunk_size(chunk_idx)] +} + +capacity_from_allocated_mask :: #force_inline proc(allocated_mask: uint) -> uint { + return( + (allocated_mask >> 1) << BASE_CHUNK_SIZE_LOG2 + + (allocated_mask & 1) << BASE_CHUNK_SIZE_LOG2 \ + ) +} + +capacity :: #force_inline proc(a: $T/Xarr($E)) -> u32 { + allocated_mask := a.allocated_chunks_mask + return capacity_from_allocated_mask(allocated_mask) +} + +reserve :: proc(a: $T/^Xarr($E), cap: int, allocator := context.allocator) { + allocated_mask := a.allocated_chunks_mask + + current_chunk := msb(allocated_mask) + required_chunks := chunk_by_index(max(cap - 1, 0)) + 1 + + for i := current_chunk + 1; i < required_chunks; i += 1 { + chunk_slice := make([]E, chunk_size(i), allocator) + a.chunks[i] = raw_data(chunk_slice) + a.allocated_chunks_mask |= u32(1) << u8(i) + } +} + +append :: proc(a: $T/^Xarr($E), elems: ..E, allocator := context.allocator) { + if len(elems) == 0 { + return + } + + reserve(a, a.len + len(elems)) + set_elems_assume_allocated(a^, elems) + a.len += len(elems) +} + +translate_index :: #force_inline proc( + #any_int idx: int, +) -> ( + chunk_idx: i8, + idx_within_chunk: uint, +) { + assert(idx >= 0) + + chunk_idx = chunk_by_index(idx) + idx_within_chunk = uint(idx) & (chunk_size(chunk_idx) - 1) + + return +} + +@(private = "file") +set_elems_assume_allocated :: proc(a: $T/Xarr($E), elems: []E) { + for &e, i in elems { + idx := a.len + i + chunk_idx, idx_within_chunk := translate_index(idx) + assert(a.chunks[chunk_idx] != nil) + + a.chunks[chunk_idx][idx_within_chunk] = e + } +} + +set :: proc(a: $T/Xarr($E), #any_int idx: int, val: E) { + assert(idx >= 0 && idx < a.len) + chunk_idx, idx_within_chunk := translate_index(idx) + return get_chunk_slice(a, chunk_idx)[idx_within_chunk] +} + +get :: proc(a: $T/Xarr($E), #any_int idx: int) -> E { + assert(idx >= 0 && idx < a.len) + + chunk_idx, idx_within_chunk := translate_index(idx) + return get_chunk_slice(a, chunk_idx)[idx_within_chunk] +} + +get_ptr :: proc(a: $T/Xarr($E), #any_int idx: int) -> ^E { + assert(idx >= 0 && idx < a.len) + + chunk_idx, idx_within_chunk := translate_index(idx) + return &get_chunk_slice(a, chunk_idx)[idx_within_chunk] +} + +unordered_remove :: proc(a: $T/^Xarr($E), #any_int idx: int) { + assert(idx >= 0 && idx < a.len) + + get_ptr(a^, idx)^ = get(a^, a.len - 1) + a.len -= 1 +} + +clear :: proc "contextless" (a: $T/^Xarr($E)) { + a.len = 0 +} + +delete :: proc(a: $T/^Xarr($E), allocator := context.allocator) { + for i in 0 ..< len(a.chunks) { + builtin.delete(get_chunk_slice(a^, i8(i)), allocator) + } + + a^ = Xarr(E){} +} + +Iterator :: struct($E: typeid) { + xarr: ^Xarr(E), + idx: int, +} + +iterator :: proc(a: $T/^Xarr($E), start_idx := 0) -> Iterator(E) { + return Iterator(E){xarr = a, idx = start_idx} +} + +iterator_next :: proc(it: ^Iterator($E)) -> (e: ^E, idx: int, ok: bool) { + if it.idx >= it.xarr.len { + return nil, it.idx, false + } + + e = get_ptr(it.xarr^, it.idx) + idx = it.idx + ok = true + + it.idx += 1 + return +} diff --git a/common/container/xarr/xarr_test.odin b/common/container/xarr/xarr_test.odin new file mode 100644 index 0000000..b3bbc74 --- /dev/null +++ b/common/container/xarr/xarr_test.odin @@ -0,0 +1,122 @@ +package xarr + +import "core:testing" + +@(test) +test_msb :: proc(t: ^testing.T) { + testing.expect_value(t, msb(0), -1) + testing.expect_value(t, msb(1), 0) + testing.expect_value(t, msb(2), 1) + testing.expect_value(t, msb(3), 1) + testing.expect_value(t, msb(4), 2) + testing.expect_value(t, msb(5), 2) + testing.expect_value(t, msb(6), 2) + testing.expect_value(t, msb(7), 2) + testing.expect_value(t, msb(8), 3) + testing.expect_value(t, msb(16), 4) + testing.expect_value(t, msb(64), 6) +} + +@(test) +test_chunk_sizes :: proc(t: ^testing.T) { + testing.expect_value(t, chunk_size(0), BASE_CHUNK_SIZE) + testing.expect_value(t, chunk_size(1), BASE_CHUNK_SIZE) + testing.expect_value(t, chunk_size(2), BASE_CHUNK_SIZE * 2) + testing.expect_value(t, chunk_size(3), BASE_CHUNK_SIZE * 4) + testing.expect_value(t, chunk_size(4), BASE_CHUNK_SIZE * 8) +} + +@(test) +test_capacity_from_mask :: proc(t: ^testing.T) { + testing.expect_value(t, capacity_from_allocated_mask(0b1), chunk_size(0)) + testing.expect_value(t, capacity_from_allocated_mask(0b11), chunk_size(0) + chunk_size(1)) + testing.expect_value( + t, + capacity_from_allocated_mask(0b111), + chunk_size(0) + chunk_size(1) + chunk_size(2), + ) + testing.expect_value( + t, + capacity_from_allocated_mask(0b1111), + chunk_size(0) + chunk_size(1) + chunk_size(2) + chunk_size(3), + ) + testing.expect_value( + t, + capacity_from_allocated_mask(0b11111), + chunk_size(0) + chunk_size(1) + chunk_size(2) + chunk_size(3) + chunk_size(4), + ) +} + +@(test) +test_indexing :: proc(t: ^testing.T) { + chunk, idx := translate_index(0) + testing.expect_value(t, chunk, 0) + testing.expect_value(t, idx, 0) + + chunk, idx = translate_index(BASE_CHUNK_SIZE - 1) + testing.expect_value(t, chunk, 0) + testing.expect_value(t, idx, BASE_CHUNK_SIZE - 1) + + chunk, idx = translate_index(BASE_CHUNK_SIZE) + testing.expect_value(t, chunk, 1) + testing.expect_value(t, idx, 0) + + chunk, idx = translate_index(BASE_CHUNK_SIZE * 3 - 1) + testing.expect_value(t, chunk, 2) + testing.expect_value(t, idx, BASE_CHUNK_SIZE - 1) + + chunk, idx = translate_index(BASE_CHUNK_SIZE * 5) + testing.expect_value(t, chunk, 3) + testing.expect_value(t, idx, BASE_CHUNK_SIZE) +} + +@(test) +test_basic :: proc(t: ^testing.T) { + a: Xarr(int) + defer delete(&a) + + NUM :: 10000 + RUNS :: 4 + + for _ in 0 ..< RUNS { + defer clear(&a) + + for i in 0 ..< NUM { + append(&a, i) + } + + testing.expect_value(t, a.len, NUM) + + for i in 0 ..< NUM { + testing.expect_value(t, get(a, i), i) + } + } +} + +@(test) +test_remove :: proc(t: ^testing.T) { + a: Xarr(int) + defer delete(&a) + + append(&a, 1, 2, 3, 4) + + unordered_remove(&a, 1) + + testing.expect_value(t, a.len, 3) + testing.expect_value(t, get(a, 0), 1) + testing.expect_value(t, get(a, 1), 4) + testing.expect_value(t, get(a, 2), 3) +} + +@(test) +test_iterator :: proc(t: ^testing.T) { + a: Xarr(int) + defer delete(&a) + + append(&a, 0, 1, 2, 3, 4) + + it := iterator(&a) + for e, i in iterator_next(&it) { + testing.expect_value(t, e^, i) + } +}