@@ -481,6 +481,346 @@ 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
+ /// 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
+
484
824
const std = @import ("std.zig" );
485
825
const builtin = @import ("builtin" );
486
826
const AtomicOrder = std .builtin .AtomicOrder ;
0 commit comments