From c1df6c86f8c8013068028262846831157a5790b8 Mon Sep 17 00:00:00 2001 From: Jordan Peck Date: Sat, 17 Oct 2026 17:52:48 +0100 Subject: [PATCH 1/2] AArch64: BitMask by weighting lanes and adding across 4 lanes: AND with the lane weights + ADDV instead of USHR + USHL + ADDV. 8 lanes (2 x native, also used by the 512 and 1024 bit composites): UZP2 narrows both halves into one register of 16 bit lanes, then one AND + ADDV instead of a reduction per half (GCC 13: 9 instructions to 4). Both read one bit per lane rather than the sign bit, so like AnyMask they rely on each mask lane being all ones or all zeros. The NEON level keeps the existing versions. Covered by the m32/m32i bit mask to int tests. --- include/FastSIMD/ToolSet/ARM/128/m32x4.h | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/include/FastSIMD/ToolSet/ARM/128/m32x4.h b/include/FastSIMD/ToolSet/ARM/128/m32x4.h index 2074f14..0fa9a29 100644 --- a/include/FastSIMD/ToolSet/ARM/128/m32x4.h +++ b/include/FastSIMD/ToolSet/ARM/128/m32x4.h @@ -75,9 +75,10 @@ namespace FS { if constexpr( SIMD & FastSIMD::FeatureFlag::AARCH64 ) { - static const int32_t shift[4] = { 0, 1, 2, 3 }; - uint32x4_t tmp = vshrq_n_u32( a.native, 31 ); - return vaddvq_u32( vshlq_u32( tmp, vld1q_s32( shift ) ) ); + // Weight each lane by its bit and add across. Reads one bit per lane, not the sign bit, so like AnyMask it relies + // on each lane being all ones or all zeros + static const uint32_t weights[4] = { 1, 2, 4, 8 }; + return vaddvq_u32( vandq_u32( a.native, vld1q_u32( weights ) ) ); } else { @@ -91,5 +92,15 @@ namespace FS return vgetq_lane_u8( paired, 0 ) | ( vgetq_lane_u8( paired, 8 ) << 2 ); } } - + + // 8 lane AARCH64 masks (2 x native): narrow both halves into one register of 16 bit lanes (the top half of each lane), + // then weight and add across once instead of once per half. Same all ones or all zeros requirement as the 4 lane version + template> + FS_FORCEINLINE BitStorage<8> BitMask( const Register, 8, SIMD>& a ) + { + static const uint16_t weights[8] = { 1, 2, 4, 8, 16, 32, 64, 128 }; + uint16x8_t lanes = vuzp2q_u16( vreinterpretq_u16_u32( a.v0.native ), vreinterpretq_u16_u32( a.v1.native ) ); + return static_cast>( vaddvq_u16( vandq_u16( lanes, vld1q_u16( weights ) ) ) ); + } + } \ No newline at end of file From 0275c69ffabd585f76069f5b24ba1d469ef4af41 Mon Sep 17 00:00:00 2001 From: Jordan Peck Date: Sat, 17 Oct 2026 17:52:48 +0100 Subject: [PATCH 2/2] AArch64: Convert float to int32 with FCVTNS Round + vcvtq_s32_f32 is FRINTN + FCVTZS with GCC 13 and clang 15/18; vcvtnq_s32_f32 rounds to nearest even in one instruction with the same results (ties to even, saturation, NaN -> 0). Clang 23 already fuses the pair into FCVTNS, but through the intrinsic it no longer fuses a preceding Floor into FCVTMS (or Round into FCVTNS). That only costs an instruction when the rounded float isn't used too: in FastNoise2 master clang 23 swaps all 120 FCVTMS for FCVTNS with no extra FRINTM. The NEON level keeps Round + vcvtq. --- include/FastSIMD/ToolSet/ARM/NEON.h | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/include/FastSIMD/ToolSet/ARM/NEON.h b/include/FastSIMD/ToolSet/ARM/NEON.h index 3d48a06..0d3ac7e 100644 --- a/include/FastSIMD/ToolSet/ARM/NEON.h +++ b/include/FastSIMD/ToolSet/ARM/NEON.h @@ -10,7 +10,15 @@ namespace FS template>> FS_FORCEINLINE i32<4, SIMD> Convert( const f32<4, SIMD>& a, TypeDummy ) { - return vcvtq_s32_f32( Round( a ).native ); + if constexpr( SIMD & FastSIMD::FeatureFlag::AARCH64 ) + { + // FCVTNS rounds to nearest even like Round, in one instruction + return vcvtnq_s32_f32( a.native ); + } + else + { + return vcvtq_s32_f32( Round( a ).native ); + } } template>>