Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ENH: Adding bit_count (popcount) #19355

Merged
merged 16 commits into from Nov 1, 2021
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
13 changes: 13 additions & 0 deletions doc/release/upcoming_changes/19355.new_feature.rst
@@ -0,0 +1,13 @@
`bit_count` to compute the number of 1-bits in an integer
ganesh-k13 marked this conversation as resolved.
Show resolved Hide resolved
---------------------------------------------------------

Computes the number of 1-bits in the absolute value of the input.
This works on all the numpy integer types. Analogous to the builtin
`int.bit_count` or `popcount` in C++.
ganesh-k13 marked this conversation as resolved.
Show resolved Hide resolved

.. code-block:: python

>>> np.uint32(1023).bit_count()
10
>>> np.int32(-127).bit_count()
7
1 change: 1 addition & 0 deletions numpy/__init__.pyi
Expand Up @@ -2985,6 +2985,7 @@ class integer(number[_NBit1]): # type: ignore
) -> int: ...
def tolist(self) -> int: ...
def is_integer(self) -> L[True]: ...
def bit_count(self: _ScalarType) -> int: ...
def __index__(self) -> int: ...
__truediv__: _IntTrueDiv[_NBit1]
__rtruediv__: _IntTrueDiv[_NBit1]
Expand Down
19 changes: 19 additions & 0 deletions numpy/core/_add_newdocs_scalars.py
Expand Up @@ -290,3 +290,22 @@ def add_newdoc_for_scalar_type(obj, fixed_aliases, doc):
>>> np.{float_name}(3.2).is_integer()
False
"""))

for int_name in ('int8', 'uint8', 'int16', 'uint16', 'int32', 'uint32',
'int64', 'uint64', 'int64', 'uint64', 'int64', 'uint64'):
# Add negative examples for signed cases by checking typecode
add_newdoc('numpy.core.numerictypes', int_name, ('bit_count',
ganesh-k13 marked this conversation as resolved.
Show resolved Hide resolved
f"""
{int_name}.bit_count() -> int

Computes the number of 1-bits in the absolute value of the input.
Analogous to the builtin `int.bit_count` or ``popcount`` in C++.

