Compiler Builtins and Bit Operations

Last update: 2022-03-03 23:02:53 +0800 +0800
Available translations: 日本語 / 中文(简体) / 中文(繁體)

GCC and Clang provide builtin spellings for operations that their front ends recognize directly. Bit-counting builtins can lower to a target instruction, a library sequence, or an optimized expression depending on the selected target and optimization context. Source code should choose the interface that states its portability and zero-input contract.

GNU-Style Bit Builtins #

Both GCC and Clang support these families for unsigned int, unsigned long, and unsigned long long:

Operationunsigned intunsigned longunsigned long long
Number of set bits__builtin_popcount__builtin_popcountl__builtin_popcountll
Leading zero bits__builtin_clz__builtin_clzl__builtin_clzll
Trailing zero bits__builtin_ctz__builtin_ctzl__builtin_ctzll

The return type is int; the suffix selects the argument width. Cast deliberately instead of relying on integer promotions when the logical width matters. unsigned long has different widths on common ABIs, so use the ll form with an unsigned long long value or a fixed-width standard function when the algorithm requires a particular width.

__builtin_clz(0) and __builtin_ctz(0) have undefined behavior. Test first when zero is possible:

cpp
#include <limits>

unsigned trailing_or_width(unsigned value) {
    return value == 0 ? std::numeric_limits<unsigned>::digits
                      : static_cast<unsigned>(__builtin_ctz(value));
}

popcount(0) is defined and returns zero, but using a consistent standard interface avoids creating different contracts for related operations.

Prefer C++20 <bit> for Portable C++ #

C++20 supplies std::popcount, std::countl_zero, and std::countr_zero in <bit> for unsigned integer types. Unlike the GNU leading and trailing zero builtins, the counting functions define their result for zero as the number of value bits in the type:

cpp
#include <bit>
#include <cstdint>

constexpr std::uint32_t bits = 0b101101;
static_assert(std::popcount(bits) == 4);
static_assert(std::countl_zero(std::uint32_t{0}) == 32);
static_assert(std::countr_zero(std::uint32_t{16}) == 4);

Check __cpp_lib_bitops after including <bit> when a library feature boundary must be supported; C++20 specifies the value 201907L for this feature. For compiler-specific code, test builtin availability with __has_builtin only after guarding its use:

cpp
#if defined(__cpp_lib_bitops) && __cpp_lib_bitops >= 201907L
// Use std::popcount, std::countl_zero, and std::countr_zero.
#elif defined(__has_builtin)
#  if __has_builtin(__builtin_popcount)
#    define PROJECT_HAS_BUILTIN_POPCOUNT 1
#  endif
#endif

GCC and Clang document these particular builtins, so a project that explicitly supports both may instead centralize compiler-family checks and retain a portable fallback for other compilers. Do not assume that __has_builtin exists, and do not use it to select a builtin whose zero precondition the public API does not enforce.

Code Generation and Constant Evaluation #

Inspect generated code for a performance-critical target, because an intrinsic spelling does not promise one instruction. Hardware support, target features, operand width, surrounding control flow, and optimization level determine lowering. Standard <bit> functions give optimizers equivalent semantic information on conforming implementations while retaining portability and use in constant evaluation when their operands and library implementation permit it. A builtin fallback can be warranted for a pre-C++20 library or a measured target gap, but it should remain behind one width-aware wrapper with documented preconditions.

Comments

Read or comment on GitHub (sign-in required to comment)