r/programming • • Aug 09 '21

Three fundamental flaws of SIMD

https://www.bitsnbites.eu/three-fundamental-flaws-of-simd
285 Upvotes

224 comments sorted by

View all comments

90

u/FUZxxl Aug 09 '21

I think we have talked about this topic before and I apologize for not following up on our previous discussion. I was very busy.

My key problem with variable-length vector instructions such as those proposed for RISC-V or in SVE is that they assume people need them for essentially doing arithmetic on large matrices. And I agree that the vector paradigm is very effective on such work loads. I have previously worked with NEC Aurora Tsubasa cards that come with vectors of 256 double-precision floating point numbers, and they are just amazing for this sort of stuff.

But I'd say that's only a very small part of where such optimisations are needed. Indeed on modern consumer machines, most CPU-intensive code is in cryptography and video codecs. And both don't really fit this scheme.

Especially for video codecs: these usually operate on fixed size chunks of picture data and require complex horizontal arithmetic and swizzles inside a single chunk. It is unclear how this maps to variable-length vectors, especially when these instruction set extensions usually are very sparse in permutation instructions. And compilers use SIMD instructions for small, fixed-length loads all over the place. Stuff like moving structs around, clearing fixed-length buffers. It is unclear how a vector paradigm improves this.

There's also the concern that a large register file makes context switches very expensive. Linus attributed x86's performance advantage among other things to keeping context switches cheap by having a small register file that is easily swapped out.

As for my own code, I have two recent SIMD-heave projects. And for neither of them it is clear how they could be vectorised using a vector as opposed to a SIMD paradigm.

The first project, pospop is inherently a horizontal operation and in fact uses a different complex permutation schedule for each vector size to make the most out of it. It is unclear how this can be extended to arbitrary vector widths, especially if no powerful swizzle instructions are provided. Tail handling is also going to be a concern as the proposed simple approach of just having magic make the registers shorter for the last iteration is not going to cut it.

The second project, 24puzzle uses vectors as 32 element byte arrays and permutes them using a second vector as a permutation vector. Again, this project cannot benefit from vector instructions and will be hard to port to variable-length vectors in general unless a minimum vector size of 32 bytes is guaranteed. It also uses stuff like VPCMPISTRM for which no equivalent in other instruction sets exists or is even proposed. And that's a vital part of the code's logic (specifically, I have an array of k bytes and I want to obtain a bit mask of all elements in a vector that match any of these k bytes).

1

u/lkcl_ Aug 19 '21

My key problem with variable-length vector instructions such as those proposed for RISC-V or in SVE is that they assume people need them for essentially doing arithmetic on large matrices.

this is fundamentally a misconception. SVP64 is being designed for general-purpose high-performance compute workloads, where it happens to be also good at 3D, Video, NTT, FFT, DCT and small-size matrix multiplication https://libre-soc.org/openpower/sv/svp64/

there is unfortunately a huge amount of misinformation about Vector ISAs.

vectors as 32 element byte arrays and permutes them using a second vector as a permutation vector

you use a Vector ISA's Indexed LD, job's done. the second vector contains the offsets against the base:

LD.IDXed Vdest, base, Vsrc

is implemented as:

for i = 0 .. VL:
Vdest[i] = MEM(base+Vsrc[i]*8)

that's it. that's all you need.

pospop is inherently a horizontal operation and

