From 392a709b77c82bfa98ce669ae6f781cf93835967 Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Wed, 27 Nov 2024 11:15:15 +0100 Subject: [PATCH] runtime: use uint32 for the channel state and select index This uses uint32 instead of uint64. The reason for this is that uint64 atomic operations aren't universally available (especially on 32-bit architectures). We could also use uintptr, but that seems needlessly complicated: it's unlikely real-world programs will use more than a billion select states (2^30). --- compiler/channel.go | 16 ++++++++++++++++ src/internal/task/task.go | 12 ++++++++++++ src/runtime/chan.go | 34 +++++++++++++++++----------------- 3 files changed, 45 insertions(+), 17 deletions(-) diff --git a/compiler/channel.go b/compiler/channel.go index 7e867c278..0ff2ab7f3 100644 --- a/compiler/channel.go +++ b/compiler/channel.go @@ -4,7 +4,9 @@ package compiler // or pseudo-operations that are lowered during goroutine lowering. import ( + "fmt" "go/types" + "math" "github.com/tinygo-org/tinygo/compiler/llvmutil" "golang.org/x/tools/go/ssa" @@ -124,6 +126,20 @@ func (b *builder) createSelect(expr *ssa.Select) llvm.Value { } } + const maxSelectStates = math.MaxUint32 >> 2 + if len(expr.States) > maxSelectStates { + // The runtime code assumes that the number of state must fit in 30 bits + // (so the select index can be stored in a uint32 with two bits reserved + // for other purposes). It seems unlikely that a real program would have + // that many states, but we check for this case anyway to be sure. + // We use a uint32 (and not a uintptr or uint64) to avoid 64-bit atomic + // operations which aren't available everywhere. + b.addError(expr.Pos(), fmt.Sprintf("too many select states: got %d but the maximum supported number is %d", len(expr.States), maxSelectStates)) + + // Continue as usual (we'll generate broken code but the error will + // prevent the compilation to complete). + } + // This code create a (stack-allocated) slice containing all the select // cases and then calls runtime.chanSelect to perform the actual select // statement. diff --git a/src/internal/task/task.go b/src/internal/task/task.go index bce65b582..587c67526 100644 --- a/src/internal/task/task.go +++ b/src/internal/task/task.go @@ -26,6 +26,18 @@ type Task struct { DeferFrame unsafe.Pointer } +// DataUint32 returns the Data field as a uint32. The value is only valid after +// setting it through SetDataUint32. +func (t *Task) DataUint32() uint32 { + return *(*uint32)(unsafe.Pointer(&t.Data)) +} + +// SetDataUint32 updates the uint32 portion of the Data field (which could be +// the first 4 or last 4 bytes depending on the architecture endianness). +func (t *Task) SetDataUint32(val uint32) { + *(*uint32)(unsafe.Pointer(&t.Data)) = val +} + // getGoroutineStackSize is a compiler intrinsic that returns the stack size for // the given function and falls back to the default stack size. It is replaced // with a load from a special section just before codegen. diff --git a/src/runtime/chan.go b/src/runtime/chan.go index c62685700..0d0cbf06a 100644 --- a/src/runtime/chan.go +++ b/src/runtime/chan.go @@ -83,7 +83,7 @@ func (q *chanQueue) push(node *channelOp) { // waiting (for example, when they're part of a select operation) will be // skipped. // This function must be called with interrupts disabled. -func (q *chanQueue) pop(chanOp uint64) *channelOp { +func (q *chanQueue) pop(chanOp uint32) *channelOp { for { if q.first == nil { return nil @@ -96,11 +96,11 @@ func (q *chanQueue) pop(chanOp uint64) *channelOp { // The new value for the 'data' field will be a combination of the // channel operation and the select index. (The select index is 0 for // non-select channel operations). - newDataValue := chanOp | uint64(popped.index<<2) + newDataValue := chanOp | popped.index<<2 // Try to be the first to proceed with this goroutine. - if popped.task.Data == chanOperationWaiting { - popped.task.Data = newDataValue + if popped.task.DataUint32() == chanOperationWaiting { + popped.task.SetDataUint32(newDataValue) return popped } } @@ -123,7 +123,7 @@ func (q *chanQueue) remove(remove *channelOp) { type channelOp struct { next *channelOp task *task.Task - index uintptr // select index, 0 for non-select operation + index uint32 // select index, 0 for non-select operation value unsafe.Pointer // if this is a sender, this is the value to send } @@ -239,7 +239,7 @@ func chanSend(ch *channel, value unsafe.Pointer, op *channelOp) { // Can't proceed. Add us to the list of senders and wait until we're awoken. t := task.Current() - t.Data = chanOperationWaiting + t.SetDataUint32(chanOperationWaiting) op.task = t op.index = 0 op.value = value @@ -251,7 +251,7 @@ func chanSend(ch *channel, value unsafe.Pointer, op *channelOp) { // Check whether the sent happened normally (not because the channel was // closed while sending). - if t.Data == chanOperationClosed { + if t.DataUint32() == chanOperationClosed { // Oops, this channel was closed while sending! runtimePanic("send on closed channel") } @@ -313,7 +313,7 @@ func chanRecv(ch *channel, value unsafe.Pointer, op *channelOp) bool { // until we're awoken. t := task.Current() t.Ptr = value - t.Data = chanOperationWaiting + t.SetDataUint32(chanOperationWaiting) op.task = t op.index = 0 ch.receivers.push(op) @@ -323,7 +323,7 @@ func chanRecv(ch *channel, value unsafe.Pointer, op *channelOp) bool { task.Pause() // Return whether the receive happened from a closed channel. - return t.Data != chanOperationClosed + return t.DataUint32() != chanOperationClosed } // chanClose closes the given channel. If this channel has a receiver or is @@ -375,10 +375,10 @@ func chanClose(ch *channel) { // chanSelect implements blocking or non-blocking select operations. // The 'ops' slice must be set if (and only if) this is a blocking select. -func chanSelect(recvbuf unsafe.Pointer, states []chanSelectState, ops []channelOp) (uintptr, bool) { +func chanSelect(recvbuf unsafe.Pointer, states []chanSelectState, ops []channelOp) (uint32, bool) { mask := interrupt.Disable() - const selectNoIndex = ^uintptr(0) + const selectNoIndex = ^uint32(0) selectIndex := selectNoIndex selectOk := true @@ -393,13 +393,13 @@ func chanSelect(recvbuf unsafe.Pointer, states []chanSelectState, ops []channelO if state.value == nil { // chan receive if received, ok := state.ch.tryRecv(recvbuf); received { - selectIndex = uintptr(i) + selectIndex = uint32(i) selectOk = ok break } } else { // chan send if state.ch.trySend(state.value) { - selectIndex = uintptr(i) + selectIndex = uint32(i) break } } @@ -421,14 +421,14 @@ func chanSelect(recvbuf unsafe.Pointer, states []chanSelectState, ops []channelO // will be able to "take" this select operation. t := task.Current() t.Ptr = recvbuf - t.Data = chanOperationWaiting + t.SetDataUint32(chanOperationWaiting) for i, state := range states { if state.ch == nil { continue } op := &ops[i] op.task = t - op.index = uintptr(i) + op.index = uint32(i) if state.value == nil { // chan receive state.ch.receivers.push(op) } else { // chan send @@ -460,8 +460,8 @@ func chanSelect(recvbuf unsafe.Pointer, states []chanSelectState, ops []channelO } // Pull the return values out of t.Data (which contains two bitfields). - selectIndex = uintptr(t.Data) >> 2 - selectOk = t.Data&chanOperationMask != chanOperationClosed + selectIndex = t.DataUint32() >> 2 + selectOk = t.DataUint32()&chanOperationMask != chanOperationClosed return selectIndex, selectOk }