Skip to content

Commit ca1e205

Browse files
committed
popcnt: expand comments in popcnt_avx2_amd64.s
Document the AVX2 popcount assembly more thoroughly. Comments only -- no instruction was changed; the build, go vet's asm checker, and the differential tests all still pass. - Header split into an algorithm overview (the Mula/Lemire VPSHUFB nibble-lookup popcount) and a Go-assembler conventions section (operand order, Yn/Xn registers, FP argument layout, NOSPLIT/$0 leaf frames, and why VMOVDQU and VZEROUPPER are used). - lutmask: decode the constant blob -- how the bytes map to nibble popcounts, why the 16-entry table is duplicated per 128-bit lane, and what the 0x0F mask is for. - COUNTBLOCK: line-by-line walkthrough of the eight instructions plus the no-overflow reasoning; SETUP and HSUM expanded similarly. - _popcntSliceAVX2 fully annotated as the canonical template; the And/Or/Xor/Mask variants note they mirror it and differ only in the combine op (including the VPANDN operand-order detail) and explain the +48 return slot. - _hasAVX2: explain why each CPUID/XGETBV check matters.
1 parent 9381251 commit ca1e205

1 file changed

Lines changed: 112 additions & 47 deletions

File tree

popcnt_avx2_amd64.s

Lines changed: 112 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -4,28 +4,61 @@
44

55
#include "textflag.h"
66

7-
// AVX2 population-count routines using the Mula/Lemire VPSHUFB nibble-lookup
8-
// kernel. Each 256-bit vector holds 4 uint64; the popcount of each byte is
9-
// looked up via VPSHUFB over a 16-entry nibble table (replicated in both
10-
// 128-bit lanes), the two nibble counts are added byte-wise, then VPSADBW sums
11-
// each group of 8 bytes into a quadword that is accumulated. A scalar POPCNTQ
12-
// tail handles the remaining (len % 4) words, so the routines are correct for
13-
// any length. Loads/stores are unaligned (VMOVDQU) because the bitmap slices
14-
// are only 8-byte aligned. VZEROUPPER precedes every return to avoid the
15-
// AVX<->SSE transition penalty.
7+
// AVX2 population-count routines for amd64. They count the set bits across a
8+
// []uint64 (the backing storage of a bitmap container), optionally combining
9+
// each pair of words with a boolean op first: And, Or, Xor, and Mask (s &^ m).
10+
//
11+
// Algorithm (Mula/Lemire VPSHUFB nibble lookup)
12+
// ---------------------------------------------
13+
// AVX2 has no single "popcount a whole vector" instruction, so each byte's
14+
// popcount is taken from a 16-entry lookup table indexed by a 4-bit nibble:
15+
// a byte is split into its low and high nibble, each nibble is looked up (one
16+
// VPSHUFB performs all 32 lookups in a 256-bit register at once), the two
17+
// results are added to give a per-byte popcount, and VPSADBW then sums each
18+
// group of 8 byte-counts into a 64-bit lane total that is accumulated. After
19+
// the loop the four lane totals are summed (HSUM) into a scalar register.
20+
// Each iteration handles 256 bits (4 uint64); a scalar POPCNTQ tail handles
21+
// the trailing len%4 words, so any slice length is counted correctly.
22+
//
23+
// Go assembler conventions used below
24+
// -----------------------------------
25+
// - Operands are written source(s) first, destination LAST. So
26+
// "VPAND Ymask, Ydata, Ylo" means Ylo = Ydata AND Ymask.
27+
// - Yn are the 256-bit AVX registers; Xn aliases the low 128 bits of Yn.
28+
// - Arguments/results are read from the frame pointer (FP). A Go slice is a
29+
// 3-word header {ptr,len,cap}: s_base+0(FP), s_len+8(FP); a second slice
30+
// argument starts at +24(FP). The uint64 result slot follows the args
31+
// (e.g. ret+24(FP) for one slice arg, ret+48(FP) for two).
32+
// - Every routine is a leaf (makes no calls): NOSPLIT with a $0 local frame.
33+
// - Loads/stores use VMOVDQU (unaligned): container slices are only 8-byte
34+
// aligned, not 32. VZEROUPPER precedes every RET to avoid the AVX<->SSE
35+
// transition penalty in any non-VEX SSE code that runs afterwards.
1636