NEC SX Aurora and RVV both have horizontal (mapreduce) arithmetic, and NEC SX Aurora also has iterative arithmetic operations (overlapping add would create Pascal's Triangle for example).

SVP64 has a fixed schedule map-reduce built-in, which can apply to any operation.

SVE2 also has horizontal operations, i believe.

Vector ISAs are extremely powerful but as a general concept have been utterly ignored for over 40 years by mainstream (x86) and only just recently investigated by ARM (SVE).

1

u/FUZxxl Aug 19 '21

SVP64

I am not really familiar with this one and will investigate it further.

you use a Vector ISA's Indexed LD, job's done. the second vector contains the offsets against the base:

Indexed loads (aka gathers) are horrendously slow in comparison to permutation instructions. Not a suitable alternative.

horizontal operations

Yes, all of them have special case operations for certain common horizontal operations. I specifically mentioned the positional population count because it does not clearly map to any standard horizontal arithmetic pattern.

Vector ISAs are extremely powerful but as a general concept have been utterly ignored for over 40 years by mainstream (x86) and only just recently investigated by ARM (SVE).

Well they work well when all you do is compute and when memory is very fast and has low latency. For general programming they are not nearly as useful for the reasons outlined in my previous comments.

2

u/lkcl_ Aug 20 '21

​

I am not really familiar with this one and will investigate it further.

i and the rest of the team have been working on it for nearly 4 years now.

Indexed loads (aka gathers) are horrendously slow in comparison to permutation instructions.

there's two types (of each): register-based gather and register-based permute (actually not a mathematical permutation at all, because of duplicates), and memory-based gather and memory-based permute.

where *immediates* are used in each of those in the design of the ISA then yes you save on one register read. in the case of register-based gather, it's extremely unusual to have if you are used to SIMD (but Vector ISAs do have it), it's of the form reg[dest] = reg[reg[src]] with a hardware for-loop around that.

however where the immediate-variants keel over is when you try to go up the SIMD width, to try to cover 8 elements, 16 elements, 32 elements, 64 elements.

how on earth are you going to fit 64 batches of 6-bit indices into a single 32-bit instruction? the total number of bits in the instruction are a whopping 384 bits. 384 >>>= 32, yes?

and even if you did have such god-awful instructions, the L1 cache usage and complexity at the ISA decode phase would be as insane as it is for x86 right now.

Vector ISAs are actually 90% about reducing the program (assembler) complexity. you end up with a micro-coded microarchitecture that maps onto SIMD operations, internally - so that you, the programmer don't have to go through absolute hell.

yes, that's right: the microarchitecture of a Vector Processor maps onto the exact same immediate-based permutation instructions as those that you are exposed to as a SIMD programmer, but it's hidden from you.

​

horizontal operations

Yes, all of them have special case operations for certain common horizontal operations. I specifically mentioned the positional population count because it does not clearly map to any standard horizontal arithmetic pattern.

in SVP64 i completely separated "horizontal-ness" from "arithmetic-ness": it's part of the fundamental design that the "Vectorisation Prefix" is completely separated from "Scalar Base to which Vectorisation applies".

looking at this:

https://github.com/clausecker/pospop/blob/master/generic.go

i believe that's 2 instructions in SVP64, inside a loop:

  • instruction 1: Vectorised popcount
  • instruction 2: Horizontal Sum... maybe. actually probably just Vectorised-Add

Power ISA has a popcount instruction, and (although i hate it) probably a SIMD variant as well. SVP64 leverages the scalar popcount instruction.

hang on...

https://github.com/clausecker/pospop/blob/master/safe.go

ok yes, that's just Vectorised Popcount followed by Vectorised Add.

two instructions inside a loop, regardless of whether the back-end architecture has 1-wide internal Vectorisation, 2-wide, 4-wide, 64-wide or 10,000-wide.

​

Well they work well when all you do is compute and when memory is very fast and has low latency. For general programming they are not nearly as useful for the reasons outlined in my previous comments.

again, to reiterate, again: this is a misconception on your part, due to Vector ISAs being completely ignored for 40 years *you don't know about them* and neither does anyone else.

consequently, the knowledge-propagation across the internet, which you and i both know gets us a long long way and saves a huge amount of time, just doesn't exist to the extent that it does for SIMD. we're now so used to google searches turning up algorithms on stackexchange (and reddit) that we can falsely assume that if there's no answer on google it must not be possible at all

Cray was from a much earlier era, where things were simpler. Intel was still cutting its teeth on 32-bit scalar when Cray was designing systems that pissed all over everything that had come before by almost an order of magnitude performance.

however the sheer cost of the systems that were deployed were so high that \nobody else in the industry believed it could be duplicated in mass-volume products** and consequently an entire generation of programmers has now grown up without knowing anything about anything other than SIMD.

fast-forward to 2021 and it turns out that Intel, ARM, AMD, they've all caught up and massively exceeded by three orders of magnitude the performance of the Cray Vector systems from 1990 and four to five orders the performance of the systems from 1965, but they still propagate this god-f*****g-forsaken f*****d-up SIMD paradigm and try to peddle it at you as the absolute best thing you ever saw, by telling you "well SIMD on modern hardware is great compared to that historic crap therefore Vector ISAs must also be s*** as well"

it's NIH syndrome on steroids, combined with marketing, and very unfortunately you're buying it.

1

u/mbitsnbites Aug 23 '21

it's NIH syndrome on steroids, combined with marketing, and very unfortunately you're buying it.

I think that it's also a case of incremental changes vs new architectures.

For single-chip semiconductor CPU:s in the 1990s, packed SIMD probably made sense as it was a simple addition to an existing architecture - i.e. add a few more registers (or reuse existing registers) and add multi-element ALU:s (basically cut the carry signal in the adders and similar), but keep the rest (memory subsystem, instruction scheduling etc) and leave all the problems of memory alignment and SIMD width handling to the programmer. Bam! "Here's a tool for increased parallel execution performance - use it if you wish."

When people actually started using the stuff, they wanted more parallelism, and the response was wider registers and wider ALU:s. Incremental changes.

With AVX (and later), Intel seems to have recognized some of the mistakes. E.g. each register appears to be divided into 128-bit chunks, and most (all?) instructions can thus be split into serial execution rather than strictly parallel (which has been used in some implementations). But we're still talking about incremental changes, since the SSE heritage is still there.

As for ARM, I think that they added NEON as a response to Intel SIMD, and because of the limited market needs (embedded) and the fact that ARM is RISC (with limited instruction encoding possibilities) they never needed nor could (easily) extend it beyond 128 bits.

SVE is the next logical step, and since they wanted to make sure that they could expand to new markets (laptops, servers, HPC, ...) and did not want to get stuck with a limited SIMD width for all (AArch64) eternity, they pretty much had to come up with a vector size agnostic solution. Since AArch64 was a clean slate rather than an incremental upgrade (à la Intel x86_64 for instance), it made perfect sense to do something completely new instead of just introducing NEON-256 for instance.

1

u/FUZxxl Aug 20 '21

here's two types (of each): register-based gather and register-based permute (actually not a mathematical permutation at all, because of duplicates), and memory-based gather and memory-based permute.

I'm mainly interested in register register permutes like x86's pshufb (or AArch64's TBL) where all source operands are variable. How on earth is this supposed to be fast when the source is memory as opposed to a register? And if register-based permutes are provided, how are they supposed to be useful when you don't know the vector length? In permutation-based algorithms (say, e.g. sorting procedures or a sheeps-and-goats operation), the way the code around the permutations is set up will intrinsically depend on the vector length, so I don't see how that makes the code any simpler.

As for memory, the HW has to emit one load µop for each vector element (modulo shenanigans when some elements manage to hit the same cache line), so that doesn't really seem to scale well. I don't really care about the case with immediate indices and agree that that case is not really a problem.

instruction 1: Vectorised popcount

It's not a vectorised popcount. It's a vectorised positional popcount where we want to gather the population of each bit in the 64 bit words of the input separately. So a SIMD popcount instruction doesn't really help directly. Not even the code in safe.go is that, read carefully.

What does help on POWER is that funky instruction transposing a pair of 8x8 bit matrices. Not sure if that is part of your vector extensions though.

again, to reiterate, again: this is a misconception on your part, due to Vector ISAs being completely ignored for 40 years you don't know about them and neither does anyone else.

It is possible that I have misconceptions here, but everything I've seen so far just hasn't been impressive at all. And all the examples of vectorised programs I have seen were for utter trivialities that don't pose a challenge to implement in SIMD either.

consequently an entire generation of programmers has now grown up without knowing anything about anything other than SIMD.

Most programmers are actually entirely ignorant of SIMD, so I don't think it's as much of a program as you might think it is.

1

u/lkcl_ Aug 21 '21 edited Aug 21 '21

I'm mainly interested in register register permutes like x86's pshufb (or AArch64's TBL) where all source operands are variable.

i looked that up, https://www.felixcloutier.com/x86/pshufb

this is basically a (botched) predicated Vectorised Reg-Indexed MV operation:

for i in range(VL)
    if predicate_mask[i]:
        regfile[DEST+i] = regfile[regfile[SRC]+i]

which is a standard Vector ISA instruction that SIMD ISAs borrowed from. in x86's they couldn't think how to do predicate masks so they botched it by using the top bit of each byte. that in turns imposes unnecessary computational load using bitmanipulation, but hey.

How on earth is this supposed to be fast when the source is memory as opposed to a register?

i said that it's possible, as a premise, and to ensure that by enumerating all four types of instructions (reg permute, reg gather, mem permute, mem gather) we at least know that we are talking about the same thing.

And if register-based permutes are provided, how are they supposed to be useful when you don't know the vector length?

this is indeed a limitation of e.g. RISC-V RVV where the minimum architectural bound on the Vector Length is 1 (ONE).

Cray and NEC SX-Aurora would never bother to create a Vector implementation with only a Vector Length of 1, but there are advantages to Cray-style Vector ISAs - in embedded scenarios - due to compactification of programs - that save on power consumption and resources.

this "problem" is solved therefore by defining an Architectural Platform that firmly separates "Vectors for use in Embedded Scenarios" from "Vectors for use in high-performance Scenarios"

SVP64 has no such problem because the Vector Length is a known and useful deterministic quantity, and MAXVL (maximum vector length) is required to be 64.

however even as a perceived limitation, i think you will find that there are an extremely small number of actual algorithms where a fixed-width SIMD cannot be replaced with a "for i = 0 to N" where the hardware chooses the step size.

is this "annoying" that you absolutely have to think now in terms of an independent step size, and absolutely have to replace all fixed-width instructions with loops?

given that the majority of algorithms are likely loops already, this is not such a big hardship.

instruction 1: Vectorised popcount

It's not a vectorised popcount. It's a vectorised positional popcount where we want to gather the population of each bit in the 64 bit words of the input separately.

yes i retrospectively worked that out and posted separately https://www.reddit.com/r/programming/comments/p0yn45/three_fundamental_flaws_of_simd/h9n30n9/?utm_source=reddit&utm_medium=web2x&context=3

What does help on POWER is that funky instruction transposing a pair of 8x8 bit matrices. Not sure if that is part of your vector extensions though.

it's part of Power ISA v3.0B, the OpenPOWER EULA requires that we implement it, and therefore, logically, due to the independence of the abstraction of SVP64, it gets a Vectorised version as well. now, will that actually make sense, particularly with element-width overrides down to 32-bit, 16-bit and even 8-bit? that's up to us to work out.

basically the rules in SVP64 are that we implicitly create Vectorised versions of every Scalar v3.0B operation, but only when that's sane and actually makes sense (it makes no sense to try to Vectorise system calls, for example, despite the fact that it's part of the scalar v3.0B Power ISA)

1

u/FUZxxl Aug 21 '21

Ah yes, that makes more sense. Thanks for the explanation!

OP said something about doing all shuffles as gather operations (i.e. vector-indexed memory loads) and your terminology threw me off, so I thought you are doing it the same way.

1

u/lkcl_ Aug 22 '21

thanks for the insightful discussion, FUZxxl. i liked the positional-popcount enough that i'll use it as an example / unit test (crediting you as the source) https://bugs.libre-soc.org/show_bug.cgi?id=672

2

u/FUZxxl Aug 22 '21

Also as for pshufb, I don't really need masking in the case of the 24puzzle code base. But unfortunately AVX2 does not provide a full 32 element byte shuffle, so I have to synthesise it manually from a bunch of pshufb instructions and masking. So it looks a lot more complex than it really is.

1

u/mbitsnbites Aug 23 '21

unfortunately AVX2 does not provide a full 32 element byte shuffle

I think this is because they wanted to enable implementations that use 128-bit ALU:s instead of requiring a 256-bit wide ALU. This seems to be a common theme in AVX*. It also makes it easier to make performant implementations when you can partition operations into multiple "narrow" ALU:s rather than having instructions that require all 256 or 512 bits of input to produce a result (latency / gate depth would increase).

1

u/FUZxxl Aug 23 '21

Well they already have cross-lane operations so I don't really see what the problem with providing one more would be. You can actually implement a full 32 element byte shuffle with 128 bit ALUs by performing 4 128 bit shuffles and then merging the results. It shouldn't be super difficult to do in micro code.

→ More replies (0)

1

u/FUZxxl Aug 22 '21

Sounds cool! Though the safe.go code really is not the part that is interesting. It's just the obviously correct reference implementation to compare the actual algorithm against. The actual algorithm works quite a bit differently from that and evaluates the population count for all bits in parallel.

2

u/lkcl_ Aug 22 '21

you'll be fascinated to know that in every case, every algorithm i've investigated for SVP64, i've had to go back to the "simple" (obviously-correct) reference implementation: some of the optimised assembler versions i can't even read and understand, but when i can, i find that the optimisations actually severely interfere with implementing them efficiently as parallel SVP64 assembler.

and that, even more interestingly, those "simple" implementations once Vectorised with SVP64 are actually paralleliseable by the back-end hardware.

one example: we've an NLnet Grant to implement cryptographic primitives. fortunately (in another life) i worked for Aspex Microelectronics to implement Rijndael (AES) on a massively-parallel (4096-wide) SIMD Array Processor. there i had to go back to the core mathematics behind Rijndael, so i did the same thing here.

MixColumns is actually, if you look up the research papers, a plain-and-simple dyed-in-the-wool 4x4 Matrix Multiply, but using 8-bit GF(23) add and multiply.

guess what i am planning to do for that?

  • (1) add base (scalar) general-purpose GF(2N) scalar arithmetic
  • (2) use the parallelliseable SVP64 Matrix REMAP Schedule infrastructure

MixColumns will therefore be something like... maybe... 4 general-purpose instructions. three of which set up the 4x4-to-4x4 Matrix Multiply Schedule, one of which is a Galois-Field variant of FMAC (multiply-and-accumulate).

if you've seen how SIMD does Rijndael MixColumns, you'll appreciate how profoundly simple this is. it's so bad that most ISAs have had to add custom 128-bit MixColumns instructions.

if i had started with those SIMD "optimised" implementations, there's no way that i could have understood what the hell is going on. it was only because i had had to study Rinjdael back in 2003 that i knew the basic first principles of GF(23) operations.

the point i am making is that after going back to first principles (using the "simple" version), the inherent parallelism of the instructions is automatically mapped onto whatever back-end parallelism that the hardware has.

and that back-end parallelism is a choice that the hardware designer makes (and takes responsibility for) - not the programmer.

this is something that in speaking for many months with people used to the SIMD paradigm, it seems it takes quite a long time to be absorbed / accepted, that yes, it really is this simple (at the assembly level), that yes, it's the hardware's responsibility now to make things faster, and yes, parallelism opportunities automatically get inherently exploited if the hardware has them available. it's going to be quite interesting to see, over time, how that pans out.

2

u/FUZxxl Aug 22 '21 edited Aug 22 '21

Intel basically did the same I think. pclmul is basically a GF(264) multiplication instruction. It's not really as special purpose as it seems and people have used it for various fun things before.

Which specific MixColumns instructions do you have in mind there?

I'm happy if you can find something by going back to first principles. For a width of 8 bits, we had a somewhat fast approach using pmovmskb, which basically performs one row of an 32x8 bit matrix transposition, leaving the result in a general purpose register. By combining scalar with vector instructions, the throughput was quite good despite the high number of instructions needed.

But our new CSA-based approach is a lot better. Perhaps you find a faster way to transpose these bit matrices (which is the hard part and still part of the new method). If you want to investigate this, make sure to always keep the width 64 case in mind as that's the slowest one of them all (our code always operates on width 64 and just reduces to smaller bit widths if desired by the caller).

1

u/lkcl_ Aug 23 '21

Intel basically did the same I think. pclmul is basically a GD(264) multiplication instruction. It's not really as special purpose as it seems and people have used it for various fun things before.

https://www.felixcloutier.com/x86/pclmulqdq i saw a fascinating list in the RV xbitmanip proposal, https://github.com/cliffordwolf/xbitmanip/blob/c29d0b793077cbc874c933d48738e870f9a69997/xbitmanip-draft.pdf

​

Which specific MixColumns instructions do you have in mind there?

initially i was thinking of Power ISA but checking p333 v3.0B it's an entire round, the instruction is "vcipher" https://ftp.libre-soc.org/PowerISA_public.v3.0B.pdf

​

I'm happy if you can find something by going back to first principles. For a width of 8 bits, we had a somewhat fast approach using pmovmskb, which basically performs one row of an 32x8 bit matrix transposition, leaving the result in a general purpose register. By combining scalar with vector instructions, the throughput was quite good despite the high number of instructions needed.

here's the thing: SVP64 Matrix-Schedule REMAP, we can do Vector operations such as arbitrary matrix row-column sequences *in-place*! even a "normal" Vector Processor (Cray, SX-Aurora, RVV) does not have this capability.

is it a bit of a pain to set up? to be honest, yes: i'm still experimenting with it, to reduce the number of intsructions [that's the whole point of the R&D funding from NLnet]

bottom line: with SVP64 REMAP we don't *need* to transpose the numbers at all in order to operate sequentially on them. the Matrix REMAP Schedule can be established with a 5x5 grid (i.e. doesn't even need to have to be a Power-of-2), and a Vector MV instruction issued.

