From 58565b42cc9bae78c0f79fe989ecb85e95a4e0d1 Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Fri, 6 Aug 2021 15:49:02 +0200 Subject: [PATCH] compiler: move LLVM math builtin support into the compiler This simplifies src/runtime/math.go, which I eventually want to remove entirely by moving the given functionality into the compiler. --- compiler/compiler.go | 7 ++- compiler/compiler_test.go | 2 + compiler/intrinsics.go | 54 +++++++++++++++++++ compiler/testdata/intrinsics-cortex-m-qemu.ll | 27 ++++++++++ compiler/testdata/intrinsics-wasm.ll | 31 +++++++++++ compiler/testdata/intrinsics.go | 14 +++++ src/runtime/math.go | 40 ++------------ 7 files changed, 138 insertions(+), 37 deletions(-) create mode 100644 compiler/testdata/intrinsics-cortex-m-qemu.ll create mode 100644 compiler/testdata/intrinsics-wasm.ll create mode 100644 compiler/testdata/intrinsics.go diff --git a/compiler/compiler.go b/compiler/compiler.go index 7841cd65..94bbc1af 100644 --- a/compiler/compiler.go +++ b/compiler/compiler.go @@ -23,7 +23,7 @@ import ( // Version of the compiler pacakge. Must be incremented each time the compiler // package changes in a way that affects the generated LLVM module. // This version is independent of the TinyGo version number. -const Version = 12 // last change: implement syscall.rawSyscallNoError +const Version = 13 // last change: implement LLVM math builtins in the compiler func init() { llvm.InitializeAllTargets() @@ -1298,6 +1298,11 @@ func (b *builder) createFunctionCall(instr *ssa.CallCommon) (llvm.Value, error) return b.createMemoryCopyCall(fn, instr.Args) case name == "runtime.memzero": return b.createMemoryZeroCall(instr.Args) + case name == "math.Ceil" || name == "math.Floor" || name == "math.Sqrt" || name == "math.Trunc": + result, ok := b.createMathOp(instr) + if ok { + return result, nil + } case name == "device.Asm" || name == "device/arm.Asm" || name == "device/arm64.Asm" || name == "device/avr.Asm" || name == "device/riscv.Asm": return b.createInlineAsm(instr.Args) case name == "device.AsmFull" || name == "device/arm.AsmFull" || name == "device/arm64.AsmFull" || name == "device/avr.AsmFull" || name == "device/riscv.AsmFull": diff --git a/compiler/compiler_test.go b/compiler/compiler_test.go index 45d5b8c3..6db5ebb3 100644 --- a/compiler/compiler_test.go +++ b/compiler/compiler_test.go @@ -48,6 +48,8 @@ func TestCompiler(t *testing.T) { {"pragma.go", ""}, {"goroutine.go", "wasm"}, {"goroutine.go", "cortex-m-qemu"}, + {"intrinsics.go", "cortex-m-qemu"}, + {"intrinsics.go", "wasm"}, } for _, tc := range tests { diff --git a/compiler/intrinsics.go b/compiler/intrinsics.go index 634e8362..aeab5b11 100644 --- a/compiler/intrinsics.go +++ b/compiler/intrinsics.go @@ -48,3 +48,57 @@ func (b *builder) createMemoryZeroCall(args []ssa.Value) (llvm.Value, error) { b.CreateCall(llvmFn, params, "") return llvm.Value{}, nil } + +var mathToLLVMMapping = map[string]string{ + "math.Sqrt": "llvm.sqrt.f64", + "math.Floor": "llvm.floor.f64", + "math.Ceil": "llvm.ceil.f64", + "math.Trunc": "llvm.trunc.f64", +} + +// createMathOp tries to lower the given call as a LLVM math intrinsic, if +// possible. It returns the call result if possible, and a boolean whether it +// succeeded. If it doesn't succeed, the architecture doesn't support the given +// intrinsic. +func (b *builder) createMathOp(call *ssa.CallCommon) (llvm.Value, bool) { + // Check whether this intrinsic is supported on the given GOARCH. + // If it is unsupported, this can have two reasons: + // + // 1. LLVM can expand the intrinsic inline (using float instructions), but + // the result doesn't pass the tests of the math package. + // 2. LLVM cannot expand the intrinsic inline, will therefore lower it as a + // libm function call, but the libm function call also fails the math + // package tests. + // + // Whatever the implementation, it must pass the tests in the math package + // so unfortunately only the below intrinsic+architecture combinations are + // supported. + name := call.StaticCallee().RelString(nil) + switch name { + case "math.Ceil", "math.Floor", "math.Trunc": + if b.GOARCH != "wasm" && b.GOARCH != "arm64" { + return llvm.Value{}, false + } + case "math.Sqrt": + if b.GOARCH != "wasm" && b.GOARCH != "amd64" && b.GOARCH != "386" { + return llvm.Value{}, false + } + default: + return llvm.Value{}, false // only the above functions are supported. + } + + llvmFn := b.mod.NamedFunction(mathToLLVMMapping[name]) + if llvmFn.IsNil() { + // The intrinsic doesn't exist yet, so declare it. + // At the moment, all supported intrinsics have the form "double + // foo(double %x)" so we can hardcode the signature here. + llvmType := llvm.FunctionType(b.ctx.DoubleType(), []llvm.Type{b.ctx.DoubleType()}, false) + llvmFn = llvm.AddFunction(b.mod, mathToLLVMMapping[name], llvmType) + } + // Create a call to the intrinsic. + args := make([]llvm.Value, len(call.Args)) + for i, arg := range call.Args { + args[i] = b.getValue(arg) + } + return b.CreateCall(llvmFn, args, ""), true +} diff --git a/compiler/testdata/intrinsics-cortex-m-qemu.ll b/compiler/testdata/intrinsics-cortex-m-qemu.ll new file mode 100644 index 00000000..e3e7580c --- /dev/null +++ b/compiler/testdata/intrinsics-cortex-m-qemu.ll @@ -0,0 +1,27 @@ +; ModuleID = 'intrinsics.go' +source_filename = "intrinsics.go" +target datalayout = "e-m:e-p:32:32-Fi8-i64:64-v128:64:128-a:0:32-n32-S64" +target triple = "armv7m-none-eabi" + +declare noalias nonnull i8* @runtime.alloc(i32, i8*, i8*) + +define hidden void @main.init(i8* %context, i8* %parentHandle) unnamed_addr { +entry: + ret void +} + +define hidden double @main.mySqrt(double %x, i8* %context, i8* %parentHandle) unnamed_addr { +entry: + %0 = call double @math.Sqrt(double %x, i8* undef, i8* undef) + ret double %0 +} + +declare double @math.Sqrt(double, i8*, i8*) + +define hidden double @main.myTrunc(double %x, i8* %context, i8* %parentHandle) unnamed_addr { +entry: + %0 = call double @math.Trunc(double %x, i8* undef, i8* undef) + ret double %0 +} + +declare double @math.Trunc(double, i8*, i8*) diff --git a/compiler/testdata/intrinsics-wasm.ll b/compiler/testdata/intrinsics-wasm.ll new file mode 100644 index 00000000..433b0a7e --- /dev/null +++ b/compiler/testdata/intrinsics-wasm.ll @@ -0,0 +1,31 @@ +; ModuleID = 'intrinsics.go' +source_filename = "intrinsics.go" +target datalayout = "e-m:e-p:32:32-i64:64-n32:64-S128" +target triple = "wasm32--wasi" + +declare noalias nonnull i8* @runtime.alloc(i32, i8*, i8*) + +define hidden void @main.init(i8* %context, i8* %parentHandle) unnamed_addr { +entry: + ret void +} + +define hidden double @main.mySqrt(double %x, i8* %context, i8* %parentHandle) unnamed_addr { +entry: + %0 = call double @llvm.sqrt.f64(double %x) + ret double %0 +} + +; Function Attrs: nounwind readnone speculatable willreturn +declare double @llvm.sqrt.f64(double) #0 + +define hidden double @main.myTrunc(double %x, i8* %context, i8* %parentHandle) unnamed_addr { +entry: + %0 = call double @llvm.trunc.f64(double %x) + ret double %0 +} + +; Function Attrs: nounwind readnone speculatable willreturn +declare double @llvm.trunc.f64(double) #0 + +attributes #0 = { nounwind readnone speculatable willreturn } diff --git a/compiler/testdata/intrinsics.go b/compiler/testdata/intrinsics.go new file mode 100644 index 00000000..fdca446f --- /dev/null +++ b/compiler/testdata/intrinsics.go @@ -0,0 +1,14 @@ +package main + +// Test how intrinsics are lowered: either as regular calls to the math +// functions or as LLVM builtins (such as llvm.sqrt.f64). + +import "math" + +func mySqrt(x float64) float64 { + return math.Sqrt(x) +} + +func myTrunc(x float64) float64 { + return math.Trunc(x) +} diff --git a/src/runtime/math.go b/src/runtime/math.go index f1617385..73af1591 100644 --- a/src/runtime/math.go +++ b/src/runtime/math.go @@ -56,15 +56,7 @@ func math_Cbrt(x float64) float64 { return math_cbrt(x) } func math_cbrt(x float64) float64 //go:linkname math_Ceil math.Ceil -func math_Ceil(x float64) float64 { - if GOARCH == "arm64" || GOARCH == "wasm" { - return llvm_ceil(x) - } - return math_ceil(x) -} - -//export llvm.ceil.f64 -func llvm_ceil(x float64) float64 +func math_Ceil(x float64) float64 { return math_ceil(x) } //go:linkname math_ceil math.ceil func math_ceil(x float64) float64 @@ -112,15 +104,7 @@ func math_Exp2(x float64) float64 { return math_exp2(x) } func math_exp2(x float64) float64 //go:linkname math_Floor math.Floor -func math_Floor(x float64) float64 { - if GOARCH == "arm64" || GOARCH == "wasm" { - return llvm_floor(x) - } - return math_floor(x) -} - -//export llvm.floor.f64 -func llvm_floor(x float64) float64 +func math_Floor(x float64) float64 { return math_floor(x) } //go:linkname math_floor math.floor func math_floor(x float64) float64 @@ -216,15 +200,7 @@ func math_Sinh(x float64) float64 { return math_sinh(x) } func math_sinh(x float64) float64 //go:linkname math_Sqrt math.Sqrt -func math_Sqrt(x float64) float64 { - if GOARCH == "386" || GOARCH == "amd64" || GOARCH == "wasm" { - return llvm_sqrt(x) - } - return math_sqrt(x) -} - -//export llvm.sqrt.f64 -func llvm_sqrt(x float64) float64 +func math_Sqrt(x float64) float64 { return math_sqrt(x) } //go:linkname math_sqrt math.sqrt func math_sqrt(x float64) float64 @@ -242,15 +218,7 @@ func math_Tanh(x float64) float64 { return math_tanh(x) } func math_tanh(x float64) float64 //go:linkname math_Trunc math.Trunc -func math_Trunc(x float64) float64 { - if GOARCH == "arm64" || GOARCH == "wasm" { - return llvm_trunc(x) - } - return math_trunc(x) -} - -//export llvm.trunc.f64 -func llvm_trunc(x float64) float64 +func math_Trunc(x float64) float64 { return math_trunc(x) } //go:linkname math_trunc math.trunc func math_trunc(x float64) float64