17-
// 16-entry nibble popcount table, replicated across both 128-bit lanes.
37+
// lutmask is a 64-byte read-only blob holding two constants used by every
38+
// routine:
39+
// bytes 0..31 - the nibble popcount table, i.e. table[i] = number of set
40+
// bits in the 4-bit value i. VPSHUFB indexes within each
41+
// 128-bit lane independently, so the 16-entry table is stored
42+
// twice (once per lane). Read low-byte-first, the first qword
43+
// 0x0302020102010100 is the bytes {0,1,1,2,1,2,2,3} for
44+
// nibbles 0..7, and 0x0403030203020201 is {1,2,2,3,2,3,3,4}
45+
// for nibbles 8..15.
46+
// bytes 32..63 - 0x0F in every byte: a mask that isolates the low nibble of
47+
// each byte.
48+
// RODATA|NOPTR marks it read-only and pointer-free (so the GC ignores it).
1849
DATA lutmask<>+0(SB)/8, $0x0302020102010100
1950
DATA lutmask<>+8(SB)/8, $0x0403030203020201
2051
DATA lutmask<>+16(SB)/8, $0x0302020102010100
2152
DATA lutmask<>+24(SB)/8, $0x0403030203020201
22-
// low-nibble mask, 0x0F in every byte.
2353
DATA lutmask<>+32(SB)/8, $0x0f0f0f0f0f0f0f0f
2454
DATA lutmask<>+40(SB)/8, $0x0f0f0f0f0f0f0f0f
2555
DATA lutmask<>+48(SB)/8, $0x0f0f0f0f0f0f0f0f
2656
DATA lutmask<>+56(SB)/8, $0x0f0f0f0f0f0f0f0f
2757
GLOBL lutmask<>(SB), RODATA|NOPTR, $64
2858

59+
// Register aliases. Ylut/Ymask/Yzero are constants set up once per call (see
60+
// SETUP); Yacc is the running accumulator of lane totals; Ydata/Yb hold the
61+
// current input vector(s); Ylo/Yhi/Yc1/Yc2 are scratch used by COUNTBLOCK.
2962
#define Ylut Y0
3063
#define Ymask Y1
3164
#define Yzero Y2
@@ -37,8 +70,18 @@ GLOBL lutmask<>(SB), RODATA|NOPTR, $64
3770
#define Yc1 Y8
3871
#define Yc2 Y9
3972

40-
// COUNTBLOCK adds the per-byte popcount of Ydata into the quadword accumulator
41-
// Yacc, using the lookup table Ylut, the nibble mask Ymask and Yzero.
73+
// COUNTBLOCK folds the popcount of the 32 bytes currently in Ydata into the
74+
// accumulator Yacc. Line by line:
75+
// VPAND Ymask,Ydata,Ylo : Ylo = low nibble of every byte
76+
// VPSRLW $4,Ydata,Yhi : shift each 16-bit lane right by 4...
77+
// VPAND Ymask,Yhi,Yhi : ...then mask, leaving the high nibble of each byte
78+
// VPSHUFB Ylo,Ylut,Yc1 : Yc1[b] = popcount(low nibble of byte b)
79+
// VPSHUFB Yhi,Ylut,Yc2 : Yc2[b] = popcount(high nibble of byte b)
80+
// VPADDB Yc2,Yc1,Yc1 : Yc1[b] = popcount(byte b) (0..8 each)
81+
// VPSADBW Yzero,Yc1,Yc1 : sum each group of 8 bytes -> 4 lane totals (0..512)
82+
// VPADDQ Yc1,Yacc,Yacc : add the 4 lane totals into the accumulator
83+
// Per-byte counts max at 8 and lane totals at 512, so accumulating across the
84+
// whole loop never overflows the 64-bit lanes.
4285
#define COUNTBLOCK \
4386
VPAND Ymask, Ydata, Ylo \
4487
VPSRLW $4, Ydata, Yhi \
@@ -49,14 +92,18 @@ GLOBL lutmask<>(SB), RODATA|NOPTR, $64
4992
VPSADBW Yzero, Yc1, Yc1 \
5093
VPADDQ Yc1, Yacc, Yacc
5194