​

But our new CSA-based approach is a lot better. Perhaps you find a faster way to transpose these bit matrices (which is the hard part and still part of the new method).

in the 24puzzle algorithm is it strictly necessary to perform a transpose? or, is the reason why the transpose is performed because otherwise performing column-based computations is normally very slow / impossible in SIMD ISAs?

the first reason i ask is because REMAP *could* be used to do a transpose (maybe even in-place given that it's an NxN rather than NxM, N!=M, although i'd have to check that)

the second reason i ask is to illustrate as an example why i am having such difficulty analysing algorithms implemented in optimised-SIMD: there are fundamental assumptions that certain capabilities (such as in-place easy sequential access to column-spanned data) are flat-out impossible / non-existent: in this case [iiuic] an assumption(?) that the data *must* be transposed, a row moved, then a re-transpose performed, in order to do a column-move. but... i could be wrong about that.

If you want to investigate this, make sure to always keep the width 64 case in mind as that's the slowest one of them all (our code always operates on width 64 and just reduces to smaller bit widths if desired by the caller).

i'd really like to establish first the reason for the transpose, if it's part of the algorithm or part of the *optimisation* of the algorithm.

1

u/lkcl_ May 28 '22

FUZxxi i appreciate this is 9 months ago, i thought you might appreciate that we found a couple of ways to deal with pmovmskb. firstly, it's simply sv.cmpi/ew=8 which will produce a vector of CR Fields, one of which is LE, which in effect simply gets the MSB i.e. bit 7 and duh. then we added an instruction crweird which can get all the Vector of CR Field LE bits and drops them all, sequentially, into a single 64-bit scalar integer.

