Prefer to read without ads? Become a member — from $10/month — and support the work. Already a member? Log in to read ad-free on this device.

8.2 Integer Support

The SMs have the full complement of 32-bit integer operations:

CUDA exposes most of this functionality through standard C operators. Nonstandard operations, such as 24-bit multiplication, are exposed through inline PTX assembly or intrinsic functions.

8.2.1 Addition, Subtraction, and Addressing

CUDA GPUs are 32-bit machines. The register file is a bank of 32-bit registers, and the core integer and floating-point instructions take 32-bit operands. A virtue of this design is that the registers are fungible: since Fermi made CUDA hardware a load/store architecture, a given register can hold an address, a signed or unsigned integer, or a floating-point value with equal facility, and the same execution units serve them all. Values wider than 32 bits – double, 64-bit integers, and 64-bit addresses – occupy even-numbered register pairs.

Addition and subtraction are the simplest integer operations. The SASS IADD instruction adds two 32-bit registers, and subtraction is the same operation with one operand negated (as noted in the list above). Maxwell added IADD3, a three-input add that folds a common addressing pattern – a base plus two offsets – into a single instruction. Arithmetic wider than 32 bits is synthesized from 32-bit pieces: a 64-bit add becomes a 32-bit add of the low halves that produces a carry, followed by an add-with-carry of the high halves. (In PTX these are add.cc and addc; the hardware propagates the carry flag between them.)

The tension between a 32-bit machine and a 64-bit address space is most visible when examining the machine code for addressing. Beginning with Fermi, the global address space is 64 bits wide, but the hardware that computes addresses is 32-bit, so every pointer occupies a register pair and every nontrivial address calculation must form the low and high halves separately – frequently several instructions where a CPU would emit one.

8.2.2 Multiplication

Every CUDA-capable GPU has a native 32-bit integer multiplier and an integer multiply-add.

Unlike many CPU architectures, CUDA-capable GPUs have integer multiply-add instructions. The compiler is adept at identifying opportunities to use these instructions in lieu of standalone MUL instructions, when possible.

Integer multiplication does not always run at the same throughput as the other integer operations – on several architectures 32-bit multiply issues at a lower rate than 32-bit add – so multiply-heavy integer code can reward profiling, though it needs no special handling in source: the * operator yields the low 32 bits of the product, and the compiler selects the appropriate instruction. Two intrinsics return parts of a product that a plain multiply cannot express: __[u]mulhi() returns the most significant 32 bits of a 32×32-bit product, and __[u]mul64hi() returns the most significant 64 bits of a 64×64-bit product – both useful for fixed-point arithmetic and for building wider multiplies. Table 8-4 summarizes the multiplication intrinsics.

Intrinsic Description
__mul24(x, y) Least significant 32 bits of the product of the least significant 24 bits of two signed integers; the 8 most significant bits of each input are ignored.
__umul24(x, y) As __mul24(), for unsigned integers.
__mulhi(x, y) Most significant 32 bits of the product of two signed 32-bit integers.
__umulhi(x, y) Most significant 32 bits of the product of two unsigned 32-bit integers.
__mul64hi(x, y) Most significant 64 bits of the product of two signed 64-bit integers.
__umul64hi(x, y) Most significant 64 bits of the product of two unsigned 64-bit integers.

Table 8-4. Multiplication intrinsics

A historical note. The earliest hardware told a different story. Tesla-class GPUs (SM 1.x) had only a 24-bit integer multiplier, so a full 32-bit multiply had to be synthesized from four instructions; performance-sensitive code instead multiplied 24-bit operands through the __[u]mul24() intrinsic (Table 8-4), which returns the low 32 bits of the product of the low 24 bits of its inputs. Fermi (SM 2.0) added a native 32-bit multiplier and inverted the advice: on Fermi and every architecture since, __mul24() is emulated and slower than a native multiply8, so it survives only for backward compatibility.

8.2.3 Bit Manipulation

The CUDA compiler implements a number of intrinsics for bit manipulation, as summarized in Table 8-5; on later architectures, many of these intrinsics map to single instructions. When in doubt, disassemble and look at the microcode!

