all: AVX-512 (#217)

Extends avo to support most AVX-512 instruction sets.

The instruction type is extended to support suffixes. The K family of opmask
registers is added to the register package, and the operand package is updated
to support the new operand types. Move instruction deduction in `Load` and
`Store` is extended to support KMOV* and VMOV* forms.

Internal code generation packages were overhauled. Instruction database loading
required various messy changes to account for the additional complexities of the
AVX-512 instruction sets. The internal/api package was added to introduce a
separation between instruction forms in the database, and the functions avo
provides to create them. This was required since with instruction suffixes there
is no longer a one-to-one mapping between instruction constructors and opcodes.

AVX-512 bloated generated source code size substantially, initially increasing
compilation and CI test times to an unacceptable level. Two changes were made to
address this:

1.  Instruction constructors in the `x86` package moved to an optab-based
    approach. This compiles substantially faster than the verbose code
    generation we had before.

2.  The most verbose code-generated tests are moved under build tags and
    limited to a stress test mode. Stress test builds are run on
    schedule but not in regular CI.

An example of AVX-512 accelerated 16-lane MD5 is provided to demonstrate and
test the new functionality.

Updates #20 #163 #229

Co-authored-by: Vaughn Iverson <vsivsi@yahoo.com>
This commit is contained in:
Michael McLoughlin
2021-11-12 18:35:36 -08:00
parent 2867bd7e01
commit b76e849b5c
71 changed files with 257395 additions and 61474 deletions

2
internal/api/doc.go Normal file
View File

@@ -0,0 +1,2 @@
// Package api determines the API of avo instruction constructors.
package api

177
internal/api/function.go Normal file
View File

@@ -0,0 +1,177 @@
package api
import (
"bytes"
"fmt"
"sort"
"strconv"
"strings"
"text/tabwriter"
"github.com/mmcloughlin/avo/internal/inst"
)
// Function represents a function that constructs some collection of
// instruction forms.
type Function struct {
Instruction inst.Instruction
Suffixes inst.Suffixes
inst.Forms
}
// Name returns the function name.
func (f *Function) Name() string {
return f.opcodesuffix("_")
}
// Opcode returns the full Go opcode of the instruction built by this function. Includes any suffixes.
func (f *Function) Opcode() string {
return f.opcodesuffix(".")
}
func (f *Function) opcodesuffix(sep string) string {
n := f.Instruction.Opcode
for _, suffix := range f.Suffixes {
n += sep
n += suffix.String()
}
return n
}
// HasSuffix reports whether the function has the provided suffix.
func (f *Function) HasSuffix(suffix inst.Suffix) bool {
for _, s := range f.Suffixes {
if s == suffix {
return true
}
}
return false
}
// Summary returns a summary of the instruction this function constructs.
func (f *Function) Summary() string {
summary := f.Instruction.Summary
if len(f.Suffixes) > 0 {
summary += " (" + strings.Join(f.Suffixes.Summaries(), ", ") + ")"
}
return summary
}
// Doc returns the function document comment as a list of lines.
func (f *Function) Doc() []string {
lines := []string{
fmt.Sprintf("%s: %s.", f.Name(), f.Summary()),
"",
"Forms:",
"",
}
// Write a table of instruction forms.
buf := bytes.NewBuffer(nil)
w := tabwriter.NewWriter(buf, 0, 0, 1, ' ', 0)
for _, form := range f.Forms {
row := f.Opcode() + "\t" + strings.Join(form.Signature(), "\t") + "\n"
fmt.Fprint(w, row)
}
w.Flush()
tbl := strings.TrimSpace(buf.String())
for _, line := range strings.Split(tbl, "\n") {
lines = append(lines, "\t"+line)
}
return lines
}
// Signature of the function. Derived from the instruction forms generated by this function.
func (f *Function) Signature() Signature {
// Handle the case of forms with multiple arities.
switch {
case f.IsVariadic():
return variadic{name: "ops"}
case f.IsNiladic():
return niladic{}
}
// Generate nice-looking variable names.
n := f.Arity()
ops := make([]string, n)
count := map[string]int{}
for j := 0; j < n; j++ {
// Collect unique lowercase bytes from first characters of operand types.
s := map[byte]bool{}
for _, form := range f.Forms {
c := form.Operands[j].Type[0]
if 'a' <= c && c <= 'z' {
s[c] = true
}
}
// Operand name is the sorted bytes.
var b []byte
for c := range s {
b = append(b, c)
}
sort.Slice(b, func(i, j int) bool { return b[i] < b[j] })
name := string(b)
// Append a counter if we've seen it already.
m := count[name]
count[name]++
if m > 0 {
name += strconv.Itoa(m)
}
ops[j] = name
}
return argslist(ops)
}
// InstructionFunctions builds the list of all functions for a given
// instruction.
func InstructionFunctions(i inst.Instruction) []*Function {
// One function for each possible suffix combination.
bysuffix := map[string]*Function{}
for _, f := range i.Forms {
for _, suffixes := range f.SupportedSuffixes() {
k := suffixes.String()
if _, ok := bysuffix[k]; !ok {
bysuffix[k] = &Function{
Instruction: i,
Suffixes: suffixes,
}
}
bysuffix[k].Forms = append(bysuffix[k].Forms, f)
}
}
// Convert to a sorted slice.
var fns []*Function
for _, fn := range bysuffix {
fns = append(fns, fn)
}
SortFunctions(fns)
return fns
}
// InstructionsFunctions builds all functions for a list of instructions.
func InstructionsFunctions(is []inst.Instruction) []*Function {
var all []*Function
for _, i := range is {
fns := InstructionFunctions(i)
all = append(all, fns...)
}
SortFunctions(all)
return all
}
// SortFunctions sorts a list of functions by name.
func SortFunctions(fns []*Function) {
sort.Slice(fns, func(i, j int) bool {
return fns[i].Name() < fns[j].Name()
})
}

View File

@@ -0,0 +1,50 @@
package api
import (
"strings"
"testing"
"github.com/mmcloughlin/avo/internal/inst"
)
func TestFunctionsDuplicateFormSignatures(t *testing.T) {
// Test for duplicate form signatures within a given function. This could
// manifest as duplicate case statements in generated code.
fns := InstructionsFunctions(inst.Instructions)
for _, fn := range fns {
fn := fn // scopelint
t.Run(fn.Name(), func(t *testing.T) {
seen := map[string]bool{}
for _, f := range fn.Forms {
sig := strings.Join(f.Signature(), "_")
t.Log(sig)
if seen[sig] {
t.Fatalf("duplicate: %s", sig)
}
seen[sig] = true
}
})
}
}
func TestFunctionsUniqueArgNames(t *testing.T) {
fns := InstructionsFunctions(inst.Instructions)
for _, fn := range fns {
s := fn.Signature()
for _, n := range fn.Arities() {
if n == 0 {
continue
}
names := map[string]bool{}
for j := 0; j < n; j++ {
names[s.ParameterName(j)] = true
}
if len(names) != n {
t.Errorf("repeated argument for instruction %s", fn.Name())
}
if _, found := names[""]; found {
t.Errorf("empty argument name for instruction %s", fn.Name())
}
}
}
}

59
internal/api/signature.go Normal file
View File

@@ -0,0 +1,59 @@
package api
import (
"fmt"
"strconv"
"strings"
)
// Signature provides access to details about the signature of an instruction function.
type Signature interface {
ParameterList() string
Arguments() string
ParameterName(int) string
ParameterSlice() string
Length() string
}
// ArgsList builds a signature for a function with the named parameters.
func ArgsList(args []string) Signature {
return argslist(args)
}
type argslist []string
func (a argslist) ParameterList() string { return strings.Join(a, ", ") + " " + OperandType }
func (a argslist) Arguments() string { return strings.Join(a, ", ") }
func (a argslist) ParameterName(i int) string { return a[i] }
func (a argslist) ParameterSlice() string {
return fmt.Sprintf("[]%s{%s}", OperandType, strings.Join(a, ", "))
}
func (a argslist) Length() string { return strconv.Itoa(len(a)) }
// Variadic is the signature for a variadic function with the named argument slice.
func Variadic(name string) Signature {
return variadic{name: name}
}
type variadic struct {
name string
}
func (v variadic) ParameterList() string { return v.name + " ..." + OperandType }
func (v variadic) Arguments() string { return v.name + "..." }
func (v variadic) ParameterName(i int) string { return fmt.Sprintf("%s[%d]", v.name, i) }
func (v variadic) ParameterSlice() string { return v.name }
func (v variadic) Length() string { return fmt.Sprintf("len(%s)", v.name) }
// Niladic is the signature for a function with no arguments.
func Niladic() Signature {
return niladic{}
}
type niladic struct{}
func (n niladic) ParameterList() string { return "" }
func (n niladic) Arguments() string { return "" }
func (n niladic) ParameterName(i int) string { panic("niladic function has no parameters") }
func (n niladic) ParameterSlice() string { return fmt.Sprintf("[]%s{}", OperandType) }
func (n niladic) Length() string { return "0" }

75
internal/api/types.go Normal file
View File

@@ -0,0 +1,75 @@
package api
import (
"path"
"sort"
"strings"
)
const (
// Package is the avo package import path.
Package = "github.com/mmcloughlin/avo"
// IRPackage is the package that defines intermediate representation types.
IRPackage = "ir"
// OperandPackage is the package for operand types.
OperandPackage = "operand"
// OperandType is the type used for operands.
OperandType = OperandPackage + ".Op"
// RegisterPackage is the name of the package containing register types.
RegisterPackage = "reg"
// RegisterType is the type used for registers.
RegisterType = RegisterPackage + ".Register"
)
// ImportPath returns the full import path for an avo subpackage.
func ImportPath(pkg string) string {
return path.Join(Package, pkg)
}
// ImplicitRegisterIdentifier maps an implicit register name to a string
// suitable for a related Go identifier.
func ImplicitRegisterIdentifier(r string) string {
r = strings.Replace(r, "mm", "", 1) // handles the "xmm0" type
return strings.ToUpper(r)
}
// ImplicitRegister returns avo syntax for the given implicit register type (from Opcodes XML).
func ImplicitRegister(r string) string {
return RegisterPackage + "." + ImplicitRegisterIdentifier(r)
}
// OperandTypeIdentifier maps an operand type to a string suitable for a related
// Go identifier.
func OperandTypeIdentifier(t string) string {
return strings.ToUpper(strings.ReplaceAll(t, "/", ""))
}
// CheckerName returns the name of the function that checks an operand of type t.
func CheckerName(t string) string {
return "operand.Is" + OperandTypeIdentifier(t)
}
// ISAsIdentifier returns a string representation of an ISA list that suitable
// for use in a Go identifier.
func ISAsIdentifier(isas []string) string {
if len(isas) == 0 {
return "Base"
}
sorted := append([]string(nil), isas...)
sort.Strings(sorted)
ident := strings.Join(sorted, "_")
ident = strings.ReplaceAll(ident, ".", "") // SSE4.1
ident = strings.ReplaceAll(ident, "+", "") // MMX+
return ident
}
// SuffixesClassIdentifier returns a string representation of a suffix class
// that's suitable for use in a Go identifier.
func SuffixesClassIdentifier(c string) string {
return strings.ToUpper(c)
}

View File

@@ -0,0 +1,26 @@
package api
import (
"go/token"
"testing"
"github.com/mmcloughlin/avo/internal/inst"
)
func TestISAsIdentifier(t *testing.T) {
for _, isas := range inst.ISACombinations(inst.Instructions) {
ident := ISAsIdentifier(isas)
if !token.IsIdentifier(ident) {
t.Errorf("expected %q to be an identifier", ident)
}
}
}
func TestSuffixesClassIdentifier(t *testing.T) {
for key := range inst.SuffixesClasses(inst.Instructions) {
ident := SuffixesClassIdentifier(key)
if !token.IsIdentifier(ident) {
t.Errorf("expected %q to be an identifier", ident)
}
}
}