52-
// SETUP loads the constant table/mask and zeroes the accumulator registers.
95+
// SETUP loads the lookup table and nibble mask and zeroes Yzero (the VPSADBW
96+
// addend) and Yacc (the accumulator). Run once at the top of each routine.
5397
#define SETUP \
5498
VMOVDQU lutmask<>+0(SB), Ylut \
5599
VMOVDQU lutmask<>+32(SB), Ymask \
56100
VPXOR Yzero, Yzero, Yzero \
57101
VPXOR Yacc, Yacc, Yacc
58102

59-
// HSUM horizontally sums the quadwords of Yacc into AX.
103+
// HSUM reduces Yacc's four 64-bit lane totals to a single sum in AX. X3 is the
104+
// low 128 bits of Yacc (Y3); VEXTRACTI128 pulls the high 128 bits into X5, the
105+
// two halves are added (giving two qwords in X3), and those two qwords are then
106+
// added into AX.
60107
#define HSUM \
61108
VEXTRACTI128 $1, Yacc, X5 \
62109
VPADDQ X5, X3, X3 \
@@ -66,43 +113,49 @@ GLOBL lutmask<>(SB), RODATA|NOPTR, $64
66113
ADDQ DX, AX
67114

68115
// func _popcntSliceAVX2(s []uint64) uint64
116+
// Returns the total number of set bits in s. This is the canonical routine;
117+
// the And/Or/Xor/Mask variants below share its structure and differ only by
118+
// the boolean op applied before counting.
69119
TEXT ·_popcntSliceAVX2(SB), NOSPLIT, $0-32
70-
MOVQ s_base+0(FP), SI
71-
MOVQ s_len+8(FP), CX
72-
XORQ AX, AX
73-
SETUP
120+
MOVQ s_base+0(FP), SI // SI = &s[0]
121+
MOVQ s_len+8(FP), CX // CX = len(s), in 64-bit words
122+
XORQ AX, AX // AX = running result
123+
SETUP // load table/mask; zero Yzero and Yacc
74124
MOVQ CX, R8
75-
SHRQ $2, R8
125+
SHRQ $2, R8 // R8 = len/4 = number of full 256-bit blocks
76126
TESTQ R8, R8
77-
JZ slicetail
127+
JZ slicetail // fewer than 4 words: skip the vector loop
78128
sliceloop:
79-
VMOVDQU (SI), Ydata
80-
COUNTBLOCK
81-
ADDQ $32, SI
129+
VMOVDQU (SI), Ydata // load 4 words (32 bytes)
130+
COUNTBLOCK // Yacc += popcount(those 32 bytes)
131+
ADDQ $32, SI // advance to the next block
82132
DECQ R8
83133
JNZ sliceloop
84-
HSUM
134+
HSUM // AX += sum of Yacc's lane totals
85135
slicetail:
86-
ANDQ $3, CX
136+
ANDQ $3, CX // CX = len % 4 = leftover words (0..3)
87137
TESTQ CX, CX
88138
JZ slicedone
89139
slicetailloop:
90140
MOVQ (SI), DX
91-
POPCNTQ DX, DX
141+
POPCNTQ DX, DX // scalar popcount of one word
92142
ADDQ DX, AX
93143
ADDQ $8, SI
94144
DECQ CX
95145
JNZ slicetailloop
96146
slicedone:
97-
VZEROUPPER
98-
MOVQ AX, ret+24(FP)
147+
VZEROUPPER // clear upper YMM state before returning
148+
MOVQ AX, ret+24(FP) // return AX
99149
RET
100150