Examples
--------
>>> np.{int_name}(127).bit_count()
7""" +
(f"""
>>> np.{int_name}(-127).bit_count()
7
""" if dtype(int_name).char.islower() else "")))
11 changes: 11 additions & 0 deletions numpy/core/include/numpy/npy_math.h
Expand Up @@ -150,6 +150,17 @@ NPY_INPLACE npy_long npy_lshiftl(npy_long a, npy_long b);
NPY_INPLACE npy_longlong npy_rshiftll(npy_longlong a, npy_longlong b);
NPY_INPLACE npy_longlong npy_lshiftll(npy_longlong a, npy_longlong b);

NPY_INPLACE uint8_t npy_popcountuhh(npy_ubyte a);
NPY_INPLACE uint8_t npy_popcountuh(npy_ushort a);
NPY_INPLACE uint8_t npy_popcountu(npy_uint a);
NPY_INPLACE uint8_t npy_popcountul(npy_ulong a);
NPY_INPLACE uint8_t npy_popcountull(npy_ulonglong a);
NPY_INPLACE uint8_t npy_popcounthh(npy_byte a);
NPY_INPLACE uint8_t npy_popcounth(npy_short a);
NPY_INPLACE uint8_t npy_popcount(npy_int a);
NPY_INPLACE uint8_t npy_popcountl(npy_long a);
NPY_INPLACE uint8_t npy_popcountll(npy_longlong a);

/*
* C99 double math funcs
*/
Expand Down
52 changes: 50 additions & 2 deletions numpy/core/src/multiarray/scalartypes.c.src
Expand Up @@ -208,6 +208,27 @@ gentype_multiply(PyObject *m1, PyObject *m2)
return PyArray_Type.tp_as_number->nb_multiply(m1, m2);
}

/**begin repeat
* #TYPE = BYTE, UBYTE, SHORT, USHORT, INT, UINT,
* LONG, ULONG, LONGLONG, ULONGLONG#
* #type = npy_byte, npy_ubyte, npy_short, npy_ushort, npy_int, npy_uint,
* npy_long, npy_ulong, npy_longlong, npy_ulonglong#
* #c = hh, uhh, h, uh,, u, l, ul, ll, ull#
* #Name = Byte, UByte, Short, UShort, Int, UInt,
* Long, ULong, LongLong, ULongLong#
* #convert = Long*8, LongLong*2#
*/
static PyObject *
@type@_bit_count(PyObject *self)
{
@type@ scalar = PyArrayScalar_VAL(self, @Name@);
uint8_t count = npy_popcount@c@(scalar);
PyObject *result = PyLong_From@convert@(count);

return result;
}
/**end repeat**/

/**begin repeat
*
* #name = positive, negative, absolute, invert, int, float#
Expand Down Expand Up @@ -2306,8 +2327,7 @@ static PyMethodDef @name@type_methods[] = {
/**end repeat**/

/**begin repeat
* #name = byte, short, int, long, longlong, ubyte, ushort,
* uint, ulong, ulonglong, timedelta, cdouble#
* #name = timedelta, cdouble#
*/
static PyMethodDef @name@type_methods[] = {
/* for typing; requires python >= 3.9 */
Expand All @@ -2318,6 +2338,23 @@ static PyMethodDef @name@type_methods[] = {
};
/**end repeat**/

/**begin repeat
* #name = byte, ubyte, short, ushort, int, uint,
* long, ulong, longlong, ulonglong#
*/
static PyMethodDef @name@type_methods[] = {
/* for typing; requires python >= 3.9 */
{"__class_getitem__",
(PyCFunction)numbertype_class_getitem,
METH_CLASS | METH_O, NULL},
seberg marked this conversation as resolved.
Show resolved Hide resolved
{"bit_count",
(PyCFunction)npy_@name@_bit_count,
METH_NOARGS, NULL},
{NULL, NULL, 0, NULL} /* sentinel */
};
/**end repeat**/


/************* As_mapping functions for void array scalar ************/

static Py_ssize_t
Expand Down Expand Up @@ -4091,6 +4128,17 @@ initialize_numeric_types(void)

/**end repeat**/

/**begin repeat
* #name = byte, short, int, long, longlong,
* ubyte, ushort, uint, ulong, ulonglong#
* #Name = Byte, Short, Int, Long, LongLong,
* UByte, UShort, UInt, ULong, ULongLong#
*/

Py@Name@ArrType_Type.tp_methods = @name@type_methods;

/**end repeat**/

/**begin repeat
* #name = half, float, double, longdouble#
* #Name = Half, Float, Double, LongDouble#
Expand Down
86 changes: 86 additions & 0 deletions numpy/core/src/npymath/npy_math_internal.h.src
Expand Up @@ -55,6 +55,29 @@
*/
#include "npy_math_private.h"

/* Magic binary numbers used by bit_count
* For type T, the magic numbers are computed as follows:
* Magic[0]: 01 01 01 01 01 01... = (T)~(T)0/3
* Magic[1]: 0011 0011 0011... = (T)~(T)0/15 * 3
* Magic[2]: 00001111 00001111... = (T)~(T)0/255 * 15
* Magic[3]: 00000001 00000001... = (T)~(T)0/255
*
* Counting bits set, in parallel
* Based on: http://graphics.stanford.edu/~seander/bithacks.html#CountBitsSetParallel
*
* Generic Algorithm for type T:
* a = a - ((a >> 1) & (T)~(T)0/3);
* a = (a & (T)~(T)0/15*3) + ((a >> 2) & (T)~(T)0/15*3);
* a = (a + (a >> 4)) & (T)~(T)0/255*15;
* c = (T)(a * ((T)~(T)0/255)) >> (sizeof(T) - 1) * CHAR_BIT;
*/

static const npy_uint8 MAGIC8[] = {0x55u, 0x33u, 0x0Fu, 0x01u};
static const npy_uint16 MAGIC16[] = {0x5555u, 0x3333u, 0x0F0Fu, 0x0101u};
static const npy_uint32 MAGIC32[] = {0x55555555ul, 0x33333333ul, 0x0F0F0F0Ful, 0x01010101ul};
static const npy_uint64 MAGIC64[] = {0x5555555555555555ull, 0x3333333333333333ull, 0x0F0F0F0F0F0F0F0Full, 0x0101010101010101ull};


/*
*****************************************************************************
** BASIC MATH FUNCTIONS **
Expand Down Expand Up @@ -814,3 +837,66 @@ npy_rshift@u@@c@(npy_@u@@type@ a, npy_@u@@type@ b)
}
/**end repeat1**/
/**end repeat**/


#define __popcnt32 __popcnt
/**begin repeat
*
* #type = ubyte, ushort, uint, ulong, ulonglong#
* #STYPE = BYTE, SHORT, INT, LONG, LONGLONG#
* #c = hh, h, , l, ll#
*/
#undef TO_BITS_LEN
#if 0
/**begin repeat1
* #len = 8, 16, 32, 64#
*/
#elif NPY_BITSOF_@STYPE@ == @len@
#define TO_BITS_LEN(X) X##@len@
/**end repeat1**/
#endif


NPY_INPLACE uint8_t
npy_popcount_parallel@c@(npy_@type@ a)
{
a = a - ((a >> 1) & (npy_@type@) TO_BITS_LEN(MAGIC)[0]);
a = ((a & (npy_@type@) TO_BITS_LEN(MAGIC)[1])) + ((a >> 2) & (npy_@type@) TO_BITS_LEN(MAGIC)[1]);
a = (a + (a >> 4)) & (npy_@type@) TO_BITS_LEN(MAGIC)[2];
return (npy_@type@) (a * (npy_@type@) TO_BITS_LEN(MAGIC)[3]) >> ((NPY_SIZEOF_@STYPE@ - 1) * CHAR_BIT);
}

NPY_INPLACE uint8_t
npy_popcountu@c@(npy_@type@ a)
{
/* use built-in popcount if present, else use our implementation */
#if (defined(__clang__) || defined(__GNUC__)) && NPY_BITSOF_@STYPE@ >= 32
return __builtin_popcount@c@(a);
#elif defined(_MSC_VER) && NPY_BITSOF_@STYPE@ >= 16
/* no builtin __popcnt64 for 32 bits */
#if defined(_WIN64) || (defined(_WIN32) && NPY_BITSOF_@STYPE@ != 64)
return TO_BITS_LEN(__popcnt)(a);
/* split 64 bit number into two 32 bit ints and return sum of counts */
#elif (defined(_WIN32) && NPY_BITSOF_@STYPE@ == 64)
npy_uint32 left = (npy_uint32) (a>>32);
npy_uint32 right = (npy_uint32) a;
return __popcnt32(left) + __popcnt32(right);
#endif
#else
return npy_popcount_parallel@c@(a);
#endif
}
/**end repeat**/

/**begin repeat
*
* #type = byte, short, int, long, longlong#
* #c = hh, h, , l, ll#
*/
NPY_INPLACE uint8_t
npy_popcount@c@(npy_@type@ a)
{
/* Return popcount of abs(a) */
return npy_popcountu@c@(a < 0 ? -a : a);
}
/**end repeat**/
18 changes: 18 additions & 0 deletions numpy/core/tests/test_scalar_methods.py
Expand Up @@ -183,3 +183,21 @@ def test_class_getitem_38(cls: Type[np.number]) -> None:
match = "Type subscription requires python >= 3.9"
with pytest.raises(TypeError, match=match):
cls[Any]


class TestBitCount:
# derived in part from the cpython test "test_bit_count"

@pytest.mark.parametrize("itype", np.sctypes['int']+np.sctypes['uint'])
def test_small(self, itype):
for a in range(max(np.iinfo(itype).min, 0), 128):
msg = f"Smoke test for {itype}({a}).bit_count()"
assert itype(a).bit_count() == bin(a).count("1"), msg

def test_bit_count(self):
for exp in [10, 17, 63]:
a = 2**exp
assert np.uint64(a).bit_count() == 1
ganesh-k13 marked this conversation as resolved.
Show resolved Hide resolved
assert np.uint64(a - 1).bit_count() == exp
assert np.uint64(a ^ 63).bit_count() == 7
assert np.uint64((a - 1) ^ 510).bit_count() == exp - 8