Skip to content

Commit 0bdf5f0

Browse files
committed
feat: add per-lane popcount on batch
Count the bits set in each element of an integer batch. The common kernel is the SWAR fold; x86 uses the PSHUFB nibble lookup from SSSE3 up, NEON uses CNT with pairwise widening adds, SVE uses svcnt_x, and WASM uses i8x16.popcnt with pairwise widening extends, folding 32-bit counts with a shift-and-add pair for 64-bit elements. VSX and VXE use vec_popcnt, which the compiler maps to a single VPOPCNTB/H/W/D or VPOPCT. For 64-bit elements the two x86 nibble tables carry a +4 and a -4 bias, after libpopcnt, so PSADBW yields the byte count and the 8-byte sum in one instruction. This drops the VPADDB, and measures 1.09x on SSE and AVX2 and 1.05x on AVX-512. avx512vnni gains a 32-bit kernel: VPDPBUSD does in one uop what the VPMADDUBSW and VPMADDWD pair does in two, and the zero accumulator is free because the register copy is eliminated at rename. This measures 1.14x. The same substitution on 256-bit vectors is neutral, since three ports serve them, so avxvnni gets no kernel. The CI job labelled avx512vnni built for knm, which enables avx5124vnniw rather than avx512vnni and selected the avx512pf arch, so it covered neither kernel. It now builds for cascadelake. Assisted-by: Claude Opus 5 <noreply@anthropic.com>
1 parent e3cdb6a commit 0bdf5f0

18 files changed

Lines changed: 372 additions & 1 deletion

.github/workflows/linux.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,8 @@ jobs:
116116
CMAKE_EXTRA_ARGS="$CMAKE_EXTRA_ARGS -DTARGET_ARCH=icelake-server"
117117
fi
118118
if [[ '${{ matrix.sys.flags }}' == 'avx512vnni' ]]; then
119-
CMAKE_EXTRA_ARGS="$CMAKE_EXTRA_ARGS -DTARGET_ARCH=knm"
119+
# knm enables avx5124vnniw, not avx512vnni: it selects the avx512pf arch
120+
CMAKE_EXTRA_ARGS="$CMAKE_EXTRA_ARGS -DTARGET_ARCH=cascadelake"
120121
fi
121122
if [[ '${{ matrix.sys.flags }}' == 'i386' ]]; then
122123
CXX_FLAGS="$CXX_FLAGS -m32"

docs/source/api/bitwise_operators_index.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ Bitwise Operators
4848
+---------------------------------------+----------------------------------------------------+
4949
| :cpp:func:`rotl` | per slot rotate left |
5050
+---------------------------------------+----------------------------------------------------+
51+
| :cpp:func:`popcount` | per slot population count |
52+
+---------------------------------------+----------------------------------------------------+
5153