Intrinsic Summary Description
__brev(x) Bit reverse Reverses the order of bits in a word.
__byte_perm(x,y,s) Permute bytes Returns a 32-bit word whose bytes were selected from the two inputs according to the selector parameter s.
__clz(x) Count leading zeros Returns the number of zero bits (0-32) before the most significant set bit.
__ffs(x) Find first set bit Returns the position of the least significant set bit. The least significant bit is position 1. For an input of 0, __ffs() returns 0.
__popc(x) Population count Returns the number of set bits.
__fns(mask,base,offset) Find n-th set bit Searches mask from bit position base for the set bit selected by offset, whose sign chooses the search direction; returns the position (0-32), or 0xFFFFFFFF when none is found.
__[u]sad(x,y,z) Sum of absolute differences Adds abs(x - y) to z and returns the result.

64-bit variants have ‘ll’ (two ells for “long long”) appended to the intrinsic name: __clzll(), __ffsll(), __popcll(), __brevll().

Table 8-5. Bit Manipulation Intrinsics

bfind returns the index of the most significant set bit—the counterpart to __ffs, which returns the least—and has no dedicated intrinsic: it is reached through inline PTX, or computed as 31 - __clz(x) for a nonzero 32-bit input, with a .shiftamt form that returns the left shift needed to normalize the value rather than the raw bit position. __fns is most useful inside a warp: given the ballot mask of participating lanes, it maps a rank produced by a prefix sum back to its lane, the operation at the heart of stream compaction.

8.2.4 Funnel Shift (SM 3.5)

GK110 added a 64-bit "funnel shift" instruction that concatenates two 32-bit values together (the least significant and most significant halves are specified as separate 32-bit inputs, but the hardware operates on an aligned register pair), shifts the resulting 64-bit value left or right, then returns the most significant (for left shift) or least significant (for right shift) 32 bits.

Funnel shift may be accessed with the intrinsics given in Table 8-6. These intrinsics are implemented as inline device functions (using inline PTX assembler) in sm_35_intrinsics.h. By default, the least significant 5 bits of the shift count are masked off; the _lc and _rc intrinsics clamp the shift value to the range 0..32.

Applications for funnel shift include the following:

To right-shift data sizes greater than 64 bits, use repeated __funnelshift_r() calls, operating from the least-significant to the most-significant word. The most-significant word of the result is computed using operator>>, which shifts in zero or sign bits as appropriate for the integer type.

To left-shift data sizes greater than 64 bits, use repeated __funnelshift_l() calls, operating from the most-significant to the least-significant word. The least-significant word of the result is computed using operator<<.

If the hi and lo parameters are the same, the funnel shift effects a rotate operation.

Intrinsic Description
__funnelshift_l(hi, lo, sh) Concatenates [hi:lo] into a 64-bit quantity, shifts it left by (sh&31) bits, and returns the most significant 32 bits.
__funnelshift_lc(hi, lo, sh) Concatenates [hi:lo] into a 64-bit quantity, shifts it left by min(sh,32) bits, and returns the most significant 32 bits.
__funnelshift_r(hi, lo, sh) Concatenates [hi:lo] into a 64-bit quantity, shifts it right by (sh&31) bits, and returns the least significant 32 bits.
__funnelshift_rc(hi, lo, sh) Concatenates [hi:lo] into a 64-bit quantity, shifts it right by min(sh,32) bits, and returns the least significant 32 bits.

Table 8-6. Funnel Shift Intrinsics

8.2.5 Logic Operations

The bitwise operators &, |, ^, and ~ map to the SASS LOP instruction on early hardware. Beginning with Maxwell (SM 5.0), the hardware also provides LOP3, a three-input logic instruction that computes an arbitrary Boolean function of three operands in a single instruction – effectively a tiny lookup table, much like a lookup table (LUT) in an FPGA. The compiler emits it automatically to collapse chains of &/|/^/~ into one operation, so a kernel need never spell it out; but it turns up constantly in disassembly, and reading it requires understanding how the function is encoded.

The function is selected by an 8-bit immediate, immLut. This byte is the truth table: LOP3 computes, for inputs A, B, and C, a result whose value for each of the eight possible input combinations is the corresponding bit of immLut.

The trick to computing immLut for a desired function F(A, B, C) is to evaluate the function itself as a bitwise operation, substituting three specific constants for the inputs9:

A = 0xF0
B = 0xCC
C = 0xAA

For example, to compute F = (A | B) & ~C, evaluate the same expression over those constants:

immLut = (0xF0 | 0xCC) & (~0xAA);   // == 0x54

and pass the result as the immediate to the instruction, which in inline PTX reads:

asm( "lop3.b32 %0, %1, %2, %3, 0x54;"
     : "=r"(d) : "r"(a), "r"(b), "r"(c) );