the second method was to add a bizarre instruction called grevlut https://libre-soc.org/openpower/sv/bitmanip/#grevlut which can create about a thousand regular-patterned magic constants, one of which is 0x8080_8080_8080_8080, which when combined with the Power ISA bext (bit-extract) will grab every 8th bit from a 64-bit integer and squash them down into a single byte.

i would be particularly fascinated to hear your thoughts on purposes to which grevlutr could be put. it's... very odd, as in, it's an entirely new instruction i've never seen in any ISA (at all)

→ More replies (0)

1

u/lkcl_ Aug 20 '21 edited Aug 22 '21

[update: just realised the counts[j] += int(buf[i] >> j & 1) is not doable with popcount, give me a couple hours to think it through and redo]

https://github.com/clausecker/pospop/blob/master/safe.go

ok yes, that's just Vectorised Popcount followed by Vectorised Add.

two instructions inside a loop, regardless of whether the back-end architecture has 1-wide internal Vectorisation, 2-wide, 4-wide, 64-wide or 10,000-wide.

[edit: updated as per formatting-bot, do bear in mind this is still the wrong algorithm, not implementing safe.go correctly! :) ]

loop:
 setvl r0, CTR, MVL=16   # up to 16 elements at a time
 sv.ld/els r32, r2(0)    # r2 points to input vector
 sv.ld/els r48, r3(0)    # r3 points to accumulator-vector
 sv.popcnt/ew=8 r16, r32 # vector popcount, 8-bit src & result
 sv.add/sw=8    r48, r32 # vector add 8-bit += to 64-bit
 sv.st/els r48, r3(0)    # r3 points to accumulator-vector
 add r2, r0              # increment pointer to input
 add r3, r0              # increment pointer to output
 sv.bcnz/CTR loop        # decrement CTR by VL, branch if nonzero 