5254
----
5355

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/***************************************************************************
2+
* Copyright (c) Johan Mabille, Sylvain Corlay, Wolf Vollprecht and *
3+
* Martin Renou *
4+
* Copyright (c) QuantStack *
5+
* Copyright (c) Serge Guelton *
6+
* Copyright (c) Marco Barbone *
7+
* *
8+
* Distributed under the terms of the BSD 3-Clause License. *
9+
* *
10+
* The full license is in the file LICENSE, distributed with this software. *
11+
****************************************************************************/
12+
13+
#ifndef XSIMD_COMMON_BITWISE_HPP
14+
#define XSIMD_COMMON_BITWISE_HPP
15+
16+
#include "./xsimd_common_details.hpp"
17+
18+
#include <climits>
19+
#include <cstddef>
20+
21+
namespace xsimd
22+
{
23+
24+
namespace kernel
25+
{
26+
27+
using namespace types;
28+
29+
namespace detail
30+
{
31+
// bit i is set when i / s is even: 0x55.. for s == 1, 0x33.. for
32+
// s == 2, 0x0f0f.. for s == 4, and so on up to s == bits / 2
33+
template <class U>
34+
constexpr U alternating_mask(unsigned s) noexcept
35+
{
36+
U m = 0;
37+
for (unsigned i = 0; i < sizeof(U) * CHAR_BIT; ++i)
38+
if (((i / s) & 1u) == 0u)
39+
m = U(m | U(U(1) << i));
40+
return m;
41+
}
42+
}
43+
44+
// popcount
45+
// SWAR fold, popcount64b from Hacker's Delight, listed in
46+
// https://en.wikipedia.org/wiki/Hamming_weight#Efficient_implementation
47+
template <class A, class T, class /*=std::enable_if_t<std::is_integral_v<T>>*/>
48+
XSIMD_INLINE batch<T, A> popcount(batch<T, A> const& self, requires_arch<common>) noexcept
49+
{
50+
using U = as_unsigned_integer_t<T>;
51+
using b_type = batch<U, A>;
52+
constexpr std::size_t bits = sizeof(T) * CHAR_BIT;
53+
54+
b_type x = bitwise_cast<U>(self);
55+
x = x - ((x >> 1) & b_type(detail::alternating_mask<U>(1)));
56+
b_type const m2(detail::alternating_mask<U>(2));
57+
x = (x & m2) + ((x >> 2) & m2);
58+
x = (x + (x >> 4)) & b_type(detail::alternating_mask<U>(4));
59+
if constexpr (bits >= 16)
60+
x = (x + (x >> 8)) & b_type(detail::alternating_mask<U>(8));
61+
if constexpr (bits >= 32)
62+
x = (x + (x >> 16)) & b_type(detail::alternating_mask<U>(16));
63+
if constexpr (bits >= 64)
64+
x = (x + (x >> 32)) & b_type(detail::alternating_mask<U>(32));
65+
return bitwise_cast<T>(x);
66+
}
67+
}
68+
}
69+
70+
#endif

include/xsimd/arch/xsimd_avx.hpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1411,6 +1411,15 @@ namespace xsimd
14111411
return _mm256_castps_si256(_mm256_xor_ps(_mm256_castsi256_ps(self.data), _mm256_castsi256_ps(other.data)));
14121412
}
14131413

1414+
// popcount
1415+
template <class A, class T, class = std::enable_if_t<std::is_integral_v<T>>>
1416+
XSIMD_INLINE batch<T, A> popcount(batch<T, A> const& self, requires_arch<avx>) noexcept
1417+
{
1418+
return detail::fwd_to_sse([](__m128i s) noexcept
1419+
{ return popcount(batch<T, ssse3>(s), ssse3 {}); },
1420+
self);
1421+
}
1422+
14141423
// reciprocal
14151424
template <class A>
14161425
XSIMD_INLINE batch<float, A> reciprocal(batch<float, A> const& self,

include/xsimd/arch/xsimd_avx2.hpp

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -962,6 +962,38 @@ namespace xsimd
962962
{ return batch<uint64_t, A>(_mm256_mul_epu32(a, b)); });
963963
}
964964

965+
// popcount
966+
template <class A, class T, class = std::enable_if_t<std::is_integral_v<T>>>
967+
XSIMD_INLINE batch<T, A> popcount(batch<T, A> const& self, requires_arch<avx2>) noexcept
968+
{
969+
// per-byte counts from a nibble lookup indexed by VPSHUFB, after
970+
// Wojciech Muła, http://0x80.pl/articles/sse-popcount.html
971+
__m256i const low_mask = _mm256_set1_epi8(0x0f);
972+
__m256i const lo = _mm256_and_si256(self, low_mask);
973+
__m256i const hi = _mm256_and_si256(_mm256_srli_epi16(self, 4), low_mask);
974+
if constexpr (sizeof(T) == 8)
975+
{
976+
// tables biased by +4 and -4 turn the VPSADBW difference into the
977+
// per-byte count, so one instruction adds the nibble counts and
978+
// sums the eight bytes, after https://github.com/kimwalisch/libpopcnt
979+
__m256i const lookup_lo = _mm256_broadcastsi128_si256(_mm_setr_epi8(4, 5, 5, 6, 5, 6, 6, 7, 5, 6, 6, 7, 6, 7, 7, 8));
980+
__m256i const lookup_hi = _mm256_broadcastsi128_si256(_mm_setr_epi8(4, 3, 3, 2, 3, 2, 2, 1, 3, 2, 2, 1, 2, 1, 1, 0));
981+
return _mm256_sad_epu8(_mm256_shuffle_epi8(lookup_lo, lo), _mm256_shuffle_epi8(lookup_hi, hi));
982+
}
983+
else
984+
{
985+
__m256i const lookup = _mm256_broadcastsi128_si256(_mm_setr_epi8(0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4));
986+
__m256i const counts = _mm256_add_epi8(_mm256_shuffle_epi8(lookup, lo), _mm256_shuffle_epi8(lookup, hi));
987+
// wider elements sum their byte counts
988+
if constexpr (sizeof(T) == 1)
989+
return counts;
990+
else if constexpr (sizeof(T) == 2)
991+
return _mm256_maddubs_epi16(counts, _mm256_set1_epi8(1));
992+
else
993+
return _mm256_madd_epi16(_mm256_maddubs_epi16(counts, _mm256_set1_epi8(1)), _mm256_set1_epi16(1));
994+
}
995+
}
996+
965997
// reduce_add
966998
template <class A, class T, class = std::enable_if_t<std::is_integral_v<T>>>
967999
XSIMD_INLINE T reduce_add(batch<T, A> const& self, requires_arch<avx2>) noexcept

