Open mmcloughlin opened 3 years ago
As a simpler half-measure that also has the advantage of being able to drive test file generation, etc. I wonder about just specifying a new exported function in build/global.go
like:
var irFile *ir.File
type FuncInfo struct {
Name string
Signature string
ISA []string
}
// Return the functions generated
func GeneratedFuncInfo() (retVal []FuncInfo) {
if irFile == nil {
return
}
for _, f := range irFile.Functions() {
retVal = append(retVal, FuncInfo{f.Name, f.Signature.String(), f.ISA})
}
return
}
Questions:
What is the preferred way to set irFile
?
ctx.Result()
directly within GeneratedFuncInfo()
and hope for the best.Main()
when it is called by Generate()
Main()
to return it to Generate()
and store it there. GenerateWithFuncInfo()
that does everything itself.1) could return weird results depending on when GeneratedFuncInfo is called.
2) adds an inter-file global side effect.
3) requires a change to the exported API for Main.
4) will unavoidably lead to some code redundancy, although Generate()
could use GenerateWithFuncInfo()
and just discard the return value.
Thoughts?
I quickly prototyped this using approach 2) above, and it works exactly as I need, e.g.:
Generate()
for _, f := range GeneratedFuncInfo() {
fmt.Printf("Func: %s \t Sig: %s \t Reqs: %v\n", f.Name, f.Signature, f.ISA)
}
Prints:
Func: varLenWriteAVX512_4 Sig: (in [][4]uint64, out *[4][]uint64, thresh uint64) byte Reqs: [AVX2 AVX512DQ AVX512F AVX512VL]
Func: varLenWriteAVX2_4 Sig: (in [][4]uint64, out *[4][]uint64, thresh uint64) byte Reqs: [AVX AVX2 SSE2]
Func: varLenWriteAVX512_8 Sig: (in [][8]uint64, out *[8][]uint64, thresh uint64) byte Reqs: [AVX512DQ AVX512F]
Func: varLenWriteAVX2_8 Sig: (in [][8]uint64, out *[8][]uint64, thresh uint64) byte Reqs: [AVX AVX2 SSE2]
Func: varLenWriteAVX512_16 Sig: (in [][16]uint64, out *[16][]uint64, thresh uint64) byte Reqs: [AVX512DQ AVX512F]
Func: varLenWriteAVX2_16 Sig: (in [][16]uint64, out *[16][]uint64, thresh uint64) byte Reqs: [AVX AVX2 SSE2]
Func: varLenWriteAVX512_24 Sig: (in [][24]uint64, out *[24][]uint64, thresh uint64) byte Reqs: [AVX512DQ AVX512F]
Func: varLenWriteAVX512_32 Sig: (in [][32]uint64, out *[32][]uint64, thresh uint64) byte Reqs: [AVX512DQ AVX512F]
Func: varLenWriteAVX512_48 Sig: (in [][48]uint64, out *[48][]uint64, thresh uint64) byte Reqs: [AVX512DQ AVX512F]
Func: varLenWriteAVX512_64 Sig: (in [][64]uint64, out *[64][]uint64, thresh uint64) byte Reqs: [AVX512DQ AVX512F]
BTW, a much simpler way to enable all of this would be to just export the global context variable and let users go to town, buyer beware style. I suppose that's possible today by eschewing everything in build/global.go
and setting everything up manually, but that's a pretty big inconvenience just to get at some basic info about the generated functions.
After sleeping on this, I've spent a bit of time this morning trying out another approach to this issue.
What I'm really trying to do here is gain hooks into (currently) internal Avo state that is needed to generate certain kinds of support code (runtime codepath selection, test coverage, automated benchmarking, etc.). My recent comments above articulate two possible approaches:
The third way I've just prototyped is to add an API call to register new file "Printers" using the existing internal hooks.
In global.go:
// AddPrinter registers a custom printer
func AddPrinter(flag, desc string, pB printer.Builder, dflt io.WriteCloser) {
pV := newPrinterValue(pB, dflt)
flagSet.Var(pV, flag, desc)
flags.printers = append(flags.printers, pV)
}
Then in my code I can write, e.g.
type myGenerator struct {
cfg printer.Config
printer.Generator
}
// NewMyGenerator constructs a printer for writing a function comments file.
func NewMyGenerator(cfg printer.Config) printer.Printer {
return &myGenerator{cfg: cfg}
}
func (gen *myGenerator) Print(f *ir.File) ([]byte, error) {
gen.Comment(gen.cfg.GeneratedWarning())
gen.NL()
gen.Printf("package %s\n", gen.cfg.Pkg)
for _, val := range f.Functions() {
gen.Comment(fmt.Sprintf("Func: %s \t Sig: %s \t Reqs: %v\n", val.Name, val.Signature, val.ISA))
}
return gen.Result()
}
And in main()
:
AddPrinter("myfile", "produce file enumerating generated functions in comments", NewMyGenerator, nil)
Generate()
Which when run with flag -myfile woot.go
produces:
woot.go
// Code generated by command: go run generate_var_len_write.go -out var_len_write_amd64.s -stubs var_len_write_amd64.go -pkg prototype -myfile woot.go. DO NOT EDIT.
package prototype
// Func: varLenWriteAVX512_4 Sig: (in [][4]uint64, out *[4][]uint64, thresh uint64) byte Reqs: [AVX2 AVX512DQ AVX512F AVX512VL]
// Func: varLenWriteAVX2_4 Sig: (in [][4]uint64, out *[4][]uint64, thresh uint64) byte Reqs: [AVX AVX2 SSE2]
// Func: varLenWriteAVX512_8 Sig: (in [][8]uint64, out *[8][]uint64, thresh uint64) byte Reqs: [AVX512DQ AVX512F]
// Func: varLenWriteAVX2_8 Sig: (in [][8]uint64, out *[8][]uint64, thresh uint64) byte Reqs: [AVX AVX2 SSE2]
// Func: varLenWriteAVX512_16 Sig: (in [][16]uint64, out *[16][]uint64, thresh uint64) byte Reqs: [AVX512DQ AVX512F]
// Func: varLenWriteAVX2_16 Sig: (in [][16]uint64, out *[16][]uint64, thresh uint64) byte Reqs: [AVX AVX2 SSE2]
// Func: varLenWriteAVX512_24 Sig: (in [][24]uint64, out *[24][]uint64, thresh uint64) byte Reqs: [AVX512DQ AVX512F]
// Func: varLenWriteAVX512_32 Sig: (in [][32]uint64, out *[32][]uint64, thresh uint64) byte Reqs: [AVX512DQ AVX512F]
// Func: varLenWriteAVX512_48 Sig: (in [][48]uint64, out *[48][]uint64, thresh uint64) byte Reqs: [AVX512DQ AVX512F]
// Func: varLenWriteAVX512_64 Sig: (in [][64]uint64, out *[64][]uint64, thresh uint64) byte Reqs: [AVX512DQ AVX512F]
That was way simpler than I thought it would be, and has the benefit of maybe preventing a lot of wheel reinvention around code generation.
The principle drawbacks are:
internal/prnt/printer.go
into the exported printer
package. That was extremely simple and "just works", but of course that adds a substantial new public API.My takeaway is that I think this AddPrinter
approach is the most congruent. But it very much needs to be positioned in the API as an internal "plugin" interface with different compatibility guarantees around user supplied printers and use of the formerly internal Generator
API.
Thoughts?
@vsivsi suggested that
avo
could generate helpers for selecting function implementations based on runtime CPU feature checks (see https://github.com/mmcloughlin/avo/issues/20#issuecomment-767259226).This seems like a great idea but I think there are some questions about the details.
At a minimum,
avo
could generate boolean variables for each function indicating whether they are supported. This would be fairly easy:avo
already generates a comment for each function showing which ISAs it needs, and this would be enough to generate a boolean based on the constants inx/sys/cpu
.Generating runtime dispatch or function selection code might take a bit more thought, but also sounds doable.
Creating this issue for further discussion.