[LLVMdev] RFC: AVX Pattern Specification [LONG]

David Greene dag at cray.com
Thu Apr 30 15:59:11 PDT 2009


Here's the big RFC.

A I've gone through and designed patterns for AVX, I quickly realized that the 
existing SSE pattern specification, while functional, is less than ideal in 
terms of maintenance.  In particular, a number of nearly-identical patterns 
are specified all over for nearly-identical instructions.  For example:

let Constraints = "$src1 = $dst" in {
multiclass basic_sse1_fp_binop_rm<bits<8> opc, string OpcodeStr,
                                  SDNode OpNode, Intrinsic F32Int,
                                  bit Commutable = 0> {
  // Scalar operation, reg+reg.
  def SSrr : SSI<opc, MRMSrcReg, (outs FR32:$dst), 
                                 (ins FR32:$src1, FR32:$src2),
                 !strconcat(OpcodeStr, "ss\t{$src2, $dst|$dst, $src2}"),
                 [(set FR32:$dst, (OpNode FR32:$src1, FR32:$src2))]> {
    let isCommutable = Commutable;
  }

  // Scalar operation, reg+mem.
  def SSrm : SSI<opc, MRMSrcMem, (outs FR32:$dst),
                                 (ins FR32:$src1, f32mem:$src2),
                 !strconcat(OpcodeStr, "ss\t{$src2, $dst|$dst, $src2}"),
                 [(set FR32:$dst, (OpNode FR32:$src1, (load addr:$src2)))]>;
                 
  // Vector operation, reg+reg.
  def PSrr : PSI<opc, MRMSrcReg, (outs VR128:$dst),
                                 (ins VR128:$src1, VR128:$src2),
               !strconcat(OpcodeStr, "ps\t{$src2, $dst|$dst, $src2}"),
               [(set VR128:$dst, (v4f32 (OpNode VR128:$src1, 
                                                VR128:$src2)))]> {
    let isCommutable = Commutable;
  }

  // Vector operation, reg+mem.
  def PSrm : PSI<opc, MRMSrcMem, (outs VR128:$dst),
                                 (ins VR128:$src1, f128mem:$src2),
                 !strconcat(OpcodeStr, "ps\t{$src2, $dst|$dst, $src2}"),
             [(set VR128:$dst, (OpNode VR128:$src1, 
                                       (memopv4f32 addr:$src2)))]>;
}

These are all essentially the same except that ModRM formats, types and 
register classes change.  For patterns that access memory there are special 
"memory access operators" like memopv4f32.  But the base pattern of dest = 
src1 op src2 is the same.

Worse yet:

let Constraints = "$src1 = $dst" in {
multiclass basic_sse2_fp_binop_rm<bits<8> opc, string OpcodeStr,
                                  SDNode OpNode, Intrinsic F64Int,
                                  bit Commutable = 0> {
  // Scalar operation, reg+reg.
  def SDrr : SDI<opc, MRMSrcReg, (outs FR64:$dst), 
                                 (ins FR64:$src1, FR64:$src2),
                 !strconcat(OpcodeStr, "sd\t{$src2, $dst|$dst, $src2}"),
                 [(set FR64:$dst, (OpNode FR64:$src1, FR64:$src2))]> {
    let isCommutable = Commutable;
  }

  // Scalar operation, reg+mem.
  def SDrm : SDI<opc, MRMSrcMem, (outs FR64:$dst), 
                                 (ins FR64:$src1, f64mem:$src2),
                 !strconcat(OpcodeStr, "sd\t{$src2, $dst|$dst, $src2}"),
                 [(set FR64:$dst, (OpNode FR64:$src1, (load addr:$src2)))]>;
                 
  // Vector operation, reg+reg.
  def PDrr : PDI<opc, MRMSrcReg, (outs VR128:$dst), 
                                 (ins VR128:$src1, VR128:$src2),
               !strconcat(OpcodeStr, "pd\t{$src2, $dst|$dst, $src2}"),
               [(set VR128:$dst, (v2f64 (OpNode VR128:$src1, 
                                                VR128:$src2)))]> {
    let isCommutable = Commutable;
  }

  // Vector operation, reg+mem.
  def PDrm : PDI<opc, MRMSrcMem, (outs VR128:$dst), 
                                 (ins VR128:$src1, f128mem:$src2),
                 !strconcat(OpcodeStr, "pd\t{$src2, $dst|$dst, $src2}"),
                 [(set VR128:$dst, (OpNode VR128:$src1, 
                                           (memopv2f64 addr:$src2)))]>;
}

This looks identical to basic_sse1_fp_binop_rm except it's SD/PD instead of 
SS/PS and types and register classes differ in a predictable way.

So we already have two "levels" or redundancy: one within a single multiclass 
and then redudancy across multiclasses.

This gets even worse with more complicated patterns like converts.  
Essentially the same complex pattern gets duplicated for the variously-sized 
converts.  Bug fixes in once place need to be replicated everywhere and it's 
easy to miss one or two.  This is the very definition of "maintenance 
problem."

Moreover, the various SSE levels were implemented at different times and do 
things subtly differently.  For example:

SSE1 :

  def ANDNPSrr : PSI<0x55, MRMSrcReg,
                     (outs VR128:$dst), (ins VR128:$src1, VR128:$src2),
                     "andnps\t{$src2, $dst|$dst, $src2}",
                     [(set VR128:$dst,
                       (v2i64 (and (xor VR128:$src1,
                                    (bc_v2i64 (v4i32 immAllOnesV))),
                               VR128:$src2)))]>;

SSE2 :

  def ANDNPDrr : PDI<0x55, MRMSrcReg,
                     (outs VR128:$dst), (ins VR128:$src1, VR128:$src2),
                     "andnpd\t{$src2, $dst|$dst, $src2}",
                     [(set VR128:$dst,
                       (and (vnot (bc_v2i64 (v2f64 VR128:$src1))),
                        (bc_v2i64 (v2f64 VR128:$src2))))]>;

Note the use of xor vs. vnot and the different placement of the bc* fragments
and use of type specifiers.  I wonder if we even match both of these.

And naming is not consistent:

def Int_CVTSS2SIrr : SSI<0x2D, MRMSrcReg, (outs GR32:$dst), (ins VR128:$src),
def MOVUPSrm_Int : PSI<0x10, MRMSrcMem, (outs VR128:$dst), (ins f128mem:$src),

Furthermore, the current scheme ties patterns to prefix encodings and Requires 
predicates:

  // Scalar operation, reg+reg.
  def SSrr : SSI<opc, MRMSrcReg, (outs FR32:$dst), 
                                 (ins FR32:$src1, FR32:$src2),
                 !strconcat(OpcodeStr, "ss\t{$src2, $dst|$dst, $src2}"),
                 [(set FR32:$dst, (OpNode FR32:$src1, FR32:$src2))]> {
    let isCommutable = Commutable;
  }

>From X86InstrFormats.td:

class SSI<bits<8> o, Format F, dag outs, dag ins, string asm, 
          list<dag> pattern>
      : I<o, F, outs, ins, asm, pattern>, XS, Requires<[HasSSE1]>;

For AVX we would need a different set of format classes because while AVX 
could reuse the existing XS class (it's recoded as part of the VEX prefix so 
we still need the information XS provides), "Requires<[HasSSE1]>" is 
certainly inappropriate.  Initially I started factoring things out to separate 
XS and other prefix classes from Requires<> but that didn't solve the pattern 
problems I mentioned above.

All of this complication gets multipled with AVX because AVX recodes all of 
the legacy SSE instructions using VEX to provide three-address forms.  So if 
we were to follow the existing sceheme, we would duplicate *all* of 
X86InstrSSE.td and edit patterns to match three-address modes and then add the 
256-bit patterns on top of that,  effectively duplicating X86InstrSSE.td a 
second time.

This is not scalable.

So what I've done is a little experiment to see if I can unify all SSE and AVX 
SIMD instructions under one framework.  I'll leave MMX and 3dNow alone since 
they're oddballs and hardly anyone uses them.

Essentially I've created a set of base pattern classes that are very generic.  
These contain the basic asm string templates and dag patterns we want to 
match.  These classes are parameterized by things like register class, 
operand type, ModRM format and "memory access operation."  I've also created 
patterns that take a fully specified asm string and/or dag pattern to provide 
flexibility for "oddball" instructions.

Multiclasses sit on top of the patterns and aggregate various legal 
combinations (e.g. SS, SD, PS, PD for basic arithmetic).  There's a set
of base multiclasses and a set of derived multiclasses that aggregate
things into legal sets.  For example, some SSE instructions are vector-only
while others have scalar and vector versions.  Some instructions use the
XS, XD, TB and OpSize/TB prefixes while others use the TA, T8 and OpSize
prefixes.

The point of all of this is to write patterns and asm strings *once* for each
kind of instruction (binary arithmetic, convert, shuffle, etc.) and then use 
multiclasses to generate all of the concrete patterns for SSE and AVX.

So for example, an ADD would be specified like this:

// Arithmetic ops with intrinsics and scalar equivalents
defm ADD : 
sse1_sse2_avx_binary_scalar_xs_xd_vector_tb_ostb_node_intrinsic_rm_rrm<
   0x58,   // Opcode
   "add",  // asm base opcode name
   fadd,   // SDNode name
   "add",  // Intrinsic base name (we pre-concat int_x86_sse*/avx and 
           // post-contact ps/pd, etc.)
   1       // Commutative
>;

Now the multiclass name is rather unwieldy, I know.  That can be changed so 
don't worry too much about it.  I'm more concerned about the overall scheme
and that it make sense to you all.

I have a Perl script that auto-generates the necessary mutliclass combinations 
as well as the needed base classes depending on what's in the top-level .td 
file.  For now, I've named that top-level file X86InstrSIMD.td.

The Perl script would only be need to run as X86InstrSIMD.td changes.  Thus
its use would be similar to how we use autoconf today.  We only run autoconf / 
automake when we update the .ac files, not as part of the build process.

Initially, X86InstrSIMD.td would define only AVX instructions so it would not
impact existing SSE clients.  My intent is that X86InstrSIMD.td essentially
become the canonical description of all SSE and AVX instructions and 
X86InstrSSE.td would go away completely.

Of course we would not transition away from X86InstrSSE.td until 
X86InstrSIMD.td is proven to cover all current uses of  SSE correctly.

The pros of the scheme:

* Unify all "important" x86 SIMD instructions into one framework and provide 
  consistency

* Specify patterns and asm strings *once* per instruction type / family
  rather than the current scheme of multiple patterns for essentially the
  same instruction

* Bugfixes / optimizations / new patterns instantly apply to all SSE levels 
  and AVX

The cons:

* Transition from X86InstrSSE.td

* A more complex class hierarchy

* A class-generating tool / indirection

Personally, I think the pros far outweigh the cons but I realize that this 
proposes a major change and there are probably cons I haven't considered (and 
pros as well!).

So right now I'm looking for comments.  This is the way I intend to go because 
it's far easier in the long run considering maintenance and future extension.

I'll post an example as soon as I have time to package it up and get approval 
on this end to release it.  As of now I have simple arithmetic operations 
implemented and the proposed scheme seems to work.  Right now we're generating 
simple arithmetic and having it correctly assembled by gas.

Thanks for your ideas and input.

                             -Dave




More information about the llvm-dev mailing list