Carry-less product: std::clmul
- Document number:
- P3642R7
- Date:
2026-09-21 - Audience:
- LWG
- Project:
- ISO/IEC 14882 Programming Languages — C++, ISO/IEC JTC1/SC22/WG21
- Reply-to:
- Jan Schultke <janschultke@gmail.com>
- GitHub Issue:
- wg21.link/P3642/github
- Source:
- github.com/eisenwave/cpp-proposals/blob/master/src/clmul.cow
Contents
Revision history
Changes since R6
Changes since R5
Changes since R4
Changes since R3
Changes since R2
Changes since R1
Changes since R0
Introduction
Motivation
Parity computation and JSON parsing
Fast space-filling curves
Possible implementation
Intrinsic implementation
Hardware support
Design considerations
Naming
Widening operation and std::wide_result
Aggregate type std::wide_result
Alignment of std :: wide_result < std :: uint64_t >
Integer interoperability
Naming of low() and high()
SIMD support
SIMD widening operations are out of scope
Proposed wording
[version.syn]
[numeric.ops]
[simd]
References
1. Revision history
1.1. Changes since R6
-
Convert
from an aggregate type to a class withstd :: wide_result andlow ( ) accessorshigh ( ) -
Turn the former sections containing LWG suggestions
into subsections of §5.2. Widening operation and
std::wide_result -
Make various minor updates to discussion based on recent developments,
such as Rust's
widening_carryless_mul operation - Extend drafting note in § [numeric.ops] with reference to [bit.permute]
- Rebase §6. Proposed wording on [N5054]
1.2. Changes since R5
As requested by LEWG during the 2026 Brno meeting:
- Replace the bump of
with a new__cpp_lib_simd __cpp_lib_simd_clmul - Constrain
to unsigned integer typesstd :: wide_result
Also, following LWG review during the 2026 Brno meeting:
- Apply various wording adjustments during review
- Add section discussing potential design changes discussed in LWG review
- Add wording for those design changes in optional wording sections
Furthermore, some editorial changes:
-
Mention in §4.1. Intrinsic implementation that
is available in Clang 23__builtin_elementwise_clmul - Expand §4.1. Intrinsic implementation with possible optimizations
1.3. Changes since R4
- Rename
tostd :: clmul_wide std :: widening_clmul - Fix
specified as a unary function (should be binary)std :: simd :: clmul - Rebase §6. Proposed wording on N5032 with post-Croydon changes applied
1.4. Changes since R3
- Reference faster possible implementations in §4. Possible implementation
- Add some thoughts on why implementations in the compiler are better than pure library implementations
- Mention that
@llvm.clmul has now been added to LLVM ([LLVMClmul]) - Change
foroperator <=> to be a hidden friend againwide_result
1.5. Changes since R2
- Add missing
<T> foroperator <=> - Provide a §4. Possible implementation better suited for SIMD parallelization
- Rebase §6. Proposed wording on N5014
- Improve §6. Proposed wording editorially
1.6. Changes since R1
The paper was seen by SG6 at Sofia 2025 with the following feedback:
Summary: SG6 had no numerics concerns but recommended to include std::simd overloads into the paper.
POLL: Forward P3642R1 to LEWG with the expectation that the next revision includes
overloads.std :: simd
SF F N A SA 7 4 0 0 0
The following changes were made:
- Provide (non-widening) §5.3. SIMD support
- Use two-space indentation, and generally match the code style of the C++ standard
- Provide detailed design description for §5.2. Widening operation and
std::wide_result - Make §6. Proposed wording and design independent of [P3161R4]
- Fix stray
type name in §6. Proposed wording, and improve wording generallyU - Rebase §6. Proposed wording on
naming changes in [P3691R1]std :: simd
1.7. Changes since R0
- Generate the proposal using COWEL instead of bikeshed
- Fix incorrect formula in §6. Proposed wording for bits ≥ the integer width
- Fix §4.2. Hardware support missing new VPCLMULQDQ instructions
- Fix improper uses of
in §2. Introductionstd :: unsigned_integral - Make slight editorial wording adjustments
- Rebase on N5008 and [P3161R4]
- Mention [SimdJsonClmul] in §3. Motivation
2. Introduction
Carry-less multiplication
is a simple numerical operation on unsigned integers.
It can be a seen as a regular multiplication where
is being used as a reduction instead of .
It is also known as "XOR multiplication" and "polynomial multiplication". The latter name is used because mathematically, it is equivalent to performing a multiplication of two polynomials in GF(2), where each bit is a coefficient.
I propose a function to perform this operation:
I also propose a widening operation in the style of [P3161R4], as follows:
3. Motivation
Carry-less multiplication is an important operation in a number of use cases:
-
CRC Computation: While cyclic redundancy checks can theoretically be performed with a finite
field of any length, in practice,
GF(2)[X],
the polynomial ring over the Galois field with two elements is used.
Polynomial addition in this ring can be implemented via
, and multiplication viaxor , which makes cyclic redundancy checks considerably faster.clmul -
Cryptography:
may be used to implement AES-GCM. [IntelClmul] describes this process in great detail and motivates hardware support for carry-less multiplication via theclmul instruction.pclmulqdq -
Bit manipulation:
performs a large amount ofclmul and<< operations in parallel. This is utilized in the reference implementation [BitPermutations] ofxor , proposed in [P3104R3]. For example, the formstd :: bit_compressr computes the bitwise inclusive parity for each bit ofclmul ( x , - 1 u ) and the bits to its right.x
Carry-less multiplication is of such great utility that there is widespread hardware support, some dating back more than a decade. See below for motivating examples.
3.1. Parity computation and JSON parsing
The parity of an integer is if the number of one-bits is even,
and if it is odd.
The parity can also be computed with .
computes the parity of each bit in
and the bits to its right.
The most significant bit holds the parity of as a whole.
While the parity of all bits can be obtained with ,
it computes the inclusive cumulative parity,
which can be used to accelerate parsing JSON and other file formats ([SimdJsonClmul]).
This can be done by mapping each character onto a -bit,
and any other character onto .
would then produce masks where string characters
corresponds to a -bit.
abc xxx" foobar " zzz" a " // input string 000000001 0000001 000001 01 // quote_mask 00000000. 111111 . 00000. 1 . // clmul(quote_mask, -1), ignoring 1-bits of quote_mask
3.2. Fast space-filling curves
The special form can be used to accelerate the computation of Hilbert curves.
To properly understand this example, I will explain the basic notion of space-filling curves.
We can fill space using a 2D curve by mapping the index on the curve
onto Cartesian coordinates and .
A naive curve that fills a 4x4 square can be computed as follows:
When mapping the index onto the returned 2D coordinates,
we obtain the following pattern:
0 1 2 3 4 5 6 7 8 9 a b c d e f
The problem with such a naive curve is that adjacent indices can be positioned very far apart (the distance increases with row length). For image processing, if we store pixels in this pattern, cache locality is bad; two adjacent pixels can be very far apart in memory.
A Hilbert curve
is a family of space-filling curves where the distance between two adjacent
elements is :
0 1 e f 3 2 d c 4 7 8 b 5 6 9 a
De-interleaving bits of into and
yields a Z-order curve,
and performing further transformations yields a
Hilbert curve.
can be used to compute the bitwise parity for each bit and the bits to its right,
which is helpful for computing Hilbert curves.
Note that the following example uses the function from [P3104R3],
which may also be accelerated using .
This specific example is taken from [FastHilbertCurves]. [HackersDelight] explains the basis behind this computation of Hilbert curves using bitwise operations.
When working with space-filling curves, the inverse operation is also common:
mapping the Cartesian coordinates onto an index on the curve.
In the case of Z-order curves aka. Morton curves,
this can be done by simply interleaving the bits of and .
A Z-order curve is laid out as follows:
0 1 4 5 2 3 6 7 8 9 c d a b e f
can be used to implement bit-interleaving in order to generate a
Z-order curves.
is equivalent to
[P3104R3]'s .
4. Possible implementation
A naive and unconstrained implementation looks as follows:
is constant and
each bitwise AND and multiplication can be done in parallel.
Lastly, all results have to be accumulated using a horizontal XOR.
Expressed in terms,
this looks something like:
Such a naive implementation is far from optimal though.
[QuickBench] shows that a naive implementation
which computes both the high and the low bits
performs 9.2× worse than an efficient implementation
taken from [NTL].
4.1. Intrinsic implementation
Since Clang 23,
LLVM also provides a portable intrinsic.
Thus, the operation can be lowered like:
→ → @llvm.clmul → .
While a pure library implementation of is possible,
it misses out on many optimization opportunities.
-
is commutative, pure, re-entrant, non-throwing, does not affect the floating-point environment, etc. Not all of these properties can be easily communicated for a library function.std :: clmul -
Carry-less multiplication with
or0 can be simplified irrespective of the other factor.1 - If one factor has a large number of trailing zeroes, the operation can be simplified by shifting that factor to the right, performing the multiplication, and shifting back to the left. This may avoid multiprecision operations; that is, it can turn a 128-bit multiplication into a 64-bit multiplication.
- If one factor is known to be a power of two, even if the exact value isn't known, normal multiplication can be used, which may be faster.
-
If one factor is a mask (that is, a sequence of contiguous one-bits),
carry-less multiplication is equivalent to a
parallel prefix XOR
orbitwise parity
operation, which is simpler than the usual software implementation. -
is equivalent to interleaving bits ofstd :: clmul ( x , x ) with zeroes. This pattern of squaring can be recognized (even if the value ofx is unknown) and transformed intox if thestd :: bit_expand ( x , 0x 5555 ' 5555 u ) PDEP instruction is available in hardware. On x86_64, this may be beneficial because carry-less multiplication is a vector instruction andPDEP may have less overhead.
4.2. Hardware support
The implementation difficulty lies mostly in utilizing available hardware instructions, not in the naive fallback implementation.
In the following table, let
| Operation | x86_64 | ARM | RV64 |
|---|---|---|---|
may look as follows:
5. Design considerations
Multiple design choices lean on [P0543R3] and [P3161R4]. Specifically,
- the choice of header
,<numeric> - the choice to have a widening operation,
- the
naming scheme,_wide - the
template, andwide_result - the decision to have a
parameter list.( T , T )
5.1. Naming
Carry-less multiplication is also commonly called "Galois Field Multiplication" or "Polynomial Multiplication".
The name was chosen because it carries no domain-specific connotation,
and because it is widespread:
-
Intel refers to
As "Carry-Less Multiplication Quadword" in its manual; see [IntelManual].PCLMULQDQ -
RISC-V refers to
as carry-less multiplication, and this is obvious from the mnemonic.clmul - The Wikipedia article ([WikipediaClmul]) for this operation is titled "Carry-less product".
-
The portable LLVM intrinsic function ([LLVMClmul]) is named
@llvm.clmul .
5.2. Widening operation and std::wide_result
In addition to the function template,
there exists a function template:
Such a widening function is important in a various cryptographic use cases. There is universal §4.2. Hardware support for obtaining all 128 bits of a multiplication for that reason.
Most of the design choices take the design of [P3161R4] and [P4052R0] into consideration:
-
The function is named
to be symmetrical withwidening_clmul from [P4052R0] (merged into C++26). That proposal broadly advocated for a naming scheme leaning on Rust, and the Rust standard library has a nightly-onlystd :: saturating_mul widening_mul function. Rust also has a nightly-onlywidening_carryless_mul function, but that name seems comically long and is being contested on the tracking issue. -
The result type is deliberately not named
so that futurewide_clmul_result and other operations can use the same result type, which avoids creating an ever-growing set of equivalent (but distinct) types.widening_mul -
[P3161R4] orders the low bits before the high bits
to cater to little-endian architectures.
That choice is not enforced in this paper
(
andhigh-bits may be reordered), but made possible for implementations (see §5.2.1. Aggregate typelow-bits std::wide_result ).
However, the comparison operators are a novel invention of this proposal.
They are intended to behave as if the comparisons were performed on an
integer with twice the width of .
These comparisons exists so that the result can be easily compared
against expected results in test cases,
stored in containers like ,
used out of the box with , etc.
There is an obvious and mathematically meaningful ordering of s,
so it would be strange not to add a comparison operator.
For now, is constrained to be an unsigned integer type.
If the aforementioned operation were added to the standard,
those constraints could be relaxed, but for now,
only unsigned integer types are needed since has that constraint.
inputs.
5.2.1. Aggregate type std::wide_result
Earlier revisions of this paper made an aggregate type,
largely because of the simplicity in specification and usability this provides.
LEWG reviewed this issue extensively during 2026 telecons,
and the decision to use a non-aggregate type was made for a number of reasons:
- With an aggregate type, the order of members is fixed, so some having a layout that is optimal for the native endianness is not always possible.
-
An aggregate type permits structured bindings,
which may be bug-prone because the order of members may not match the user's
expected order.auto [ high , low ] - To optimize for more popular little-endian architectures, the low bits would ideally come first, which conflicts with how we write integer literals (most significant digit first).
5.2.2. Alignment of std :: wide_result < std :: uint64_t >
It may be reasonable to over-align .
For example, could be over-aligned to 64 bits,
and could be over-aligned to 128 bits,
rather than having the natural alignment
of a
containing two objects of type .
This decision should be left up to implementers, and is a freedom generally available. Whether the decision to over-align makes sense depends on the target architecture, among other factors.
is aligned to 128 bits,
while is aligned to 64 bits in typical ABIs.
5.2.3. Integer interoperability
While is strongly implied
to have the same layout as ,
and while this is desired,
it's not actually guaranteed.
We could guarantee that
(though phrased generally, not just for bit-precise integers).
However, this would arguably over-specify the layout.
A conversion and integers could just as well be provided
as a .
Such a conversion operator can always be added in a future paper.
5.2.4. Naming of low() and high()
The names and for
either non-static data members or for member functions,
but this was seen as problematic during LEWG review,
in part because the C++ standard doesn't use the bits
terminology generally
in respect to integers.
Core wording talks about coefficients in the base-2 representation of integers.
Furthermore, the part provides little additional clarity.
should be sufficiently clear.
5.3. SIMD support
Upon seeing this proposal at Sofia 2025, SG6 recommended to add SIMD support. This recommendation was provided under the assumption that it would be a simple addition in the style of [P2933R4]. Therefore, this proposal provides non-widening SIMD carry-less multiplication with the following signature:
5.3.1. SIMD widening operations are out of scope
AVX-512 provides a
It would take considerable design and wording effort to standardize this,
especially if one wants to expose the full
In conclusion, a proposal for widening SIMD operations in general would be well-motivated.
For , designing SIMD widening operations would be scope creep.
6. Proposed wording
The proposed changes are relative to [N5054].
[version.syn]
Add feature test macros to [version.syn] paragraph 2 as follows:
[numeric.ops]
Add the following declarations to the synopsis in [numeric.ops.overview], immediately following the declarations associated with [numeric.sat]:
In subclause [numeric.ops], append a subclause immediately following [numeric.sat]:
Carry-less product [numeric.clmul]
1
A specialization is ill-formed
unless is an unsigned integer type ([basic.fundamental]).
2
A specialization is a standard-layout,
trivially copyable class type.
The members and
of any specialization of may be reordered.
3
Returns:
.
4 Let:
- be a reduction using the exclusive OR operation ([expr.xor]);
- for an integer , be the least significant bit in the base-2 representation of ;
- be the width of
.T
5
Constraints:
is an unsigned integer type ([basic.fundamental]).
6
Returns:
A object storing the bits of an integer
of width ,
where the value of
is given by Formula , and
is .
The result object is initialized so that
stores the least significant bits of , andlow-bits stores the subsequent bits of .high-bits
7
Constraints:
is an unsigned integer type ([basic.fundamental]).
8
Returns:
.
structural was also considered,
but not polled in LEWG.
The use case of in template parameters
is not deliberately supported.
See [bit.permute] for precedent for the
least significant bit
in the base-2 representation
idiom.
It has been pointed out that bit
and bits
are not really defined in the C++ standard,
and the term coefficient
may be better,
but this should be fixed consistently throughout the standard, if at all.
See [sf.cmath.riemann.zeta] for precedent on wording which includes formulae.
The formula above in TeX notation is:
[simd]
Add the following declarations to the synopsis in [simd.syn]:
In subclause [simd], append a subclause immediately preceding [simd.math]:
Carry-less product [simd.clmul]
1
Constraints:
The type is an unsigned integer type ([basic.fundamental]).
2
Returns:
A object where the element is initialized
to the result of
([numeric.clmul])
for all in the range [, ).