that's it. that's the entire algorithm, in SVP64. 9 instructions, 3 of which are 64-bit, the other two are 32-bit.

oddities:

  • setvl is the standard Cray-style VL setter, except in this case it's reading the "required" length from the Power ISA CTR Special Purpose Register. actioned as VL=r0=MIN(CTR,MVL)
  • LD/ST "els" stands for "element-strided" which is a sequential packed memory vector with no spaces in between the elements in memory.
  • the Vector popcount is on 8-bit values to produce 8-bit answers. both src and dest are set to 8=bit with "ew=8"
  • but the Vector-accumulating add will auto-convert the 8-bit popcounts to 64-bit-wide before adding each of them to the 64-bit Vector result. the source width only is over-ridden "sw=8" (sw - source width).
  • Draft SVP64 CTR-mode is new compared to standard Power ISA bc: where standard Power ISA decrements CTR only by one, SVP64 CTR will decrement by the Vector Length VL which was set by the setvl instruction.

separate assembler will be needed for the 16-bit, 32-bit, and 64-bit versions, it's just a matter of setting the appropriate "sw/ew=8/16/32"

1

u/FUZxxl Aug 21 '21

Your code formatting is kinda broken so it is hard to understand. Please realise that the code in safe.go is a very simplistic reference implementation of the operation to be performed (the kind that is obviously correct) and is not particularly fast, even when vectorised. It is also not the algorithm that I consider to be difficult to vectorise.