The constants work because their bits, read as columns, enumerate all eight combinations of the three inputs:

A = 0xF0 = 1 1 1 1 0 0 0 0
B = 0xCC = 1 1 0 0 1 1 0 0
C = 0xAA = 1 0 1 0 1 0 1 0

Each bit position is one row of the truth table – reading the columns from the most significant bit, (A, B, C) runs 111, 110, 101, …, 000 – and evaluating the desired expression bitwise across all eight positions at once yields exactly the byte whose bits are F for each row.

The same picture, run in reverse, lets you infer the function from a given immLut: write the byte in binary and read off which input combinations produce a 1. The value 0x54 above is 01010100; the 1 bits sit at positions 6, 4, and 2, which the constants above identify as (A, B, C) = (1,1,0), (1,0,0), and (0,1,0) – precisely the cases where (A | B) is true and C is false.

8.2.6 Integer Dot Products

Pascal (SM 6.1) added two fused integer dot-product-and-accumulate instructions – SASS IDP – aimed squarely at the 8-bit inner loops of quantized deep-learning inference. They predate the Tensor Cores and coexist with them: where a Tensor Core multiplies whole matrices, these operate on a single 32-bit register’s worth of packed bytes, which suits code that is not shaped like a matrix multiply.

__dp4a(a, b, c) treats its two 32-bit inputs as four packed 8-bit integers each, forms the four products, sums them, and adds the 32-bit accumulator c – a four-element 8-bit dot product with 32-bit accumulation in one instruction:

int      __dp4a(int a,      int b,      int c);       // signed
unsigned __dp4a(unsigned a, unsigned b, unsigned c);  // unsigned

__dp2a(a, b, c) is the two-element mixed-width variant: two packed 16-bit values in a against two packed 8-bit values drawn from the low or high half of b, accumulated into c. The half is selected by the intrinsic name – __dp2a_lo() takes the low two bytes of b, __dp2a_hi() the high two – and both come in signed and unsigned forms.

These are close cousins of the SIMD video instructions of Section 8.6.4: all pack narrow integers into 32-bit registers and process every lane at once. What the dot-product instructions add is the reduction and the accumulate, fused into the same instruction. They are single instructions on SM 6.1 and later; on older architectures, the intrinsics still compile, but expand to multi-instruction sequences.

DP4A collapses four multiply-adds into one instruction, but that only helps a kernel whose bottleneck is instruction throughput in the first place. The normalized cross-correlation kernel of Chapter 15 is a cautionary example: dropping DP4A into its byte-reduction inner loop yields no speedup at all, because the kernel is memory-latency-bound at full occupancy and the cheaper arithmetic merely hides in the latency shadow. Only after each thread is given more independent work—two output columns with separate accumulator chains—does the kernel become arithmetic-bound and DP4A contribute roughly a further 2.5× on top of the 2.4× from the added parallelism (Section 15.5). The lesson generalizes: an instruction that lowers arithmetic cost speeds up a kernel in proportion to how arithmetic-bound the code already is, so profiling to find the real limiter comes first.

8.2.7 Dynamic-Programming Instructions (DPX)

Hopper (SM 9.0) introduced the DPX family for dynamic-programming algorithms – Smith-Waterman and Needleman-Wunsch (sequence alignment), Floyd-Warshall (all-pairs shortest paths), and similar recurrences whose inner loops are dominated by chains of min/max and add-then-min/max. Each such chain, several instructions on earlier hardware, collapses to one. The intrinsics come in three shapes:

Each is available in signed (_s32), unsigned (_u32), and packed-halfword (_s16x2/_u16x2) forms, the last operating on two 16-bit values at once. A _relu suffix additionally clamps the result at zero, folding the non-negativity step of local alignment into the same instruction: __vimax3_s16x2_relu(a, b, c) computes a per-halfword max(max(a, b, c), 0).

On Hopper and later, these map to single SASS instructions (VIMNMX, and VIADD/VIMNMX fused forms); the intrinsics are portable and compile for earlier architectures as well, but only Hopper and its successors execute them at full DPX rate, where NVIDIA reports roughly a 7x Smith-Waterman speedup over the previous-generation A100.


  1. Using __mul24() or __umul24() on SM 2.x and later hardware, however, is a performance penalty.↩︎

  2. This method for computing the immLut argument is documented by NVIDIA and explained, with the worked example above, by Robert Crovella on Stack Overflow.↩︎