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" {
659659 try std.testing.expectEqual(2, arena_allocator.queryCapacity());
660660}
661661
662test "fuzz" {
662test "fuzz multi threaded" {
663663 @disableInstrumentation();
664664 if (@import("builtin").single_threaded) return error.SkipZigTest;
665665
666666 const gpa = std.heap.smp_allocator;
667667
668 var io_instance: std.Io.Threaded = .init(gpa, .{});
669 defer io_instance.deinit();
670
668671 var arena_state: ArenaAllocator.State = .init;
669672 // 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);
672677 defer gpa.free(control_buffer);
673678 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);
676681 defer gpa.free(sample_buffer);
677682 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
682684 try std.testing.fuzz(FuzzContext.Init{
683 .gpa = gpa,
684 .allocs = &allocs,
685 .threaded_instance = &io_instance,
685686 .arena_state = &arena_state,
686687 .control_instance = &control_instance,
687688 .sample_instance = &sample_instance,
688 }, fuzzArenaAllocator, .{});
689 }, fuzzMultiThreaded, .{});
689690}
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 {
692693 @disableInstrumentation();
693694 const testing = std.testing;
694
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();
695 const io = fuzz_init.threaded_instance.io();
701696
702697 fuzz_init.sample_instance.prepareFailures(smith);
703698
......@@ -708,59 +703,56 @@ fn fuzzArenaAllocator(fuzz_init: FuzzContext.Init, smith: *std.testing.Smith) an
708703 defer fuzz_init.arena_state.* = arena_instance.state;
709704
710705 var ctx: FuzzContext = .init(
711 io,
712706 control_allocator,
713707 arena_instance.allocator(),
714 fuzz_init.allocs,
715708 );
716709 defer ctx.deinit();
717710
718 ctx.rwl.lockUncancelable(io);
719
720711 var group: std.Io.Group = .init;
721712 defer group.cancel(io);
722713
714 var n_allocs: usize = 0;
723715 var n_actions: usize = 0;
724716 while (!smith.eosWeightedSimple(99, 1) and n_actions < FuzzContext.max_action_count) {
725717 errdefer comptime unreachable;
726718
727 const ActionTag = @typeInfo(FuzzContext.Action).@"union".tag_type.?;
728 const weights: []const testing.Smith.Weight = weights: {
729 if (ctx.allocs.len == ctx.allocs.capacity)
730 break :weights &.{
731 .value(ActionTag, .resize, 1),
732 .value(ActionTag, .remap, 1),
733 .value(ActionTag, .free, 1),
734 };
735 break :weights testing.Smith.baselineWeights(ActionTag) ++
736 .{testing.Smith.Weight.value(ActionTag, .alloc, 2)};
737 };
738 const action: FuzzContext.Action = switch (smith.valueWeighted(ActionTag, weights)) {
739 .alloc => action: {
740 const alloc_index = ctx.allocs.addOneBounded() catch continue;
741 ctx.allocs.items(.len)[alloc_index] = .free;
742 break :action .{ .alloc = .{
743 .len = nextLen(smith),
744 .alignment = smith.valueRangeAtMost(
719 const weights: []const testing.Smith.Weight = if (n_allocs == FuzzContext.max_alloc_count)
720 &.{
721 .value(FuzzContext.Action, .resize, 1),
722 .value(FuzzContext.Action, .remap, 1),
723 .value(FuzzContext.Action, .free, 1),
724 }
725 else
726 &.{
727 .value(FuzzContext.Action, .resize, 1),
728 .value(FuzzContext.Action, .remap, 1),
729 .value(FuzzContext.Action, .free, 1),
730 .value(FuzzContext.Action, .alloc, 3),
731 };
732 switch (smith.valueWeighted(FuzzContext.Action, weights)) {
733 .alloc => {
734 const alloc_index = n_allocs;
735 n_allocs += 1;
736 ctx.allocs[alloc_index].common.len = .free;
737 group.concurrent(io, FuzzContext.doOneAlloc, .{
738 &ctx, nextLen(smith),
739 smith.valueRangeAtMost(
745740 Alignment,
746741 .@"1",
747742 .fromByteUnits(2 * std.heap.page_size_max),
748743 ),
749 .index = alloc_index,
750 } };
744 @enumFromInt(alloc_index),
745 }) catch unreachable;
751746 },
752 .resize => .{ .resize = .{ .new_len = nextLen(smith) } },
753 .remap => .{ .remap = .{ .new_len = nextLen(smith) } },
754 .free => .free,
755 };
756 group.concurrent(io, FuzzContext.doOneAction, .{ &ctx, action }) catch break;
747 .resize => group.concurrent(io, FuzzContext.doOneResize, .{ &ctx, nextLen(smith) }) catch unreachable,
748 .remap => group.concurrent(io, FuzzContext.doOneRemap, .{ &ctx, nextLen(smith) }) catch unreachable,
749 .free => group.concurrent(io, FuzzContext.doOneFree, .{&ctx}) catch unreachable,
750 }
757751 n_actions += 1;
758752 }
759753
760 ctx.rwl.unlock(io);
761
762754 try group.await(io);
763 try ctx.check();
755 try ctx.check(n_allocs);
764756
765757 // 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
783775 }
784776
785777 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);
791778}
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 {
793780 @disableInstrumentation();
794 const Int = @Int(.unsigned, @min(64, @bitSizeOf(usize)));
795 return smith.valueRangeAtMost(Int, @intCast(at_least), @intCast(at_most));
781 const BackingInt = @typeInfo(FuzzContext.Alloc.Len).@"enum".tag_type;
782 return smith.valueRangeAtMost(BackingInt, 1, FuzzContext.max_alloc_size);
796783}
797784
798785const FuzzContext = struct {
799 io: std.Io,
800 rwl: std.Io.RwLock,
801
802786 control_allocator: Allocator,
803787 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;
808793 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 {
811798 control_ptr: [*]u8,
812799 sample_ptr: [*]u8,
813 len: Len,
814 alignment: Alignment,
815 });
800 common: packed struct(usize) {
801 len: Len,
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) {
818 free = std.math.maxInt(usize),
819 _,
813 const Index = enum(usize) {
814 none = std.math.maxInt(usize),
815 _,
816 };
820817 };
821818
822 const Action = union(enum(u8)) {
823 alloc: struct { len: usize, alignment: Alignment, index: usize },
824 resize: struct { new_len: usize },
825 remap: struct { new_len: usize },
819 const Action = enum {
820 alloc,
821 resize,
822 remap,
826823 free,
827824 };
828825
829 threadlocal var tls_next: u8 = 0;
830 threadlocal var tls_last_index: ?usize = null;
831
832826 const Init = struct {
833 gpa: Allocator,
834 allocs: *FuzzContext.Allocs,
827 threaded_instance: *std.Io.Threaded,
835828 arena_state: *ArenaAllocator.State,
836829 control_instance: *std.heap.FixedBufferAllocator,
837830 sample_instance: *FuzzAllocator,
838831 };
839832
840833 fn init(
841 io: std.Io,
842834 control_allocator: Allocator,
843835 sample_allocator: Allocator,
844 allocs: *Allocs,
845836 ) FuzzContext {
846837 @disableInstrumentation();
847838 return .{
848 .io = io,
849 .rwl = .init,
850839 .control_allocator = control_allocator,
851840 .sample_allocator = sample_allocator,
852 .allocs = allocs,
841 .last_alloc_index = .none,
842 .allocs = undefined,
853843 };
854844 }
855845
......@@ -858,35 +848,22 @@ const FuzzContext = struct {
858848 ctx.* = undefined;
859849 }
860850
861 fn check(ctx: *const FuzzContext) !void {
851 fn check(ctx: *const FuzzContext, n_allocs: usize) !void {
862852 @disableInstrumentation();
863 for (0..ctx.allocs.len) |index| {
864 const len: usize = switch (ctx.allocs.items(.len)[index]) {
853 for (ctx.allocs[0..n_allocs]) |allocation| {
854 const len: usize = switch (allocation.common.len) {
865855 .free => continue,
866856 _ => |len| @intFromEnum(len),
867857 };
868 const control = ctx.allocs.items(.control_ptr)[index][0..len];
869 const sample = ctx.allocs.items(.sample_ptr)[index][0..len];
858 const control = allocation.control_ptr[0..len];
859 const sample = allocation.sample_ptr[0..len];
870860 try std.testing.expectEqualSlices(u8, control, sample);
871861 }
872862 }
873863
874 fn doOneAction(ctx: *FuzzContext, action: Action) std.Io.Cancelable!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 {
864 fn doOneAlloc(ctx: *FuzzContext, len: usize, alignment: Alignment, index: Alloc.Index) void {
888865 @disableInstrumentation();
889 assert(ctx.allocs.items(.len)[index] == .free);
866 assert(ctx.allocs[@intFromEnum(index)].common.len == .free);
890867
891868 const control_ptr = ctx.control_allocator.rawAlloc(len, alignment, @returnAddress()) orelse
892869 return;
......@@ -895,38 +872,42 @@ const FuzzContext = struct {
895872 return;
896873 };
897874
898 ctx.allocs.set(index, .{
875 ctx.allocs[@intFromEnum(index)] = .{
899876 .control_ptr = control_ptr,
900877 .sample_ptr = sample_ptr,
901 .len = @enumFromInt(len),
902 .alignment = alignment,
903 });
904
905 for (control_ptr[0..len], sample_ptr[0..len]) |*control, *sample| {
906 control.* = tls_next;
907 sample.* = tls_next;
908 tls_next +%= 1;
878 .common = .{
879 .len = @enumFromInt(len),
880 .alignment = alignment,
881 },
882 };
883
884 for (control_ptr[0..len], sample_ptr[0..len], 0..) |*control, *sample, i| {
885 control.* = @truncate(i);
886 sample.* = @truncate(i);
909887 }
910888
911 tls_last_index = index;
889 @atomicStore(Alloc.Index, &ctx.last_alloc_index, index, .release);
912890 }
913891 fn doOneResize(ctx: *FuzzContext, new_len: usize) void {
914892 @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])));
922 assert(alignment.check(@intFromPtr(ctx.allocs.items(.sample_ptr)[index])));
894 const index = @atomicRmw(Alloc.Index, &ctx.last_alloc_index, .Xchg, .none, .acquire);
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
924905 // Since `resize` is fallible, we have to ensure that `control_allocator`
925906 // is always successful by reserving the memory we need beforehand.
926907 const new_control_ptr = ctx.control_allocator.rawAlloc(new_len, alignment, @returnAddress()) orelse
927908 return;
928909 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];
930911 const overlap = @min(memory.len, new_len);
931912 @memcpy(new_control_ptr[0..overlap], old_control[0..overlap]);
932913 ctx.control_allocator.rawFree(old_control, alignment, @returnAddress());
......@@ -935,23 +916,27 @@ const FuzzContext = struct {
935916 return;
936917 }
937918
938 ctx.allocs.set(index, .{
919 ctx.allocs[@intFromEnum(index)] = .{
939920 .control_ptr = new_control_ptr,
940921 .sample_ptr = memory.ptr,
941 .len = @enumFromInt(new_len),
942 .alignment = alignment,
943 });
922 .common = .{
923 .len = @enumFromInt(new_len),
924 .alignment = alignment,
925 },
926 };
944927
945928 if (new_len > memory.len) {
946929 for (
947 ctx.allocs.items(.control_ptr)[index][memory.len..new_len],
948 ctx.allocs.items(.sample_ptr)[index][memory.len..new_len],
949 ) |*control, *sample| {
950 control.* = tls_next;
951 sample.* = tls_next;
952 tls_next +%= 1;
930 allocation.control_ptr[memory.len..new_len],
931 allocation.sample_ptr[memory.len..new_len],
932 0..,
933 ) |*control, *sample, i| {
934 control.* = @truncate(i);
935 sample.* = @truncate(i);
953936 }
954937 }
938
939 @atomicStore(Alloc.Index, &ctx.last_alloc_index, index, .release);
955940 }
956941 fn doOneRemap(ctx: *FuzzContext, new_len: usize) void {
957942 @disableInstrumentation();
......@@ -959,26 +944,29 @@ const FuzzContext = struct {
959944 }
960945 fn doOneFree(ctx: *FuzzContext) void {
961946 @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])));
969 assert(alignment.check(@intFromPtr(ctx.allocs.items(.sample_ptr)[index])));
948 const index = @atomicRmw(Alloc.Index, &ctx.last_alloc_index, .Xchg, .none, .acquire);
949 if (index == .none) return;
970950
971 ctx.control_allocator.rawFree(ctx.allocs.items(.control_ptr)[index][0..memory.len], alignment, @returnAddress());
972 ctx.sample_allocator.rawFree(ctx.allocs.items(.sample_ptr)[index][0..memory.len], alignment, @returnAddress());
951 const allocation = &ctx.allocs[@intFromEnum(index)];
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)] = .{
975963 .control_ptr = undefined,
976964 .sample_ptr = undefined,
977 .len = .free,
978 .alignment = undefined,
979 });
980
981 tls_last_index = null;
965 .common = .{
966 .len = .free,
967 .alignment = .@"1",
968 },
969 };
982970 }
983971};
984972