Skip to content

Commit 266a0c1

Browse files
committed
add std.atomic.Op for feature detection
1 parent f34b478 commit 266a0c1

File tree

6 files changed

+496
-166
lines changed

6 files changed

+496
-166
lines changed

lib/std/Target.zig

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1276,6 +1276,49 @@ pub const Cpu = struct {
12761276
const other_v: V = other_set.ints;
12771277
return @reduce(.And, (set_v & other_v) == other_v);
12781278
}
1279+
1280+
/// Formatter to print the feature set as a comma-separated list, ending with a conjunction
1281+
pub fn fmtList(set: Set, family: Arch.Family, conjunction: []const u8) FormatList {
1282+
return .{ .set = set, .family = family, .conjunction = conjunction };
1283+
}
1284+
1285+
pub const FormatList = struct {
1286+
set: Set,
1287+
conjunction: []const u8,
1288+
family: Arch.Family,
1289+
1290+
pub fn format(fmt: @This(), writer: *std.Io.Writer) !void {
1291+
const BitSet = std.bit_set.ArrayBitSet(usize, Set.needed_bit_count);
1292+
const bit_set: BitSet = .{ .masks = fmt.set.ints };
1293+
var it = bit_set.iterator(.{});
1294+
var next = it.next();
1295+
if (next == null) {
1296+
return writer.writeAll("<none>");
1297+
}
1298+
var i: usize = 0;
1299+
while (next) |feat| : (i += 1) {
1300+
next = it.next();
1301+
1302+
if (i > 1 or (i > 0 and next != null)) {
1303+
try writer.writeAll(", ");
1304+
}
1305+
if (next == null) {
1306+
try writer.print("{s} ", .{fmt.conjunction});
1307+
}
1308+
1309+
const name = switch (fmt.family) {
1310+
inline else => |family| blk: {
1311+
const FeatureEnum = @field(Target, @tagName(family)).Feature;
1312+
if (@typeInfo(FeatureEnum).@"enum".fields.len == 0) unreachable;
1313+
1314+
const feat_enum: FeatureEnum = @enumFromInt(feat);
1315+
break :blk @tagName(feat_enum);
1316+
},
1317+
};
1318+
try writer.writeAll(name);
1319+
}
1320+
}
1321+
};
12791322
};
12801323