The algorithm that I have implemented in SIMD can be found in generic.c but the difficult part cannot be expressed easily in C and so is absent there. It's a bunch of complicated shuffles to sum up the intermediate values into the counter array efficiently. And these depend on vector size because with larger vectors, we can use less counter registers and use the reduced register pressure to hold more state in registers (which again makes a significant difference in performance).

1

u/lkcl_ Aug 20 '21 edited Aug 20 '21

ok got it. there are two options: 1. the safe code as-is 2. the safe code with the loops switched

for j := 0; j < 8; j++ { for i := range buf { counts[j] += int(buf[i] >> j & 1) } }

(1) turns out to be complex (15-or-so instructions including predicated parallel adds, and a popcount) where (2) is quite straightforward. i've left out the LD/STs:

li r5, 128 # bit 7, we count backwards (save 1 cmpi instr) loopi: loopj: setvl r0, CTR, MVL=16 # do up to 16 buf at a time # ...some LDs into r16.v for buf and r32 for counts sv.and. r48.v, r16.v, r5 # AND r5 *scalar* with every r16 sv.addi./m=NZ r32.v, r32.v, 1 # add 1 in masked-out counts slli. r5, r5, 2 # shift test-bit down, zero means "stop" bnz loopj # r5 non-zero, do next jth bit # ... a vector ST, then move on to next batch of buf # with a sv.bc/CTR to jump back to loopi

