// In order to test the heap allocator in a deterministic manner, we must run // this program without the help of the test runner, because the runner itself // makes use of heap allocation. package tests_heap_allocator import "base:intrinsics" import "base:runtime" import "core:flags" import "core:fmt" import "core:log" import "core:math/rand" import "core:os" import "core:sync" import "core:thread" import "core:time" import "core:mem" import libc_allocator "libc" // The tests are specific to feoramalloc, but the benchmarks are general-purpose. // // Utility // expect :: proc "contextless" (condition: bool, message := #caller_expression(condition), loc := #caller_location) { if !condition { @(cold) internal :: proc "contextless" (message: string, loc: runtime.Source_Code_Location) { runtime.print_string("\n* Expectation failed: ") runtime.print_string(message) runtime.print_string(" @ ") runtime.print_caller_location(loc) runtime.print_string("\n\n") when ODIN_DEBUG { intrinsics.debug_trap() } else { intrinsics.trap() } } internal(message, loc) } } verify_zeroed_slice :: proc(bytes: []byte, loc := #caller_location) { for b in bytes { expect(b == 0, loc = loc) } } verify_zeroed_ptr :: proc(ptr: [^]byte, size: int, loc := #caller_location) { for i := 0; i < size; i += 1 { expect(ptr[i] == 0, loc = loc) } } verify_zeroed :: proc { verify_zeroed_slice, verify_zeroed_ptr, } verify_integrity_slice :: proc(bytes: []byte, seed: u64, loc := #caller_location) { buf: [1]byte rand.reset(seed) for i := 0; i < len(bytes); i += len(buf) { expect(rand.read(buf[:]) == len(buf), loc = loc) length := min(len(buf), len(bytes) - i) for j := 0; j < length; j += 1 { expect(bytes[i+j] == buf[j], loc = loc) } } } verify_integrity_ptr :: proc(ptr: [^]byte, size: int, seed: u64, loc := #caller_location) { verify_integrity_slice(transmute([]byte)runtime.Raw_Slice{ data = ptr, len = size, }, seed, loc) } verify_integrity :: proc { verify_integrity_slice, verify_integrity_ptr, } randomize_bytes_slice :: proc(bytes: []byte, seed: u64, loc := #caller_location) { rand.reset(seed) buf: [1]byte for i := 0; i < len(bytes); i += len(buf) { expect(rand.read(buf[:]) == len(buf), loc = loc) length := min(len(buf), len(bytes) - i) for j := 0; j < length; j += 1 { bytes[i+j] = buf[j] } } } randomize_bytes_ptr :: proc(ptr: [^]byte, size: int, seed: u64, loc := #caller_location) { randomize_bytes_slice(transmute([]byte)runtime.Raw_Slice{ data = ptr, len = size, }, seed, loc) } randomize_bytes :: proc { randomize_bytes_slice, randomize_bytes_ptr, } // // Allocation API Testing // Size_Strategy :: enum { Adding, Multiplying, Randomizing, } Free_Strategy :: enum { Never, At_The_End, // free at end of allocs Interleaved, // free X after Y allocs } Free_Direction :: enum { Forward, // like a queue Backward, // like a stack Randomly, } test_alloc_write_free :: proc( object_count: int, starting_size: int, final_size: int, size_strategy: Size_Strategy, size_operand: int, allocs_per_free_operation: int, free_operations_at_once: int, free_strategy: Free_Strategy, free_direction: Free_Direction, ) { Allocation :: struct { data: []byte, seed: u64, } pointers := make([]Allocation, object_count, context.temp_allocator) allocator := context.allocator size := starting_size start_index := 0 end_index := 0 allocs := 0 log.infof("AWF: %i objects. Size: [%i..=%i] %v by %i each allocation. %i freed every %i, %v and %v.", object_count, starting_size, final_size, size_strategy, size_operand, free_operations_at_once, allocs_per_free_operation, free_strategy, free_direction) for o in 1..=u64(object_count) { seed := u64(intrinsics.read_cycle_counter()) * o alignment := min(size, runtime.ODIN_HEAP_MAX_ALIGNMENT) bytes, alloc_err := allocator.procedure(allocator.data, .Alloc, size, alignment, nil, 0) expect(alloc_err == nil) pointers[end_index] = Allocation{ data = bytes, seed = seed, } end_index += 1 allocs += 1 verify_zeroed(bytes) randomize_bytes(bytes, seed) if size < final_size { switch size_strategy { case .Adding: size += size_operand case .Multiplying: size *= size_operand case .Randomizing: size = starting_size + rand.int_max(final_size - starting_size) } if final_size > starting_size { size = min(size, final_size) } else { size = max(size, final_size) } } if allocs % allocs_per_free_operation != 0 { continue } switch free_strategy { case .Never, .At_The_End: break case .Interleaved: for _ in 0.. 0 && size % (runtime.ODIN_HEAP_MAX_BIN_SIZE/8) == 0 { log.infof("... %i ...", size) } alignment := min(size, runtime.ODIN_HEAP_MAX_ALIGNMENT) // Allocate and free twice to make sure that the memory is truly zeroed. // // This works on the assumption that the allocator will return the same // pointer if we allocate, free, then allocate again with the same // characteristics. // // libc malloc does not guarantee this behavior, but feoramalloc does // in non-parallel scenarios. old_ptr: rawptr for i in 0..<2 { bytes, alloc_err := allocator.procedure(allocator.data, .Alloc, size, alignment, nil, 0) if i == 0 { old_ptr = raw_data(bytes) } else { if old_ptr != raw_data(bytes) { different_pointers += 1 } } expect(alloc_err == nil) verify_zeroed(bytes) randomize_bytes(bytes, u64(intrinsics.read_cycle_counter())) _, free_err := allocator.procedure(allocator.data, .Free, 0, 0, raw_data(bytes), 0) expect(free_err == nil) } } if different_pointers > 0 { log.warnf("There were %i cases in which the allocator didn't return the same pointer after allocating, freeing, then allocating again with the same size.", different_pointers) } log.info("Done.") } test_single_alloc_and_resize :: proc(start, target: int) { log.infof("Testing allocation of %i bytes, resizing to %i, then resizing back.", start, target) allocator := context.allocator base_seed := u64(intrinsics.read_cycle_counter()) alignment := min(start, runtime.ODIN_HEAP_MAX_ALIGNMENT) seed := base_seed * (1+u64(start)) bytes, alloc_err := allocator.procedure(allocator.data, .Alloc, start, alignment, nil, 0) expect(alloc_err == nil) expect(len(bytes) == start) verify_zeroed(bytes) randomize_bytes(bytes, seed) resized_bytes_1, resize_1_err := allocator.procedure(allocator.data, .Resize, target, alignment, raw_data(bytes), start) expect(resize_1_err == nil) expect(len(resized_bytes_1) == target) verify_integrity(resized_bytes_1[:min(start, target)], seed) if target > start { verify_zeroed(resized_bytes_1[start:]) randomize_bytes(resized_bytes_1[start:], seed) } resized_bytes_2, resize_2_err := allocator.procedure(allocator.data, .Resize, start, alignment, raw_data(resized_bytes_1), target) expect(resize_2_err == nil) expect(len(resized_bytes_2) == start) verify_integrity(resized_bytes_2[:min(start, target)], seed) if start > target { verify_zeroed(resized_bytes_2[target:]) } _, free_err := allocator.procedure(allocator.data, .Free, 0, 0, raw_data(resized_bytes_2), 0) expect(free_err == nil) } /* This test helped find an issue with the orphanage. */ test_parallel_pointer_passing :: proc(thread_count: int) { Data :: struct { thread: ^thread.Thread, ptr: ^^int, sema: sync.Sema, friend: ^sync.Sema, wg: ^sync.Wait_Group, } task :: proc(t: ^thread.Thread) { data := cast(^Data)t.data sync.wait(&data.sema) expect(data.ptr != nil) expect(data.ptr^ != nil) expect(data.ptr^^ != 0) free(data.ptr^) data.ptr^ = new(int) expect(data.ptr^^ == 0) data.ptr^^ = int(intrinsics.read_cycle_counter()) if data.friend != nil { sync.post(data.friend) } sync.wait_group_done(data.wg) } data := new(int) data^ = int(intrinsics.read_cycle_counter()) tasks := make([]Data, thread_count, context.temp_allocator) wg: sync.Wait_Group sync.wait_group_add(&wg, thread_count) for i in 0..= intrinsics.atomic_load_explicit(data.all_pointers_len, .Acquire) { // Spinlock. intrinsics.cpu_relax() } intrinsics.atomic_thread_fence(.Seq_Cst) ptr := data.all_pointers[ticket] expect(ptr != nil) i_ptr := cast(^i64)ptr expect(i_ptr^ == 0) val := intrinsics.read_cycle_counter() i_ptr^ = val expect(i_ptr^ == val) free(ptr) } sync.wait_group_done(data.wg) } start_time: time.Time barrier: sync.Barrier sync.barrier_init(&barrier, thread_count) wg: sync.Wait_Group sync.wait_group_add(&wg, thread_count) // non-atomic all_pointers := make([]rawptr, thread_count*allocs_per_thread) defer delete(all_pointers) // atomic all_pointers_len := 0 all_pointers_ticket := 0 consumers := make([]Consumer_Data, thread_count) defer delete(consumers) for i in 0.. 0 { log.debugf("Testing superpage allocation and alignment ...") v := runtime.allocate_virtual_memory_superpage() expect(uintptr(v) % uintptr(size) == 0) va := cast([^]u8)v for i in 0.. Large test_single_alloc_and_resize(runtime.ODIN_HEAP_MIN_BIN_SIZE, 1 + runtime.ODIN_HEAP_SMALL_BIN_MAX) test_single_alloc_and_resize(1 + runtime.ODIN_HEAP_SMALL_BIN_MAX, runtime.ODIN_HEAP_MIN_BIN_SIZE) // Small <-> Huge test_single_alloc_and_resize(runtime.ODIN_HEAP_MIN_BIN_SIZE, 1 + runtime.ODIN_HEAP_MAX_BIN_SIZE) test_single_alloc_and_resize(1 + runtime.ODIN_HEAP_MAX_BIN_SIZE, runtime.ODIN_HEAP_MIN_BIN_SIZE) // Large <-> Huge test_single_alloc_and_resize(1 + runtime.ODIN_HEAP_MAX_BIN_SIZE, 1 + runtime.ODIN_HEAP_SMALL_BIN_MAX) test_single_alloc_and_resize(1 + runtime.ODIN_HEAP_SMALL_BIN_MAX, 1 + runtime.ODIN_HEAP_MAX_BIN_SIZE) // Brute-force tests. test_individual_allocation_and_free(runtime.ODIN_HEAP_MAX_BIN_SIZE if opt.long else 1024) test_continuous_allocation_of_size_n(16, runtime.ODIN_HEAP_MAX_BIN_SIZE if opt.long else 1024) test_alloc_write_free( object_count = 400, starting_size = 16, final_size = 16, size_strategy = .Adding, size_operand = 0, allocs_per_free_operation = 16, free_operations_at_once = 4, free_strategy = .Interleaved, free_direction = .Randomly, ) test_alloc_write_free( object_count = 100, starting_size = 2, final_size = 4096, size_strategy = .Multiplying, size_operand = 2, allocs_per_free_operation = 16, free_operations_at_once = 4, free_strategy = .Interleaved, free_direction = .Randomly, ) test_alloc_write_free( object_count = 100, starting_size = 2, final_size = 32768, size_strategy = .Adding, size_operand = 2, allocs_per_free_operation = 16, free_operations_at_once = 4, free_strategy = .Interleaved, free_direction = .Randomly, ) test_alloc_write_free( object_count = 100, starting_size = 2, final_size = 8096, size_strategy = .Adding, size_operand = 2, allocs_per_free_operation = 16, free_operations_at_once = 15, free_strategy = .Interleaved, free_direction = .Backward, ) test_alloc_write_free( object_count = 10, starting_size = 8096, final_size = 2, size_strategy = .Adding, size_operand = -2, allocs_per_free_operation = 16, free_operations_at_once = 15, free_strategy = .At_The_End, free_direction = .Forward, ) test_alloc_write_free( object_count = 10, starting_size = 65535, final_size = 65535, size_strategy = .Adding, size_operand = 0, allocs_per_free_operation = 1, free_operations_at_once = 1, free_strategy = .At_The_End, free_direction = .Forward, ) test_alloc_write_free( object_count = 300000, starting_size = 8, final_size = 8, size_strategy = .Adding, size_operand = 0, allocs_per_free_operation = 1, free_operations_at_once = 1, free_strategy = .At_The_End, free_direction = .Forward, ) // This is a lengthy test and won't tell us much more than any other test will. if opt.long { test_single_alloc_and_resize_incremental(0, runtime.ODIN_HEAP_MAX_BIN_SIZE) } runtime.compact_local_heap() } if opt.serial_benchmarks { log.info("--- Single-threaded benchmarks ---") log.info("* Freeing forwards ...") bench_alloc_n_then_free_n(10_000_000, int) bench_alloc_n_then_free_n(10_000_000, Struct_16) bench_alloc_n_then_free_n(10_000_000, Struct_32) bench_alloc_n_then_free_n(10_000_000, Struct_64) bench_alloc_n_then_free_n(10_000_000, Struct_512) bench_alloc_n_then_free_n(100_000, [8192]u8) bench_alloc_n_then_free_n(100_000, [4096*4]u8) bench_alloc_n_then_free_n(10_000, [65536/4]u8) bench_alloc_n_then_free_n(10_000, [65536*4]u8) log.info("* Freeing backwards ...") bench_alloc_n_then_free_n_backwards(10_000_000, int) bench_alloc_n_then_free_n_backwards(10_000_000, Struct_16) bench_alloc_n_then_free_n_backwards(10_000_000, Struct_32) bench_alloc_n_then_free_n_backwards(10_000_000, Struct_64) bench_alloc_n_then_free_n_backwards(10_000_000, Struct_512) bench_alloc_n_then_free_n_backwards(100_000, [8192]u8) bench_alloc_n_then_free_n_backwards(10_000, [65536/4]u8) bench_alloc_n_then_free_n_backwards(10_000, [65536*4]u8) log.info("* Freeing randomly ...") bench_alloc_n_then_free_n_randomly(10_000_000, int) bench_alloc_n_then_free_n_randomly(10_000_000, Struct_16) bench_alloc_n_then_free_n_randomly(10_000_000, Struct_32) bench_alloc_n_then_free_n_randomly(10_000_000, Struct_64) bench_alloc_n_then_free_n_randomly(10_000_000, Struct_512) bench_alloc_n_then_free_n_randomly(100_000, [8192]u8) bench_alloc_n_then_free_n_randomly(100_000, [65536/4]u8) bench_alloc_n_then_free_n_randomly(100_000, [65536]u8) bench_alloc_n_then_free_n_randomly(100_000, [65536*2]u8) log.info("* Allocating and freeing repeatedly ...") bench_alloc_1_then_free_1_repeatedly(100_000, int) bench_alloc_1_then_free_1_repeatedly(100_000, Struct_16) bench_alloc_1_then_free_1_repeatedly(100_000, Struct_32) bench_alloc_1_then_free_1_repeatedly(100_000, Struct_64) bench_alloc_1_then_free_1_repeatedly(100_000, Struct_512) bench_alloc_1_then_free_1_repeatedly(10_000, [8192]u8) } if opt.parallel_benchmarks { log.info("--- Multi-threaded benchmarks ---") bench_1_producer_n_consumer_for_m_alloc(1, 100_000, Struct_16) bench_1_producer_n_consumer_for_m_alloc(2, 100_000, Struct_16) bench_1_producer_n_consumer_for_m_alloc(4, 100_000, Struct_16) bench_1_producer_n_consumer_for_m_alloc(1, 100_000, Struct_32) bench_1_producer_n_consumer_for_m_alloc(2, 100_000, Struct_32) bench_1_producer_n_consumer_for_m_alloc(4, 100_000, Struct_32) bench_1_producer_n_consumer_for_m_alloc(1, 100_000, Struct_64) bench_1_producer_n_consumer_for_m_alloc(2, 100_000, Struct_64) bench_1_producer_n_consumer_for_m_alloc(4, 100_000, Struct_64) bench_1_producer_n_consumer_for_m_alloc(1, 100_000, Struct_512) bench_1_producer_n_consumer_for_m_alloc(2, 100_000, Struct_512) bench_1_producer_n_consumer_for_m_alloc(4, 100_000, Struct_512) bench_1_producer_n_consumer_for_m_alloc(1, 10_000, [8192]u8) bench_1_producer_n_consumer_for_m_alloc(2, 10_000, [8192]u8) bench_1_producer_n_consumer_for_m_alloc(4, 10_000, [8192]u8) bench_1_producer_n_consumer_for_m_alloc(4, 100, [65536]u8) when .Thread not_in ODIN_SANITIZER_FLAGS { // NOTE: TSan doesn't work well with excessive thread counts, // in my experience. bench_1_producer_n_consumer_for_m_alloc(16, 1_000, Struct_32) bench_1_producer_n_consumer_for_m_alloc(24, 1_000, Struct_32) bench_1_producer_n_consumer_for_m_alloc(32, 1_000, Struct_32) } } } log.info("Tests complete.") if opt.compact { runtime.compact_local_heap() log.info("The main thread's heap has been compacted.") } // for ptr, entry in tracker.allocation_map { // log.infof("%p -- %v", ptr, entry) // } // mem.tracking_allocator_destroy(&tracker) runtime.heap_release_empty_orphans() if opt.info { heap_info := runtime.get_local_heap_info() log.infof("%#v", heap_info) } if opt.trap { intrinsics.debug_trap() } }