@@ -481,6 +481,331 @@ test "current CPU has a cache line size" {
481
481
_ = cache_line ;
482
482
}
483
483
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
+ fn isValidAtomicType (op : Op , comptime T : type ) bool {
688
+ return switch (@typeInfo (T )) {
689
+ .bool , .int , .@"enum" , .error_set = > true ,
690
+
691
+ // floats are not supported for cmpxchg because float equality differs from bitwise equality
692
+ .float = > op != .cmpxchg ,
693
+
694
+ .@"struct" = > | s | s .layout == .@"packed" ,
695
+
696
+ .optional = > | opt | switch (@typeInfo (opt .child )) {
697
+ .pointer = > | ptr | switch (ptr .size ) {
698
+ .slice , .c = > false ,
699
+ .one , .many = > ! ptr .is_allowzero ,
700
+ },
701
+ },
702
+ .pointer = > | ptr | switch (ptr .size ) {
703
+ .slice = > false ,
704
+ .one , .many , .c = > true ,
705
+ },
706
+
707
+ else = > false ,
708
+ };
709
+ }
710
+
711
+ test isValidAtomicType {
712
+ try testing .expect (isValidAtomicType (u8 ));
713
+ try testing .expect (isValidAtomicType (f32 ));
714
+ try testing .expect (isValidAtomicType (bool ));
715
+ try testing .expect (isValidAtomicType (enum { a , b , c }));
716
+ try testing .expect (isValidAtomicType (packed struct { a : u8 , b : u8 }));
717
+ try testing .expect (isValidAtomicType (packed union { a : u8 , b : u8 }));
718
+ try testing .expect (isValidAtomicType (error {OutOfMemory }));
719
+ try testing .expect (isValidAtomicType (u200 )); // doesn't check size
720
+
721
+ try testing .expect (! isValidAtomicType (struct { a : u8 }));
722
+ try testing .expect (! isValidAtomicType (union { a : u8 }));
723
+ }
724
+
725
+ test supportedOnCpu {
726
+ const cpu = std .Target .x86 .cpu ;
727
+ try std .testing .expect (
728
+ supportedOnCpu (.load , u64 , cpu .x86_64 .toCpu (.x86_64 )),
729
+ );
730
+ try std .testing .expect (
731
+ ! supportedOnCpu (.{ .cmpxchg = .weak }, u128 , cpu .x86_64 .toCpu (.x86_64 )),
732
+ );
733
+ try std .testing .expect (
734
+ supportedOnCpu (.{ .cmpxchg = .weak }, u128 , cpu .x86_64_v2 .toCpu (.x86_64 )),
735
+ );
736
+ }
737
+
738
+ test supportedSizes {
739
+ const sizes = supportedSizes (.{ .cmpxchg = .strong }, .x86 );
740
+
741
+ try std .testing .expect (sizes .get (4 ) != null );
742
+ try std .testing .expect (sizes .get (4 ).? .isEmpty ());
743
+
744
+ try std .testing .expect (sizes .get (8 ) != null );
745
+ try std .testing .expect (std .Target .x86 .featureSetHas (sizes .get (8 ).? , .cx8 ));
746
+
747
+ try std .testing .expect (sizes .get (16 ) == null );
748
+ }
749
+
750
+ test "wasm only supports atomics when the feature is enabled" {
751
+ const cpu = std .Target .wasm .cpu ;
752
+ try std .testing .expect (
753
+ ! supportedOnCpu (.store , u32 , cpu .mvp .toCpu (.wasm32 )),
754
+ );
755
+ try std .testing .expect (
756
+ supportedOnCpu (.store , u32 , cpu .bleeding_edge .toCpu (.wasm32 )),
757
+ );
758
+ }
759
+
760
+ test "wasm32 supports up to 64-bit atomics" {
761
+ const bleeding = std .Target .wasm .cpu .bleeding_edge .toCpu (.wasm32 );
762
+ try std .testing .expect (
763
+ supportedOnCpu (.store , u64 , bleeding ),
764
+ );
765
+ try std .testing .expect (
766
+ ! supportedOnCpu (.store , u128 , bleeding ),
767
+ );
768
+
769
+ const sizes = supportedSizes (.{ .rmw = .Add }, .wasm32 );
770
+ try std .testing .expect (sizes .supported == 0b1111 );
771
+ }
772
+
773
+ test "wasm32 doesn't support min, max, or nand RMW ops" {
774
+ const bleeding = std .Target .wasm .cpu .bleeding_edge .toCpu (.wasm32 );
775
+ try std .testing .expect (
776
+ ! supportedOnCpu (.{ .rmw = .Min }, u32 , bleeding ),
777
+ );
778
+ try std .testing .expect (
779
+ ! supportedOnCpu (.{ .rmw = .Max }, u32 , bleeding ),
780
+ );
781
+ try std .testing .expect (
782
+ ! supportedOnCpu (.{ .rmw = .Nand }, u32 , bleeding ),
783
+ );
784
+ }
785
+
786
+ test "x86_64 supports 128-bit cmpxchg with cx16 flag" {
787
+ const x86 = std .Target .x86 ;
788
+ const v2 = x86 .cpu .x86_64_v2 .toCpu (.x86_64 );
789
+ try std .testing .expect (
790
+ supportedOnCpu (.{ .cmpxchg = .strong }, u128 , v2 ),
791
+ );
792
+
793
+ const sizes = supportedSizes (.{ .cmpxchg = .strong }, .x86_64 );
794
+ try std .testing .expect (sizes .get (16 ) != null );
795
+ try std .testing .expect (x86 .featureSetHas (sizes .get (16 ).? , .cx16 ));
796
+ }
797
+ };
798
+
799
+ test Op {
800
+ try std .testing .expect (
801
+ // Query atomic operation support for a specific CPU
802
+ Op .supportedOnCpu (.load , u64 , std .Target .aarch64 .cpu .generic .toCpu (.aarch64 )),
803
+ );
804
+
805
+ // Query atomic operation support for the target CPU
806
+ _ = Op .supported (.load , u64 );
807
+ }
808
+
484
809
const std = @import ("std.zig" );
485
810
const builtin = @import ("builtin" );
486
811
const AtomicOrder = std .builtin .AtomicOrder ;
0 commit comments