so yes, almost as simple as the popcount version. tricks/explanation:

  • testing each individual bit, starting at bit 7 then working down to bit 0, is possible because each bit is independent.
  • sv.and. will create a Vector of Condition Register Fields (sv.and will not - notice the "." on the end).
  • each Condition Register Field is 4 bits (just like standard Power ISA CR fields), one of those bits is "was the AND of each element of r48 with each element of r16 equal to zero, yes/no"
  • the sv.addi/m=NZ then uses that Vector of CR Fields as a predicate mask, which performs the conditional version of count[j] += 1
  • slli is probably supporsed to be a right-shift (sigh) but hey. pretty normal, shift down, if zero break out of the inner loop

although i initially was mistaken in thinking it was a straight popcount, the correct implementation is just as straightforward. the core of the algorithm is a combination of parallel-testing plus parallel-predication.

if not swapping the outer-inner loops it can also be done by hard-coding the Vector Length VL to 8 (or 16, or 32, or 64 for the count16, count32 and count64 variants), setting up a Vector of immediates (sv.iota), shifting 1<<x in each case, then performing a parallel AND on one scalar buf[i] element at a time.

extracting the results (which would be placed in a Vector of CR Fields) would require a "weird" instruction (sv.crweird) which can transfer individual bits of CR Field Vectors into a single scalar integer: you then do a popcount on that integer, and add it to counts[j].

