Skip to content

Commit

Permalink
runtime: use uint32 for the channel state and select index
Browse files Browse the repository at this point in the history
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).
  • Loading branch information
aykevl authored and deadprogram committed Nov 27, 2024
1 parent ee6fcd7 commit 392a709
Show file tree
Hide file tree
Showing 3 changed files with 45 additions and 17 deletions.
16 changes: 16 additions & 0 deletions compiler/channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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.
Expand Down
12 changes: 12 additions & 0 deletions src/internal/task/task.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
34 changes: 17 additions & 17 deletions src/runtime/chan.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}
}
Expand All @@ -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
}

Expand Down Expand Up @@ -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
Expand All @@ -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")
}
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -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
}
}
Expand All @@ -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
Expand Down Expand Up @@ -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
}

0 comments on commit 392a709

Please sign in to comment.