12811324
pub fn FeatureSetFns(comptime F: type) type {

lib/std/atomic.zig

Lines changed: 340 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,346 @@ test "current CPU has a cache line size" {
481481
_ = cache_line;
482482
}
483483

484+
pub const Op = union(enum) {
485+
load,
486+
store,
487+
rmw: std.builtin.AtomicRmwOp,
488+
cmpxchg: enum { weak, strong },
489+
490+
/// Check if the operation is supported on the given type.
491+
pub fn supported(op: Op, comptime T: type) bool {
492+
return op.supportedOnCpu(T, builtin.cpu);
493+
}
494+
/// Check if the operation is supported on the given type, on a specified CPU.
495+
pub fn supportedOnCpu(op: Op, comptime T: type, cpu: std.Target.Cpu) bool {
496+
if (!op.isValidAtomicType(T)) return false;
497+
498+
if (!std.math.isPowerOfTwo(@sizeOf(T))) return false;
499+
500+
const required_features = op.supportedSizes(cpu.arch).get(@sizeOf(T)) orelse {
501+
return false;
502+
};
503+
504+
return cpu.features.isSuperSetOf(required_features);
505+
}
506+
507+
/// Get the set of sizes supported by this operation on the specified architecture.
508+
// TODO: Audit this. I've done my best for the architectures I'm familiar with, but there's probably a lot that can improved
509+
pub fn supportedSizes(op: Op, arch: std.Target.Cpu.Arch) Sizes {
510+
switch (arch) {
511+
.avr,
512+
.msp430,
513+
=> return .upTo(2, .empty),
514+
515+
.arc,
516+
.arm,
517+
.armeb,
518+
.hexagon,
519+
.m68k,
520+
.mips,
521+
.mipsel,
522+
.nvptx,
523+
.or1k,
524+
.powerpc,
525+
.powerpcle,
526+
.riscv32,
527+
.sparc,
528+
.thumb,
529+
.thumbeb,
530+
.xcore,
531+
.kalimba,
532+
.lanai,
533+
.csky,
534+
.spirv32,
535+
.loongarch32,
536+
.xtensa,
537+
.propeller,
538+
=> return .upTo(4, .empty),
539+
540+
.amdgcn,
541+
.bpfel,
542+
.bpfeb,
543+
.mips64,
544+
.mips64el,
545+
.nvptx64,
546+
.powerpc64,
547+
.powerpc64le,
548+
.riscv64,
549+
.sparc64,
550+
.s390x,
551+
.ve,
552+
.spirv64,
553+
.loongarch64,
554+
=> return .upTo(8, .empty),
555+
556+
.aarch64,
557+
.aarch64_be,
558+
=> return .upTo(16, .empty),
559+
560+
.wasm32,
561+
.wasm64,
562+
=> {
563+
if (op == .rmw) switch (op.rmw) {
564+
.Xchg,
565+
.Add,
566+
.Sub,
567+
.And,
568+
.Or,
569+
.Xor,
570+
=> {},
571+
572+
.Nand,
573+
.Max,
574+
.Min,
575+
=> return .none, // Not supported on wasm
576+
};
577+
578+
return .upTo(8, std.Target.wasm.featureSet(&.{.atomics}));
579+
},
580+
581+
.x86 => {
582+
var sizes: Sizes = .upTo(4, .empty);
583+
if (op == .cmpxchg) {
584+
sizes.put(8, std.Target.x86.featureSet(&.{.cx8}));
585+
}
586+
return sizes;
587+
},
588+
589+
.x86_64 => {
590+
var sizes: Sizes = .upTo(8, .empty);
591+
if (op == .cmpxchg) {
592+
sizes.put(16, std.Target.x86.featureSet(&.{.cx16}));
593+
}
594+
return sizes;
595+
},
596+
}
597+
}
598+
599+
pub const Sizes = struct {
600+
/// Bitset of supported sizes. If size `2^n` is present, `supported & (1 << n)` will be non-zero.
601+
/// For each set bit, the corresponding entry in `required_features` will be populated.
602+
supported: BitsetInt,
603+
/// for each set bit in `supported`, the corresponding entry here indicates the required CPU features to support that size.
604+
/// Otherwise, the element is `undefined`.
605+
required_features: [bit_set_len]std.Target.Cpu.Feature.Set,
606+
607+
const bit_set_len = std.math.log2_int(usize, max_supported_size) + 1;
608+
const BitsetInt = @Type(.{ .int = .{
609+
.signedness = .unsigned,
610+
.bits = bit_set_len,
611+
} });
612+
613+
pub fn isEmpty(sizes: Sizes) bool {
614+
return sizes.supported == 0;
615+
}
616+
pub fn get(sizes: Sizes, size: u64) ?std.Target.Cpu.Feature.Set {
617+
if (size == 0) return .empty; // 0-bit types are always atomic, because they only hold a single value
618+
if (!std.math.isPowerOfTwo(size)) return null;
619+
if (sizes.supported & size == 0) return null;
620+
return sizes.required_features[std.math.log2_int(u64, size)];
621+
}
622+
623+
/// Prints the set as a list of possible sizes.
624+
/// eg. `1, 2, 4, or 8`
625+
pub fn formatPossibilities(sizes: Sizes, writer: *std.Io.Writer) !void {
626+
if (sizes.supported == 0) {
627+
return writer.writeAll("<none>");
628+
}
629+
630+
var bits = sizes.supported;
631+
var count: usize = 0;
632+
while (bits != 0) : (count += 1) {
633+
const mask = @as(BitsetInt, 1) << @intCast(@ctz(bits));
634+
bits &= ~mask;
635+
636+
if (count > 1 or (count > 0 and bits != 0)) {
637+
try writer.writeAll(", ");
638+
}
639+
if (bits == 0) {
640+
try writer.writeAll("or ");
641+
}
642+
643+
try writer.print("{d}", .{mask});
644+
}
645+
}
646+
647+
const none: Sizes = .{
648+
.supported = 0,
649+
.required_features = undefined,
650+
};
651+
fn upTo(max: BitsetInt, required_features: std.Target.Cpu.Feature.Set) Sizes {
652+
std.debug.assert(std.math.isPowerOfTwo(max));
653+
var sizes: Sizes = .{
654+
.supported = (max << 1) -% 1,
655+
.required_features = @splat(required_features),
656+
};
657+
658+
// Safety
659+
const max_idx = std.math.log2_int(BitsetInt, max);
660+
@memset(sizes.required_features[max_idx + 1 ..], undefined);
661+
662+
return sizes;
663+
}
664+
fn put(sizes: *Sizes, size: BitsetInt, required_features: std.Target.Cpu.Feature.Set) void {
665+
sizes.supported |= size;
666+
sizes.required_features[std.math.log2_int(u64, size)] = required_features;
667+
}
668+
};
669+
670+
/// The maximum size supported by any architecture
671+
const max_supported_size = 16;
672+
673+
pub fn format(op: Op, writer: *std.Io.Writer) !void {
674+
switch (op) {
675+
.load => try writer.writeAll("@atomicLoad"),
676+
.store => try writer.writeAll("@atomicStore"),
677+
.rmw => |rmw| try writer.print("@atomicRmw(.{s})", .{@tagName(rmw)}),
678+
.cmpxchg => |strength| switch (strength) {
679+
.weak => try writer.writeAll("@cmpxchgWeak"),
680+
.strong => try writer.writeAll("@cmpxchgStrong"),
681+
},
682+
}
683+
}
684+
685+
/// Returns true if the type may be usable with this atomic operation.
686+
/// This does not check that the type actually fits within the target's atomic size constriants.
687+
/// This function must be kept in sync with the compiler implementation.
688+
fn isValidAtomicType(op: Op, comptime T: type) bool {
689+
const supports_floats = switch (op) {
690+
.load, .store => true,
691+
.rmw => |rmw| switch (rmw) {
692+
.Xchg, .Add, .Sub, .Min, .Max => true,
693+
.And, .Nand, .Or, .Xor => false,
694+
},
695+
// floats are not supported for cmpxchg because float equality differs from bitwise equality
696+
.cmpxchg => false,
697+
};
698+
699+
return switch (@typeInfo(T)) {
700+
.bool, .int, .@"enum", .error_set => true,
701+
.float => supports_floats,
702+
.@"struct" => |s| s.layout == .@"packed",
703+
704+
.optional => |opt| switch (@typeInfo(opt.child)) {
705+
.pointer => |ptr| switch (ptr.size) {
706+
.slice, .c => false,
707+
.one, .many => !ptr.is_allowzero,
708+
},
709+
},
710+
.pointer => |ptr| switch (ptr.size) {
711+
.slice => false,
712+
.one, .many, .c => true,
713+
},
714+
715+
else => false,
716+
};
717+
}
718+
719+
test isValidAtomicType {
720+
try testing.expect(isValidAtomicType(.load, u8));
721+
try testing.expect(isValidAtomicType(.load, f32));
722+
try testing.expect(isValidAtomicType(.load, bool));
723+
try testing.expect(isValidAtomicType(.load, enum { a, b, c }));
724+
try testing.expect(isValidAtomicType(.load, packed struct { a: u8, b: u8 }));
725+
try testing.expect(isValidAtomicType(.load, error{OutOfMemory}));
726+
try testing.expect(isValidAtomicType(.load, u200)); // doesn't check size
727+
728+
try testing.expect(!isValidAtomicType(.load, struct { a: u8 }));
729+
try testing.expect(!isValidAtomicType(.load, union { a: u8 }));
730+
731+
// cmpxchg doesn't support floats
732+
try testing.expect(!isValidAtomicType(.{ .cmpxchg = .weak }, f32));
733+
}
734+
735+
test supportedOnCpu {
736+
const x86 = std.Target.x86;
737+
try std.testing.expect(
738+
supportedOnCpu(.load, u64, x86.cpu.x86_64.toCpu(.x86_64)),
739+
);
740+
try std.testing.expect(
741+
!supportedOnCpu(.{ .cmpxchg = .weak }, u128, x86.cpu.x86_64.toCpu(.x86_64)),
742+
);
743+
try std.testing.expect(
744+
supportedOnCpu(.{ .cmpxchg = .weak }, u128, x86.cpu.x86_64_v2.toCpu(.x86_64)),
745+
);
746+
747+
const aarch64 = std.Target.aarch64;
748+
try std.testing.expect(
749+
supportedOnCpu(.load, u64, aarch64.cpu.generic.toCpu(.aarch64)),
750+
);
751+
}
752+
753+
test supportedSizes {
754+
const sizes = supportedSizes(.{ .cmpxchg = .strong }, .x86);
755+
756+
try std.testing.expect(sizes.get(4) != null);
757+
try std.testing.expect(sizes.get(4).?.isEmpty());
758+
759+
try std.testing.expect(sizes.get(8) != null);
760+
try std.testing.expect(std.Target.x86.featureSetHas(sizes.get(8).?, .cx8));
761+
762+
try std.testing.expect(sizes.get(16) == null);
763+
}
764+
765+
test "wasm only supports atomics when the feature is enabled" {
766+
const cpu = std.Target.wasm.cpu;
767+
try std.testing.expect(
768+
!supportedOnCpu(.store, u32, cpu.mvp.toCpu(.wasm32)),
769+
);
770+
try std.testing.expect(
771+
supportedOnCpu(.store, u32, cpu.bleeding_edge.toCpu(.wasm32)),
772+
);
773+
}
774+
775+
test "wasm32 supports up to 64-bit atomics" {
776+
const bleeding = std.Target.wasm.cpu.bleeding_edge.toCpu(.wasm32);
777+
try std.testing.expect(
778+
supportedOnCpu(.store, u64, bleeding),
779+
);
780+
try std.testing.expect(
781+
!supportedOnCpu(.store, u128, bleeding),
782+
);
783+
784+
const sizes = supportedSizes(.{ .rmw = .Add }, .wasm32);
785+
try std.testing.expect(sizes.supported == 0b1111);
786+
}
787+
788+
test "wasm32 doesn't support min, max, or nand RMW ops" {
789+
const bleeding = std.Target.wasm.cpu.bleeding_edge.toCpu(.wasm32);
790+
try std.testing.expect(
791+
!supportedOnCpu(.{ .rmw = .Min }, u32, bleeding),
792+
);
793+
try std.testing.expect(
794+
!supportedOnCpu(.{ .rmw = .Max }, u32, bleeding),
795+
);
796+
try std.testing.expect(
797+
!supportedOnCpu(.{ .rmw = .Nand }, u32, bleeding),
798+
);
799+
}
800+
801+
test "x86_64 supports 128-bit cmpxchg with cx16 flag" {
802+
const x86 = std.Target.x86;
803+
const v2 = x86.cpu.x86_64_v2.toCpu(.x86_64);
804+
try std.testing.expect(
805+
supportedOnCpu(.{ .cmpxchg = .strong }, u128, v2),
806+
);
807+
808+
const sizes = supportedSizes(.{ .cmpxchg = .strong }, .x86_64);
809+
try std.testing.expect(sizes.get(16) != null);
810+
try std.testing.expect(x86.featureSetHas(sizes.get(16).?, .cx16));
811+
}
812+
};
813+
814+
test Op {
815+
try std.testing.expect(
816+
// Query atomic operation support for a specific CPU
817+
Op.supportedOnCpu(.load, u64, std.Target.aarch64.cpu.generic.toCpu(.aarch64)),
818+
);
819+
820+
// Query atomic operation support for the target CPU
821+
_ = Op.supported(.load, u64);
822+
}
823+
484824
const std = @import("std.zig");
485825
const builtin = @import("builtin");
486826
const AtomicOrder = std.builtin.AtomicOrder;

0 commit comments

Comments
 (0)