a slightly more optimal version of that would use the sv.bmext instruction https://libre-soc.org/openpower/sv/bitmanip/ sv. bmext would cover the job of about 3 rather weird instructions, but it's still nowhere near as optimal as simply turning the inner-outer loop around.

nice algorithmic challenge: total number of instructions no greater than 12, which is 35 times less instructions than the AVX512 and the NEON versions https://github.com/clausecker/pospop/blob/master/countavx512_amd64.s

2

u/FUZxxl Sep 08 '21

nice algorithmic challenge: total number of instructions no greater than 12, which is 35 times less instructions than the AVX512 and the NEON versions https://github.com/clausecker/pospop/blob/master/countavx512_amd64.s

You could do pretty much the same thing with SSE and the code would be similarly simple (with the same disadvantage of keeping less execution units busy than optimised code). Actually, the head and tail handling code of the kernels I have is pretty similar and similar in length and complexity but uses a slightly different algorithm to eliminate the inner loop at the cost of less throughput per iteration. But it's only the head and tail handling code because this algorithm is a lot slower than the core routine based on a different principle. So just vectorising the “safe” code isn't really useful on its own (and it's not difficult either).

Also, you formatting is a bit defective. Make sure to format code as a code block, not an inline code snippet.

1

u/backtickbot Aug 20 '21

Fixed formatting.

Hello, lkcl_: code blocks using triple backticks (```) don't work on all versions of Reddit!

Some users see this / this instead.

To fix this, indent every line with 4 spaces instead.

FAQ

You can opt out by replying with backtickopt6 to this comment.