101151
// func _popcntAndSliceAVX2(s, m []uint64) uint64
152+
// Returns the sum of popcount(s[i] & m[i]). Mirrors _popcntSliceAVX2 but loads
153+
// a vector from each of s and m and ANDs them before counting. s and m are
154+
// assumed to have equal length.
102155
TEXT ·_popcntAndSliceAVX2(SB), NOSPLIT, $0-56
103-
MOVQ s_base+0(FP), SI
104-
MOVQ m_base+24(FP), DI
105-
MOVQ s_len+8(FP), CX
156+
MOVQ s_base+0(FP), SI // SI = &s[0]
157+
MOVQ m_base+24(FP), DI // DI = &m[0]
158+
MOVQ s_len+8(FP), CX // CX = len
106159
XORQ AX, AX
107160
SETUP
108161
MOVQ CX, R8
@@ -112,7 +165,7 @@ TEXT ·_popcntAndSliceAVX2(SB), NOSPLIT, $0-56
112165
andloop:
113166
VMOVDQU (SI), Ydata
114167
VMOVDQU (DI), Yb
115-
VPAND Yb, Ydata, Ydata
168+
VPAND Yb, Ydata, Ydata // Ydata = s & m
116169
COUNTBLOCK
117170
ADDQ $32, SI
118171
ADDQ $32, DI
@@ -125,7 +178,7 @@ andtail:
125178
JZ anddone
126179
andtailloop:
127180
MOVQ (SI), DX
128-
ANDQ (DI), DX
181+
ANDQ (DI), DX // s & m, one word
129182
POPCNTQ DX, DX
130183
ADDQ DX, AX
131184
ADDQ $8, SI
@@ -134,10 +187,12 @@ andtailloop:
134187
JNZ andtailloop
135188
anddone:
136189
VZEROUPPER
137-
MOVQ AX, ret+48(FP)
190+
MOVQ AX, ret+48(FP) // +48: result follows two 24-byte slice headers
138191
RET
139192

140193
// func _popcntOrSliceAVX2(s, m []uint64) uint64
194+
// Returns the sum of popcount(s[i] | m[i]); see _popcntAndSliceAVX2 for the
195+
// shared structure.
141196
TEXT ·_popcntOrSliceAVX2(SB), NOSPLIT, $0-56
142197
MOVQ s_base+0(FP), SI
143198
MOVQ m_base+24(FP), DI
@@ -151,7 +206,7 @@ TEXT ·_popcntOrSliceAVX2(SB), NOSPLIT, $0-56
151206
orloop:
152207
VMOVDQU (SI), Ydata
153208
VMOVDQU (DI), Yb
154-
VPOR Yb, Ydata, Ydata
209+
VPOR Yb, Ydata, Ydata // Ydata = s | m
155210
COUNTBLOCK
156211
ADDQ $32, SI
157212
ADDQ $32, DI
@@ -164,7 +219,7 @@ ortail:
164219
JZ ordone
165220
ortailloop:
166221
MOVQ (SI), DX
167-
ORQ (DI), DX
222+
ORQ (DI), DX // s | m, one word
168223
POPCNTQ DX, DX
169224
ADDQ DX, AX
170225
ADDQ $8, SI
@@ -177,6 +232,8 @@ ordone:
177232
RET
178233

179234
// func _popcntXorSliceAVX2(s, m []uint64) uint64
235+
// Returns the sum of popcount(s[i] ^ m[i]); see _popcntAndSliceAVX2 for the
236+
// shared structure.
180237
TEXT ·_popcntXorSliceAVX2(SB), NOSPLIT, $0-56
181238
MOVQ s_base+0(FP), SI
182239
MOVQ m_base+24(FP), DI
@@ -190,7 +247,7 @@ TEXT ·_popcntXorSliceAVX2(SB), NOSPLIT, $0-56
190247
xorloop:
191248
VMOVDQU (SI), Ydata
192249
VMOVDQU (DI), Yb
193-
VPXOR Yb, Ydata, Ydata
250+
VPXOR Yb, Ydata, Ydata // Ydata = s ^ m
194251
COUNTBLOCK
195252
ADDQ $32, SI
196253
ADDQ $32, DI
@@ -203,7 +260,7 @@ xortail:
203260
JZ xordone
204261
xortailloop:
205262
MOVQ (SI), DX
206-
XORQ (DI), DX
263+
XORQ (DI), DX // s ^ m, one word
207264
POPCNTQ DX, DX
208265
ADDQ DX, AX
209266
ADDQ $8, SI
@@ -216,7 +273,9 @@ xordone:
216273
RET
217274