include/xsimd/arch/xsimd_avx512bw.hpp

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -561,6 +561,49 @@ namespace xsimd
561561
return detail::compare_int_avx512bw<A, T, _MM_CMPINT_NE>(self, other);
562562
}
563563

564+
namespace detail
565+
{
566+
// per-byte counts from a nibble lookup indexed by VPSHUFB, after
567+
// Wojciech Muła, http://0x80.pl/articles/sse-popcount.html
568+
XSIMD_INLINE __m512i popcount_bytes(__m512i self) noexcept
569+
{
570+
__m512i const low_mask = _mm512_set1_epi8(0x0f);
571+
__m512i const lookup = _mm512_broadcast_i32x4(_mm_setr_epi8(0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4));
572+
__m512i const lo = _mm512_and_si512(self, low_mask);
573+
__m512i const hi = _mm512_and_si512(_mm512_srli_epi16(self, 4), low_mask);
574+
return _mm512_add_epi8(_mm512_shuffle_epi8(lookup, lo), _mm512_shuffle_epi8(lookup, hi));
575+
}
576+
}
577+
578+
// popcount
579+
template <class A, class T, class = std::enable_if_t<std::is_integral_v<T>>>
580+
XSIMD_INLINE batch<T, A> popcount(batch<T, A> const& self, requires_arch<avx512bw>) noexcept
581+
{
582+
if constexpr (sizeof(T) == 8)
583+
{
584+
__m512i const low_mask = _mm512_set1_epi8(0x0f);
585+
__m512i const lo = _mm512_and_si512(self, low_mask);
586+
__m512i const hi = _mm512_and_si512(_mm512_srli_epi16(self, 4), low_mask);
587+
// tables biased by +4 and -4 turn the VPSADBW difference into the
588+
// per-byte count, so one instruction adds the nibble counts and
589+
// sums the eight bytes, after https://github.com/kimwalisch/libpopcnt
590+
__m512i const lookup_lo = _mm512_broadcast_i32x4(_mm_setr_epi8(4, 5, 5, 6, 5, 6, 6, 7, 5, 6, 6, 7, 6, 7, 7, 8));
591+
__m512i const lookup_hi = _mm512_broadcast_i32x4(_mm_setr_epi8(4, 3, 3, 2, 3, 2, 2, 1, 3, 2, 2, 1, 2, 1, 1, 0));
592+
return _mm512_sad_epu8(_mm512_shuffle_epi8(lookup_lo, lo), _mm512_shuffle_epi8(lookup_hi, hi));
593+
}
594+
else
595+
{
596+
__m512i const counts = detail::popcount_bytes(self);
597+
// wider elements sum their byte counts
598+
if constexpr (sizeof(T) == 1)
599+
return counts;
600+
else if constexpr (sizeof(T) == 2)
601+
return _mm512_maddubs_epi16(counts, _mm512_set1_epi8(1));
602+
else
603+
return _mm512_madd_epi16(_mm512_maddubs_epi16(counts, _mm512_set1_epi8(1)), _mm512_set1_epi16(1));
604+
}
605+
}
606+
564607
// sadd
565608
template <class A, class T, class = std::enable_if_t<std::is_integral_v<T>>>
566609
XSIMD_INLINE batch<T, A> sadd(batch<T, A> const& self, batch<T, A> const& other, requires_arch<avx512bw>) noexcept

