Add XARR implementation
This commit is contained in:
parent
0226e83010
commit
002122b4d6
21
common/container/xarr/LICENSE
Normal file
21
common/container/xarr/LICENSE
Normal file
@ -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.
|
5
common/container/xarr/README.md
Normal file
5
common/container/xarr/README.md
Normal file
@ -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)
|
153
common/container/xarr/xarr.odin
Normal file
153
common/container/xarr/xarr.odin
Normal file
@ -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
|
||||||
|
}
|
122
common/container/xarr/xarr_test.odin
Normal file
122
common/container/xarr/xarr_test.odin
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user