authorgravatar for justus@klausecker.deJustus Klausecker <justus@klausecker.de> 2026-03-20 17:22:12+01:00
committergravatar for justus@klausecker.deJustus Klausecker <justus@klausecker.de> 2026-03-20 17:32:17+01:00
log591bc39e573d0f4ecb00eddd1f5c372936b59fa1
tree0bb6986429cd6075da0b98c15300eea8e73c2121
parent79f8cf1326d63fd4e355aa0c0856617698afd867

std.heap.ArenaAllocator: decrease fuzz test workload per run

At smaller workloads the overhead of setting up a new `std.Io.Threaded` for every run to reset thread-local state becomes more noticeable, so this commit also switches from thread-local storage to a shared atomic variable for keeping track of the most recent allocation. This has the side-effect of simplifying the overall implementation a bit.

1 files changed, 134 insertions(+), 146 deletions(-)

lib/std/heap/ArenaAllocator.zig+134-146
...@@ -659,45 +659,40 @@ test "reset while retaining a buffer" {...@@ -659,45 +659,40 @@ test "reset while retaining a buffer" {
659 try std.testing.expectEqual(2, arena_allocator.queryCapacity());659 try std.testing.expectEqual(2, arena_allocator.queryCapacity());
660}660}
661661
662test "fuzz" {662test "fuzz multi threaded" {
663 @disableInstrumentation();663 @disableInstrumentation();
664 if (@import("builtin").single_threaded) return error.SkipZigTest;664 if (@import("builtin").single_threaded) return error.SkipZigTest;
665665
666 const gpa = std.heap.smp_allocator;666 const gpa = std.heap.smp_allocator;
667667
668 var io_instance: std.Io.Threaded = .init(gpa, .{});
669 defer io_instance.deinit();
670
668 var arena_state: ArenaAllocator.State = .init;671 var arena_state: ArenaAllocator.State = .init;
669 // No need to deinit arena_state, all allocations are in `sample_buffer`!672 // No need to deinit arena_state, all allocations are in `sample_buffer`!
670673
671 const control_buffer = try gpa.alloc(u8, 64 << 10 << 10);674 const buffer_size = FuzzContext.max_alloc_count * FuzzContext.max_alloc_size;
675
676 const control_buffer = try gpa.alloc(u8, buffer_size);
672 defer gpa.free(control_buffer);677 defer gpa.free(control_buffer);
673 var control_instance: std.heap.FixedBufferAllocator = .init(control_buffer);678 var control_instance: std.heap.FixedBufferAllocator = .init(control_buffer);
674679
675 const sample_buffer = try gpa.alloc(u8, 64 << 10 << 10);680 const sample_buffer = try gpa.alloc(u8, buffer_size);
676 defer gpa.free(sample_buffer);681 defer gpa.free(sample_buffer);
677 var sample_instance: FuzzAllocator = .init(sample_buffer);682 var sample_instance: FuzzAllocator = .init(sample_buffer);
678683
679 var allocs: FuzzContext.Allocs = try .initCapacity(gpa, FuzzContext.max_alloc_count);
680 defer allocs.deinit(gpa);
681
682 try std.testing.fuzz(FuzzContext.Init{684 try std.testing.fuzz(FuzzContext.Init{
683 .gpa = gpa,685 .threaded_instance = &io_instance,
684 .allocs = &allocs,
685 .arena_state = &arena_state,686 .arena_state = &arena_state,
686 .control_instance = &control_instance,687 .control_instance = &control_instance,
687 .sample_instance = &sample_instance,688 .sample_instance = &sample_instance,
688 }, fuzzArenaAllocator, .{});689 }, fuzzMultiThreaded, .{});
689}690}
690691
691fn fuzzArenaAllocator(fuzz_init: FuzzContext.Init, smith: *std.testing.Smith) anyerror!void {692fn fuzzMultiThreaded(fuzz_init: FuzzContext.Init, smith: *std.testing.Smith) anyerror!void {
692 @disableInstrumentation();693 @disableInstrumentation();
693 const testing = std.testing;694 const testing = std.testing;
694695 const io = fuzz_init.threaded_instance.io();
695 // We use a 'fresh' `Threaded` instance every time to reset threadlocals to
696 // their default values.
697
698 var io_instance: std.Io.Threaded = .init(fuzz_init.gpa, .{});
699 defer io_instance.deinit();
700 const io = io_instance.io();
701696
702 fuzz_init.sample_instance.prepareFailures(smith);697 fuzz_init.sample_instance.prepareFailures(smith);
703698
...@@ -708,59 +703,56 @@ fn fuzzArenaAllocator(fuzz_init: FuzzContext.Init, smith: *std.testing.Smith) an...@@ -708,59 +703,56 @@ fn fuzzArenaAllocator(fuzz_init: FuzzContext.Init, smith: *std.testing.Smith) an
708 defer fuzz_init.arena_state.* = arena_instance.state;703 defer fuzz_init.arena_state.* = arena_instance.state;
709704
710 var ctx: FuzzContext = .init(705 var ctx: FuzzContext = .init(
711 io,
712 control_allocator,706 control_allocator,
713 arena_instance.allocator(),707 arena_instance.allocator(),
714 fuzz_init.allocs,
715 );708 );
716 defer ctx.deinit();709 defer ctx.deinit();
717710
718 ctx.rwl.lockUncancelable(io);
719
720 var group: std.Io.Group = .init;711 var group: std.Io.Group = .init;
721 defer group.cancel(io);712 defer group.cancel(io);
722713
714 var n_allocs: usize = 0;
723 var n_actions: usize = 0;715 var n_actions: usize = 0;
724 while (!smith.eosWeightedSimple(99, 1) and n_actions < FuzzContext.max_action_count) {716 while (!smith.eosWeightedSimple(99, 1) and n_actions < FuzzContext.max_action_count) {
725 errdefer comptime unreachable;717 errdefer comptime unreachable;
726718
727 const ActionTag = @typeInfo(FuzzContext.Action).@"union".tag_type.?;719 const weights: []const testing.Smith.Weight = if (n_allocs == FuzzContext.max_alloc_count)
728 const weights: []const testing.Smith.Weight = weights: {720 &.{
729 if (ctx.allocs.len == ctx.allocs.capacity)721 .value(FuzzContext.Action, .resize, 1),
730 break :weights &.{722 .value(FuzzContext.Action, .remap, 1),
731 .value(ActionTag, .resize, 1),723 .value(FuzzContext.Action, .free, 1),
732 .value(ActionTag, .remap, 1),724 }
733 .value(ActionTag, .free, 1),725 else
734 };726 &.{
735 break :weights testing.Smith.baselineWeights(ActionTag) ++727 .value(FuzzContext.Action, .resize, 1),
736 .{testing.Smith.Weight.value(ActionTag, .alloc, 2)};728 .value(FuzzContext.Action, .remap, 1),
737 };729 .value(FuzzContext.Action, .free, 1),
738 const action: FuzzContext.Action = switch (smith.valueWeighted(ActionTag, weights)) {730 .value(FuzzContext.Action, .alloc, 3),
739 .alloc => action: {731 };
740 const alloc_index = ctx.allocs.addOneBounded() catch continue;732 switch (smith.valueWeighted(FuzzContext.Action, weights)) {
741 ctx.allocs.items(.len)[alloc_index] = .free;733 .alloc => {
742 break :action .{ .alloc = .{734 const alloc_index = n_allocs;
743 .len = nextLen(smith),735 n_allocs += 1;
744 .alignment = smith.valueRangeAtMost(736 ctx.allocs[alloc_index].common.len = .free;
737 group.concurrent(io, FuzzContext.doOneAlloc, .{
738 &ctx, nextLen(smith),
739 smith.valueRangeAtMost(
745 Alignment,740 Alignment,
746 .@"1",741 .@"1",
747 .fromByteUnits(2 * std.heap.page_size_max),742 .fromByteUnits(2 * std.heap.page_size_max),
748 ),743 ),
749 .index = alloc_index,744 @enumFromInt(alloc_index),
750 } };745 }) catch unreachable;
751 },746 },
752 .resize => .{ .resize = .{ .new_len = nextLen(smith) } },747 .resize => group.concurrent(io, FuzzContext.doOneResize, .{ &ctx, nextLen(smith) }) catch unreachable,
753 .remap => .{ .remap = .{ .new_len = nextLen(smith) } },748 .remap => group.concurrent(io, FuzzContext.doOneRemap, .{ &ctx, nextLen(smith) }) catch unreachable,
754 .free => .free,749 .free => group.concurrent(io, FuzzContext.doOneFree, .{&ctx}) catch unreachable,
755 };750 }
756 group.concurrent(io, FuzzContext.doOneAction, .{ &ctx, action }) catch break;
757 n_actions += 1;751 n_actions += 1;
758 }752 }
759753
760 ctx.rwl.unlock(io);
761
762 try group.await(io);754 try group.await(io);
763 try ctx.check();755 try ctx.check(n_allocs);
764756
765 // This also covers the `deinit` logic since `free_all` uses it internally.757 // This also covers the `deinit` logic since `free_all` uses it internally.
766758
...@@ -783,73 +775,71 @@ fn fuzzArenaAllocator(fuzz_init: FuzzContext.Init, smith: *std.testing.Smith) an...@@ -783,73 +775,71 @@ fn fuzzArenaAllocator(fuzz_init: FuzzContext.Init, smith: *std.testing.Smith) an
783 }775 }
784776
785 fuzz_init.control_instance.reset();777 fuzz_init.control_instance.reset();
786 fuzz_init.allocs.clearRetainingCapacity();
787}
788fn nextLen(smith: *std.testing.Smith) usize {
789 @disableInstrumentation();
790 return usizeRange(smith, 1, 16 << 10 << 10);
791}778}
792fn usizeRange(smith: *std.testing.Smith, at_least: usize, at_most: usize) usize {779fn nextLen(smith: *std.testing.Smith) @typeInfo(FuzzContext.Alloc.Len).@"enum".tag_type {
793 @disableInstrumentation();780 @disableInstrumentation();
794 const Int = @Int(.unsigned, @min(64, @bitSizeOf(usize)));781 const BackingInt = @typeInfo(FuzzContext.Alloc.Len).@"enum".tag_type;
795 return smith.valueRangeAtMost(Int, @intCast(at_least), @intCast(at_most));782 return smith.valueRangeAtMost(BackingInt, 1, FuzzContext.max_alloc_size);
796}783}
797784
798const FuzzContext = struct {785const FuzzContext = struct {
799 io: std.Io,
800 rwl: std.Io.RwLock,
801
802 control_allocator: Allocator,786 control_allocator: Allocator,
803 sample_allocator: Allocator,787 sample_allocator: Allocator,
804788
805 allocs: *Allocs,789 last_alloc_index: Alloc.Index,
790 allocs: [max_alloc_count]Alloc,
806791
807 const max_alloc_count = 4096;792 const max_alloc_count = 64;
808 const max_action_count = 2 * max_alloc_count;793 const max_action_count = 2 * max_alloc_count;
809794
810 const Allocs = std.MultiArrayList(struct {795 const max_alloc_size = 16 << 10;
796
797 const Alloc = struct {
811 control_ptr: [*]u8,798 control_ptr: [*]u8,
812 sample_ptr: [*]u8,799 sample_ptr: [*]u8,
813 len: Len,800 common: packed struct(usize) {
814 alignment: Alignment,801 len: Len,
815 });802 alignment: Alignment,
803 _: @Int(.unsigned, padding_bits) = 0,
804 },
805
806 const Len = enum(@Int(.unsigned, len_bits)) {
807 free = (1 << len_bits) - 1,
808 _,
809 };
810 const len_bits = @min(64, @bitSizeOf(usize)) - @bitSizeOf(Alignment);
811 const padding_bits = @bitSizeOf(usize) - (len_bits + @bitSizeOf(Alignment));
816812
817 const Len = enum(usize) {813 const Index = enum(usize) {
818 free = std.math.maxInt(usize),814 none = std.math.maxInt(usize),
819 _,815 _,
816 };
820 };817 };
821818
822 const Action = union(enum(u8)) {819 const Action = enum {
823 alloc: struct { len: usize, alignment: Alignment, index: usize },820 alloc,
824 resize: struct { new_len: usize },821 resize,
825 remap: struct { new_len: usize },822 remap,
826 free,823 free,
827 };824 };
828825
829 threadlocal var tls_next: u8 = 0;
830 threadlocal var tls_last_index: ?usize = null;
831
832 const Init = struct {826 const Init = struct {
833 gpa: Allocator,827 threaded_instance: *std.Io.Threaded,
834 allocs: *FuzzContext.Allocs,
835 arena_state: *ArenaAllocator.State,828 arena_state: *ArenaAllocator.State,
836 control_instance: *std.heap.FixedBufferAllocator,829 control_instance: *std.heap.FixedBufferAllocator,
837 sample_instance: *FuzzAllocator,830 sample_instance: *FuzzAllocator,
838 };831 };
839832
840 fn init(833 fn init(
841 io: std.Io,
842 control_allocator: Allocator,834 control_allocator: Allocator,
843 sample_allocator: Allocator,835 sample_allocator: Allocator,
844 allocs: *Allocs,
845 ) FuzzContext {836 ) FuzzContext {
846 @disableInstrumentation();837 @disableInstrumentation();
847 return .{838 return .{
848 .io = io,
849 .rwl = .init,
850 .control_allocator = control_allocator,839 .control_allocator = control_allocator,
851 .sample_allocator = sample_allocator,840 .sample_allocator = sample_allocator,
852 .allocs = allocs,841 .last_alloc_index = .none,
842 .allocs = undefined,
853 };843 };
854 }844 }
855845
...@@ -858,35 +848,22 @@ const FuzzContext = struct {...@@ -858,35 +848,22 @@ const FuzzContext = struct {
858 ctx.* = undefined;848 ctx.* = undefined;
859 }849 }
860850
861 fn check(ctx: *const FuzzContext) !void {851 fn check(ctx: *const FuzzContext, n_allocs: usize) !void {
862 @disableInstrumentation();852 @disableInstrumentation();
863 for (0..ctx.allocs.len) |index| {853 for (ctx.allocs[0..n_allocs]) |allocation| {
864 const len: usize = switch (ctx.allocs.items(.len)[index]) {854 const len: usize = switch (allocation.common.len) {
865 .free => continue,855 .free => continue,
866 _ => |len| @intFromEnum(len),856 _ => |len| @intFromEnum(len),
867 };857 };
868 const control = ctx.allocs.items(.control_ptr)[index][0..len];858 const control = allocation.control_ptr[0..len];
869 const sample = ctx.allocs.items(.sample_ptr)[index][0..len];859 const sample = allocation.sample_ptr[0..len];
870 try std.testing.expectEqualSlices(u8, control, sample);860 try std.testing.expectEqualSlices(u8, control, sample);
871 }861 }
872 }862 }
873863
874 fn doOneAction(ctx: *FuzzContext, action: Action) std.Io.Cancelable!void {864 fn doOneAlloc(ctx: *FuzzContext, len: usize, alignment: Alignment, index: Alloc.Index) void {
875 @disableInstrumentation();
876 ctx.rwl.lockSharedUncancelable(ctx.io);
877 defer ctx.rwl.unlockShared(ctx.io);
878
879 switch (action) {
880 .alloc => |act| ctx.doOneAlloc(act.len, act.alignment, act.index),
881 .resize => |act| ctx.doOneResize(act.new_len),
882 .remap => |act| ctx.doOneRemap(act.new_len),
883 .free => ctx.doOneFree(),
884 }
885 }
886
887 fn doOneAlloc(ctx: *FuzzContext, len: usize, alignment: Alignment, index: usize) void {
888 @disableInstrumentation();865 @disableInstrumentation();
889 assert(ctx.allocs.items(.len)[index] == .free);866 assert(ctx.allocs[@intFromEnum(index)].common.len == .free);
890867
891 const control_ptr = ctx.control_allocator.rawAlloc(len, alignment, @returnAddress()) orelse868 const control_ptr = ctx.control_allocator.rawAlloc(len, alignment, @returnAddress()) orelse
892 return;869 return;
...@@ -895,38 +872,42 @@ const FuzzContext = struct {...@@ -895,38 +872,42 @@ const FuzzContext = struct {
895 return;872 return;
896 };873 };
897874
898 ctx.allocs.set(index, .{875 ctx.allocs[@intFromEnum(index)] = .{
899 .control_ptr = control_ptr,876 .control_ptr = control_ptr,
900 .sample_ptr = sample_ptr,877 .sample_ptr = sample_ptr,
901 .len = @enumFromInt(len),878 .common = .{
902 .alignment = alignment,879 .len = @enumFromInt(len),
903 });880 .alignment = alignment,
904881 },
905 for (control_ptr[0..len], sample_ptr[0..len]) |*control, *sample| {882 };
906 control.* = tls_next;883
907 sample.* = tls_next;884 for (control_ptr[0..len], sample_ptr[0..len], 0..) |*control, *sample, i| {
908 tls_next +%= 1;885 control.* = @truncate(i);
886 sample.* = @truncate(i);
909 }887 }
910888
911 tls_last_index = index;889 @atomicStore(Alloc.Index, &ctx.last_alloc_index, index, .release);
912 }890 }
913 fn doOneResize(ctx: *FuzzContext, new_len: usize) void {891 fn doOneResize(ctx: *FuzzContext, new_len: usize) void {
914 @disableInstrumentation();892 @disableInstrumentation();
915 const index = tls_last_index orelse return;
916 const len = ctx.allocs.items(.len)[index];
917 assert(len != .free);
918 const memory = ctx.allocs.items(.sample_ptr)[index][0..@intFromEnum(len)];
919 const alignment = ctx.allocs.items(.alignment)[index];
920893
921 assert(alignment.check(@intFromPtr(ctx.allocs.items(.control_ptr)[index])));894 const index = @atomicRmw(Alloc.Index, &ctx.last_alloc_index, .Xchg, .none, .acquire);
922 assert(alignment.check(@intFromPtr(ctx.allocs.items(.sample_ptr)[index])));895 if (index == .none) return;
896
897 const allocation = &ctx.allocs[@intFromEnum(index)];
898 assert(allocation.common.len != .free);
899 const memory = allocation.sample_ptr[0..@intFromEnum(allocation.common.len)];
900 const alignment = allocation.common.alignment;
901
902 assert(alignment.check(@intFromPtr(allocation.control_ptr)));
903 assert(alignment.check(@intFromPtr(allocation.sample_ptr)));
923904
924 // Since `resize` is fallible, we have to ensure that `control_allocator`905 // Since `resize` is fallible, we have to ensure that `control_allocator`
925 // is always successful by reserving the memory we need beforehand.906 // is always successful by reserving the memory we need beforehand.
926 const new_control_ptr = ctx.control_allocator.rawAlloc(new_len, alignment, @returnAddress()) orelse907 const new_control_ptr = ctx.control_allocator.rawAlloc(new_len, alignment, @returnAddress()) orelse
927 return;908 return;
928 if (ctx.sample_allocator.rawResize(memory, alignment, new_len, @returnAddress())) {909 if (ctx.sample_allocator.rawResize(memory, alignment, new_len, @returnAddress())) {
929 const old_control = ctx.allocs.items(.control_ptr)[index][0..memory.len];910 const old_control = allocation.control_ptr[0..memory.len];
930 const overlap = @min(memory.len, new_len);911 const overlap = @min(memory.len, new_len);
931 @memcpy(new_control_ptr[0..overlap], old_control[0..overlap]);912 @memcpy(new_control_ptr[0..overlap], old_control[0..overlap]);
932 ctx.control_allocator.rawFree(old_control, alignment, @returnAddress());913 ctx.control_allocator.rawFree(old_control, alignment, @returnAddress());
...@@ -935,23 +916,27 @@ const FuzzContext = struct {...@@ -935,23 +916,27 @@ const FuzzContext = struct {
935 return;916 return;
936 }917 }
937918
938 ctx.allocs.set(index, .{919 ctx.allocs[@intFromEnum(index)] = .{
939 .control_ptr = new_control_ptr,920 .control_ptr = new_control_ptr,
940 .sample_ptr = memory.ptr,921 .sample_ptr = memory.ptr,
941 .len = @enumFromInt(new_len),922 .common = .{
942 .alignment = alignment,923 .len = @enumFromInt(new_len),
943 });924 .alignment = alignment,
925 },
926 };
944927
945 if (new_len > memory.len) {928 if (new_len > memory.len) {
946 for (929 for (
947 ctx.allocs.items(.control_ptr)[index][memory.len..new_len],930 allocation.control_ptr[memory.len..new_len],
948 ctx.allocs.items(.sample_ptr)[index][memory.len..new_len],931 allocation.sample_ptr[memory.len..new_len],
949 ) |*control, *sample| {932 0..,
950 control.* = tls_next;933 ) |*control, *sample, i| {
951 sample.* = tls_next;934 control.* = @truncate(i);
952 tls_next +%= 1;935 sample.* = @truncate(i);
953 }936 }
954 }937 }
938
939 @atomicStore(Alloc.Index, &ctx.last_alloc_index, index, .release);
955 }940 }
956 fn doOneRemap(ctx: *FuzzContext, new_len: usize) void {941 fn doOneRemap(ctx: *FuzzContext, new_len: usize) void {
957 @disableInstrumentation();942 @disableInstrumentation();
...@@ -959,26 +944,29 @@ const FuzzContext = struct {...@@ -959,26 +944,29 @@ const FuzzContext = struct {
959 }944 }
960 fn doOneFree(ctx: *FuzzContext) void {945 fn doOneFree(ctx: *FuzzContext) void {
961 @disableInstrumentation();946 @disableInstrumentation();
962 const index = tls_last_index orelse return;
963 const len = ctx.allocs.items(.len)[index];
964 assert(len != .free);
965 const memory = ctx.allocs.items(.sample_ptr)[index][0..@intFromEnum(len)];
966 const alignment = ctx.allocs.items(.alignment)[index];
967947
968 assert(alignment.check(@intFromPtr(ctx.allocs.items(.control_ptr)[index])));948 const index = @atomicRmw(Alloc.Index, &ctx.last_alloc_index, .Xchg, .none, .acquire);
969 assert(alignment.check(@intFromPtr(ctx.allocs.items(.sample_ptr)[index])));949 if (index == .none) return;
970950
971 ctx.control_allocator.rawFree(ctx.allocs.items(.control_ptr)[index][0..memory.len], alignment, @returnAddress());951 const allocation = &ctx.allocs[@intFromEnum(index)];
972 ctx.sample_allocator.rawFree(ctx.allocs.items(.sample_ptr)[index][0..memory.len], alignment, @returnAddress());952 assert(allocation.common.len != .free);
953 const len: usize = @intFromEnum(allocation.common.len);
954 const alignment = allocation.common.alignment;
973955
974 ctx.allocs.set(index, .{956 assert(alignment.check(@intFromPtr(allocation.control_ptr)));
957 assert(alignment.check(@intFromPtr(allocation.sample_ptr)));
958
959 ctx.control_allocator.rawFree(allocation.control_ptr[0..len], alignment, @returnAddress());
960 ctx.sample_allocator.rawFree(allocation.sample_ptr[0..len], alignment, @returnAddress());
961
962 ctx.allocs[@intFromEnum(index)] = .{
975 .control_ptr = undefined,963 .control_ptr = undefined,
976 .sample_ptr = undefined,964 .sample_ptr = undefined,
977 .len = .free,965 .common = .{
978 .alignment = undefined,966 .len = .free,
979 });967 .alignment = .@"1",
980968 },
981 tls_last_index = null;969 };
982 }970 }
983};971};
984972