218275
// func _popcntMaskSliceAVX2(s, m []uint64) uint64
219-
// Computes sum of popcount(s[i] &^ m[i]) == popcount(s & ~m).
276+
// Returns the sum of popcount(s[i] &^ m[i]) == popcount(s & ~m). Same structure
277+
// as _popcntAndSliceAVX2; the combine is VPANDN, which computes (NOT first) AND
278+
// second, i.e. VPANDN Ydata, Yb, Ydata -> Ydata = (NOT Yb) AND Ydata = s &^ m.
220279
TEXT ·_popcntMaskSliceAVX2(SB), NOSPLIT, $0-56
221280
MOVQ s_base+0(FP), SI
222281
MOVQ m_base+24(FP), DI
@@ -230,7 +289,7 @@ TEXT ·_popcntMaskSliceAVX2(SB), NOSPLIT, $0-56
230289
maskloop:
231290
VMOVDQU (SI), Ydata
232291
VMOVDQU (DI), Yb
233-
VPANDN Ydata, Yb, Ydata
292+
VPANDN Ydata, Yb, Ydata // Ydata = s &^ m (= (NOT m) AND s)
234293
COUNTBLOCK
235294
ADDQ $32, SI
236295
ADDQ $32, DI
@@ -243,9 +302,9 @@ masktail:
243302
JZ maskdone
244303
masktailloop:
245304
MOVQ (DI), R10
246-
NOTQ R10
305+
NOTQ R10 // ~m
247306
MOVQ (SI), DX
248-
ANDQ R10, DX
307+
ANDQ R10, DX // s &^ m = s & ~m, one word
249308
POPCNTQ DX, DX
250309
ADDQ DX, AX
251310
ADDQ $8, SI
@@ -258,22 +317,28 @@ maskdone:
258317
RET
259318

260319
// func _hasAVX2() bool
261-
// Reports whether the CPU supports AVX2 and the OS has enabled YMM state.
320+
// Reports whether the CPU supports AVX2 and the OS has enabled the wide (YMM)
321+
// register state. All three checks must pass; otherwise the Go wrappers fall
322+
// back to the scalar implementation. Note CPUID clobbers AX/BX/CX/DX.
262323
TEXT ·_hasAVX2(SB), NOSPLIT, $0-1
263-
// CPUID leaf 1: require OSXSAVE (ECX bit 27) and AVX (ECX bit 28).
324+
// CPUID leaf 1: require OSXSAVE (ECX bit 27) and AVX (ECX bit 28). Both must
325+
// be set, so mask and compare against the combined bit pattern.
264326
MOVL $1, AX
265327
XORL CX, CX
266328
CPUID
267329
ANDL $0x18000000, CX
268330
CMPL CX, $0x18000000
269331
JNE noavx2
270-
// XGETBV(0): require XCR0 bits 1 (SSE) and 2 (AVX/YMM) set.
332+
// XGETBV(0): the OS must have enabled saving of SSE and AVX/YMM state, i.e.
333+
// XCR0 bits 1 and 2. Without this the YMM registers would be corrupted
334+
// across a context switch even though the CPU supports the instructions.
271335
XORL CX, CX
272336
XGETBV
273337
ANDL $0x6, AX
274338
CMPL AX, $0x6
275339
JNE noavx2
276-
// CPUID leaf 7, sub-leaf 0: require AVX2 (EBX bit 5).
340+
// CPUID leaf 7, sub-leaf 0: require AVX2 itself (EBX bit 5). The sub-leaf is
341+
// selected via ECX, which must be 0.
277342
MOVL $7, AX
278343
XORL CX, CX
279344
CPUID

0 commit comments

Comments
 (0)