Files
wazero/internal/engine/compiler/compiler_value_location.go
2023-04-10 14:58:47 +09:00

426 lines
14 KiB
Go

package compiler
import (
"fmt"
"strings"
"github.com/tetratelabs/wazero/internal/asm"
"github.com/tetratelabs/wazero/internal/wasm"
)
var (
// unreservedGeneralPurposeRegisters contains unreserved general purpose registers of integer type.
unreservedGeneralPurposeRegisters []asm.Register
// unreservedVectorRegisters contains unreserved vector registers.
unreservedVectorRegisters []asm.Register
)
func isGeneralPurposeRegister(r asm.Register) bool {
return unreservedGeneralPurposeRegisters[0] <= r && r <= unreservedGeneralPurposeRegisters[len(unreservedGeneralPurposeRegisters)-1]
}
func isVectorRegister(r asm.Register) bool {
return unreservedVectorRegisters[0] <= r && r <= unreservedVectorRegisters[len(unreservedVectorRegisters)-1]
}
// runtimeValueLocation corresponds to each variable pushed onto the wazeroir (virtual) stack,
// and it has the information about where it exists in the physical machine.
// It might exist in registers, or maybe on in the non-virtual physical stack allocated in memory.
type runtimeValueLocation struct {
valueType runtimeValueType
// register is set to asm.NilRegister if the value is stored in the memory stack.
register asm.Register
// conditionalRegister is set to conditionalRegisterStateUnset if the value is not on the conditional register.
conditionalRegister asm.ConditionalRegisterState
// stackPointer is the location of this value in the memory stack at runtime,
stackPointer uint64
}
func (v *runtimeValueLocation) getRegisterType() (ret registerType) {
switch v.valueType {
case runtimeValueTypeI32, runtimeValueTypeI64:
ret = registerTypeGeneralPurpose
case runtimeValueTypeF32, runtimeValueTypeF64,
runtimeValueTypeV128Lo, runtimeValueTypeV128Hi:
ret = registerTypeVector
default:
panic("BUG")
}
return
}
type runtimeValueType byte
const (
runtimeValueTypeNone runtimeValueType = iota
runtimeValueTypeI32
runtimeValueTypeI64
runtimeValueTypeF32
runtimeValueTypeF64
runtimeValueTypeV128Lo
runtimeValueTypeV128Hi
)
func (r runtimeValueType) String() (ret string) {
switch r {
case runtimeValueTypeI32:
ret = "i32"
case runtimeValueTypeI64:
ret = "i64"
case runtimeValueTypeF32:
ret = "f32"
case runtimeValueTypeF64:
ret = "f64"
case runtimeValueTypeV128Lo:
ret = "v128.lo"
case runtimeValueTypeV128Hi:
ret = "v128.hi"
}
return
}
func (v *runtimeValueLocation) setRegister(reg asm.Register) {
v.register = reg
v.conditionalRegister = asm.ConditionalRegisterStateUnset
}
func (v *runtimeValueLocation) onRegister() bool {
return v.register != asm.NilRegister && v.conditionalRegister == asm.ConditionalRegisterStateUnset
}
func (v *runtimeValueLocation) onStack() bool {
return v.register == asm.NilRegister && v.conditionalRegister == asm.ConditionalRegisterStateUnset
}
func (v *runtimeValueLocation) onConditionalRegister() bool {
return v.conditionalRegister != asm.ConditionalRegisterStateUnset
}
func (v *runtimeValueLocation) String() string {
var location string
if v.onStack() {
location = fmt.Sprintf("stack(%d)", v.stackPointer)
} else if v.onConditionalRegister() {
location = fmt.Sprintf("conditional(%d)", v.conditionalRegister)
} else if v.onRegister() {
location = fmt.Sprintf("register(%s)", registerNameFn(v.register))
}
return fmt.Sprintf("{type=%s,location=%s}", v.valueType, location)
}
func newRuntimeValueLocationStack() runtimeValueLocationStack {
return runtimeValueLocationStack{
unreservedVectorRegisters: unreservedVectorRegisters,
unreservedGeneralPurposeRegisters: unreservedGeneralPurposeRegisters,
}
}
// runtimeValueLocationStack represents the wazeroir virtual stack
// where each item holds the location information about where it exists
// on the physical machine at runtime.
//
// Notably this is only used in the compilation phase, not runtime,
// and we change the state of this struct at every wazeroir operation we compile.
// In this way, we can see where the operands of an operation (for example,
// two variables for wazeroir add operation.) exist and check the necessity for
// moving the variable to registers to perform actual CPU instruction
// to achieve wazeroir's add operation.
type runtimeValueLocationStack struct {
// stack holds all the variables.
stack []runtimeValueLocation
// sp is the current stack pointer.
sp uint64
// usedRegisters is the bit map to track the used registers.
usedRegisters usedRegistersMask
// stackPointerCeil tracks max(.sp) across the lifespan of this struct.
stackPointerCeil uint64
// unreservedGeneralPurposeRegisters and unreservedVectorRegisters hold
// architecture dependent unreserved register list.
unreservedGeneralPurposeRegisters, unreservedVectorRegisters []asm.Register
}
func (v *runtimeValueLocationStack) reset() {
stack := v.stack[:0]
*v = runtimeValueLocationStack{
unreservedVectorRegisters: unreservedVectorRegisters,
unreservedGeneralPurposeRegisters: unreservedGeneralPurposeRegisters,
stack: stack,
}
}
func (v *runtimeValueLocationStack) String() string {
var stackStr []string
for i := uint64(0); i < v.sp; i++ {
stackStr = append(stackStr, v.stack[i].String())
}
usedRegisters := v.usedRegisters.list()
return fmt.Sprintf("sp=%d, stack=[%s], used_registers=[%s]", v.sp, strings.Join(stackStr, ","), strings.Join(usedRegisters, ","))
}
// cloneFrom clones the values on `from` into self except for the slice of .stack field.
// The content on .stack will be copied from the origin to self, and grow the underlying slice
// if necessary.
func (v *runtimeValueLocationStack) cloneFrom(from runtimeValueLocationStack) {
// Assigns the same values for fields except for the stack which we want to reuse.
prev := v.stack
*v = from
v.stack = prev[:cap(prev)] // Expand the length to the capacity so that we can minimize "diff" below.
// Copy the content in the stack.
if diff := int(from.sp) - len(v.stack); diff > 0 {
v.stack = append(v.stack, make([]runtimeValueLocation, diff)...)
}
copy(v.stack, from.stack[:from.sp])
}
// pushRuntimeValueLocationOnRegister creates a new runtimeValueLocation with a given register and pushes onto
// the location stack.
func (v *runtimeValueLocationStack) pushRuntimeValueLocationOnRegister(reg asm.Register, vt runtimeValueType) (loc *runtimeValueLocation) {
loc = v.push(reg, asm.ConditionalRegisterStateUnset)
loc.valueType = vt
return
}
// pushRuntimeValueLocationOnRegister creates a new runtimeValueLocation and pushes onto the location stack.
func (v *runtimeValueLocationStack) pushRuntimeValueLocationOnStack() (loc *runtimeValueLocation) {
loc = v.push(asm.NilRegister, asm.ConditionalRegisterStateUnset)
loc.valueType = runtimeValueTypeNone
return
}
// pushRuntimeValueLocationOnRegister creates a new runtimeValueLocation with a given conditional register state
// and pushes onto the location stack.
func (v *runtimeValueLocationStack) pushRuntimeValueLocationOnConditionalRegister(state asm.ConditionalRegisterState) (loc *runtimeValueLocation) {
loc = v.push(asm.NilRegister, state)
loc.valueType = runtimeValueTypeI32
return
}
// push a runtimeValueLocation onto the stack.
func (v *runtimeValueLocationStack) push(reg asm.Register, conditionalRegister asm.ConditionalRegisterState) (ret *runtimeValueLocation) {
if v.sp >= uint64(len(v.stack)) {
// This case we need to grow the stack capacity by appending the item,
// rather than indexing.
v.stack = append(v.stack, runtimeValueLocation{})
}
ret = &v.stack[v.sp]
ret.register, ret.conditionalRegister, ret.stackPointer = reg, conditionalRegister, v.sp
v.sp++
// stackPointerCeil must be set after sp is incremented since
// we skip the stack grow if len(stack) >= basePointer+stackPointerCeil.
if v.sp > v.stackPointerCeil {
v.stackPointerCeil = v.sp
}
return
}
func (v *runtimeValueLocationStack) pop() (loc *runtimeValueLocation) {
v.sp--
loc = &v.stack[v.sp]
return
}
func (v *runtimeValueLocationStack) popV128() (loc *runtimeValueLocation) {
v.sp -= 2
loc = &v.stack[v.sp]
return
}
func (v *runtimeValueLocationStack) peek() (loc *runtimeValueLocation) {
loc = &v.stack[v.sp-1]
return
}
func (v *runtimeValueLocationStack) releaseRegister(loc *runtimeValueLocation) {
v.markRegisterUnused(loc.register)
loc.register = asm.NilRegister
loc.conditionalRegister = asm.ConditionalRegisterStateUnset
}
func (v *runtimeValueLocationStack) markRegisterUnused(regs ...asm.Register) {
for _, reg := range regs {
v.usedRegisters.remove(reg)
}
}
func (v *runtimeValueLocationStack) markRegisterUsed(regs ...asm.Register) {
for _, reg := range regs {
v.usedRegisters.add(reg)
}
}
type registerType byte
const (
registerTypeGeneralPurpose registerType = iota
// registerTypeVector represents a vector register which can be used for either scalar float
// operation or SIMD vector operation depending on the instruction by which the register is used.
//
// Note: In normal assembly language, scalar float and vector register have different notations as
// Vn is for vectors and Qn is for scalar floats on arm64 for example. But on physical hardware,
// they are placed on the same locations. (Qn means the lower 64-bit of Vn vector register on arm64).
//
// In wazero, for the sake of simplicity in the register allocation, we intentionally conflate these two types
// and delegate the decision to the assembler which is aware of the instruction types for which these registers are used.
registerTypeVector
)
func (tp registerType) String() (ret string) {
switch tp {
case registerTypeGeneralPurpose:
ret = "int"
case registerTypeVector:
ret = "vector"
}
return
}
// takeFreeRegister searches for unused registers. Any found are marked used and returned.
func (v *runtimeValueLocationStack) takeFreeRegister(tp registerType) (reg asm.Register, found bool) {
var targetRegs []asm.Register
switch tp {
case registerTypeVector:
targetRegs = v.unreservedVectorRegisters
case registerTypeGeneralPurpose:
targetRegs = v.unreservedGeneralPurposeRegisters
}
for _, candidate := range targetRegs {
if v.usedRegisters.exist(candidate) {
continue
}
return candidate, true
}
return 0, false
}
// Search through the stack, and steal the register from the last used
// variable on the stack.
func (v *runtimeValueLocationStack) takeStealTargetFromUsedRegister(tp registerType) (*runtimeValueLocation, bool) {
for i := uint64(0); i < v.sp; i++ {
loc := &v.stack[i]
if loc.onRegister() {
switch tp {
case registerTypeVector:
if loc.valueType == runtimeValueTypeV128Hi {
panic("BUG: V128Hi must be above the corresponding V128Lo")
}
if isVectorRegister(loc.register) {
return loc, true
}
case registerTypeGeneralPurpose:
if isGeneralPurposeRegister(loc.register) {
return loc, true
}
}
}
}
return nil, false
}
// init sets up the runtimeValueLocationStack which reflects the state of
// the stack at the beginning of the function.
//
// See the diagram in callEngine.stack.
func (v *runtimeValueLocationStack) init(sig *wasm.FunctionType) {
for _, t := range sig.Params {
loc := v.pushRuntimeValueLocationOnStack()
switch t {
case wasm.ValueTypeI32:
loc.valueType = runtimeValueTypeI32
case wasm.ValueTypeI64, wasm.ValueTypeFuncref, wasm.ValueTypeExternref:
loc.valueType = runtimeValueTypeI64
case wasm.ValueTypeF32:
loc.valueType = runtimeValueTypeF32
case wasm.ValueTypeF64:
loc.valueType = runtimeValueTypeF64
case wasm.ValueTypeV128:
loc.valueType = runtimeValueTypeV128Lo
hi := v.pushRuntimeValueLocationOnStack()
hi.valueType = runtimeValueTypeV128Hi
default:
panic("BUG")
}
}
// If the len(results) > len(args), the slots for all results are reserved after
// arguments, so we reflect that into the location stack.
for i := 0; i < sig.ResultNumInUint64-sig.ParamNumInUint64; i++ {
_ = v.pushRuntimeValueLocationOnStack()
}
// Then push the control frame fields.
for i := 0; i < callFrameDataSizeInUint64; i++ {
loc := v.pushRuntimeValueLocationOnStack()
loc.valueType = runtimeValueTypeI64
}
}
// getCallFrameLocations returns each field of callFrame's runtime location.
//
// See the diagram in callEngine.stack.
func (v *runtimeValueLocationStack) getCallFrameLocations(sig *wasm.FunctionType) (
returnAddress, callerStackBasePointerInBytes, callerFunction *runtimeValueLocation,
) {
offset := callFrameOffset(sig)
return &v.stack[offset], &v.stack[offset+1], &v.stack[offset+2]
}
// pushCallFrame pushes a call frame's runtime locations onto the stack assuming that
// the function call parameters are already pushed there.
//
// See the diagram in callEngine.stack.
func (v *runtimeValueLocationStack) pushCallFrame(callTargetFunctionType *wasm.FunctionType) (
returnAddress, callerStackBasePointerInBytes, callerFunction *runtimeValueLocation,
) {
// If len(results) > len(args), we reserve the slots for the results below the call frame.
reservedSlotsBeforeCallFrame := callTargetFunctionType.ResultNumInUint64 - callTargetFunctionType.ParamNumInUint64
for i := 0; i < reservedSlotsBeforeCallFrame; i++ {
v.pushRuntimeValueLocationOnStack()
}
// Push the runtime location for each field of callFrame struct. Note that each of them has
// uint64 type, and therefore must be treated as runtimeValueTypeI64.
// callFrame.returnAddress
returnAddress = v.pushRuntimeValueLocationOnStack()
returnAddress.valueType = runtimeValueTypeI64
// callFrame.returnStackBasePointerInBytes
callerStackBasePointerInBytes = v.pushRuntimeValueLocationOnStack()
callerStackBasePointerInBytes.valueType = runtimeValueTypeI64
// callFrame.function
callerFunction = v.pushRuntimeValueLocationOnStack()
callerFunction.valueType = runtimeValueTypeI64
return
}
// usedRegistersMask tracks the used registers in its bits.
type usedRegistersMask uint64
// add adds the given `r` to the mask.
func (u *usedRegistersMask) add(r asm.Register) {
*u = *u | (1 << registerMaskShift(r))
}
// remove drops the given `r` from the mask.
func (u *usedRegistersMask) remove(r asm.Register) {
*u = *u & ^(1 << registerMaskShift(r))
}
// exist returns true if the given `r` is used.
func (u *usedRegistersMask) exist(r asm.Register) bool {
shift := registerMaskShift(r)
return (*u & (1 << shift)) > 0
}
// list returns the list of debug string of used registers.
// Only used for debugging and testing.
func (u *usedRegistersMask) list() (ret []string) {
mask := *u
for i := 0; i < 64; i++ {
if mask&(1<<i) > 0 {
ret = append(ret, registerNameFn(registerFromMaskShift(i)))
}
}
return
}