include/xsimd/arch/xsimd_avx512vnni_avx512bw.hpp

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,26 @@
1313
#define XSIMD_AVX512VNNI_AVX512_BW_HPP
1414

1515
#include "../types/xsimd_avx512vnni_avx512bw_register.hpp"
16+
#include "./xsimd_avx512bw.hpp"
17+
18+
namespace xsimd
19+
{
20+
21+
namespace kernel
22+
{
23+
24+
using namespace types;
25+
26+
// popcount
27+
template <class A, class T, class = std::enable_if_t<std::is_integral_v<T> && sizeof(T) == 4>>
28+
XSIMD_INLINE batch<T, A> popcount(batch<T, A> const& self, requires_arch<avx512vnni<avx512bw>>) noexcept
29+
{
30+
// VPDPBUSD does in one uop what the VPMADDUBSW + VPMADDWD pair of
31+
// the avx512bw kernel does in two; the zero accumulator it needs
32+
// costs nothing, since the register copy is eliminated at rename
33+
return _mm512_dpbusd_epi32(_mm512_setzero_si512(), detail::popcount_bytes(self), _mm512_set1_epi8(1));
34+
}
35+
}
36+
}
1637

1738
#endif

include/xsimd/arch/xsimd_avx512vnni_avx512vbmi2.hpp

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,27 @@
1313
#define XSIMD_AVX512VNNI_AVX512VBMI2_HPP
1414

1515
#include "../types/xsimd_avx512vnni_avx512vbmi2_register.hpp"
16+
#include "./xsimd_avx512bw.hpp"
17+
#include "./xsimd_avx512vbmi2.hpp"
18+
19+
namespace xsimd
20+
{
21+
22+
namespace kernel
23+
{
24+
25+
using namespace types;
26+
27+
// popcount
28+
template <class A, class T, class = std::enable_if_t<std::is_integral_v<T> && sizeof(T) == 4>>
29+
XSIMD_INLINE batch<T, A> popcount(batch<T, A> const& self, requires_arch<avx512vnni<avx512vbmi2>>) noexcept
30+
{
31+
// VPDPBUSD does in one uop what the VPMADDUBSW + VPMADDWD pair of
32+
// the avx512bw kernel does in two; the zero accumulator it needs
33+
// costs nothing, since the register copy is eliminated at rename
34+
return _mm512_dpbusd_epi32(_mm512_setzero_si512(), detail::popcount_bytes(self), _mm512_set1_epi8(1));
35+
}
36+
}
37+
}
1638

1739
#endif

include/xsimd/arch/xsimd_common.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
#include "./common/xsimd_common_arithmetic.hpp"
1616
#include "./common/xsimd_common_bit.hpp"
17+
#include "./common/xsimd_common_bitwise.hpp"
1718
#include "./common/xsimd_common_cast.hpp"
1819
#include "./common/xsimd_common_complex.hpp"
1920
#include "./common/xsimd_common_logical.hpp"

include/xsimd/arch/xsimd_common_fwd.hpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ namespace xsimd
8585
XSIMD_INLINE batch<T, A> rotr(batch<T, A> const& self, STy other, requires_arch<common>) noexcept;
8686
template <size_t count, class A, class T>
8787
XSIMD_INLINE batch<T, A> rotr(batch<T, A> const& self, requires_arch<common>) noexcept;
88+
template <class A, class T, class = std::enable_if_t<std::is_integral_v<T>>>
89+
XSIMD_INLINE batch<T, A> popcount(batch<T, A> const& self, requires_arch<common>) noexcept;
8890
template <class A, class T>
8991
XSIMD_INLINE batch<T, A> load(T const* mem, aligned_mode, requires_arch<A>) noexcept;
9092
template <class A, class T>

0 commit comments

Comments
 (0)