mirror of
https://github.com/tinygo-org/tinygo.git
synced 2026-08-13 07:23:39 +00:00
interp: bail out of loops that iterate too many times (#5395)
* interp: bail out of loops that iterate too many times The existing loop guard (errLoopUnrolled) only fires when a loop body emits runtime instructions. Loops that are fully evaluable at compile time, such as inserting thousands of entries into a map, were not caught and could hang the compiler. Add a per-basic-block iteration counter that triggers a recoverable error (errLoopTooLong) when any block is entered more than 1000 times in a single function call. This defers the init function to runtime, which is the same behavior as other interp bailouts. Profiling showed that 83% of CPU time was spent in GC, caused by allocation pressure from the interp memory cloning on each map mutation. The iteration limit avoids this entirely by bailing out before the quadratic cost becomes significant. Performance on the reproducer from #2090 (map init with strconv.Itoa): entries before after 5,000 7.4s 2.1s 10,000 17.5s 2.1s 20,000 48.0s 2.8s 65,536 >180s (OOM) 3.2s * Add -interp-loop-limit
This commit is contained in:
+32
-30
@@ -19,35 +19,37 @@ const checks = true
|
||||
|
||||
// runner contains all state related to one interp run.
|
||||
type runner struct {
|
||||
mod llvm.Module
|
||||
targetData llvm.TargetData
|
||||
builder llvm.Builder
|
||||
pointerSize uint32 // cached pointer size from the TargetData
|
||||
dataPtrType llvm.Type // often used type so created in advance
|
||||
uintptrType llvm.Type // equivalent to uintptr in Go
|
||||
maxAlign int // maximum alignment of an object, alignment of runtime.alloc() result
|
||||
byteOrder binary.ByteOrder // big-endian or little-endian
|
||||
debug bool // log debug messages
|
||||
pkgName string // package name of the currently executing package
|
||||
functionCache map[llvm.Value]*function // cache of compiled functions
|
||||
objects []object // slice of objects in memory
|
||||
globals map[llvm.Value]int // map from global to index in objects slice
|
||||
start time.Time
|
||||
timeout time.Duration
|
||||
callsExecuted uint64
|
||||
mod llvm.Module
|
||||
targetData llvm.TargetData
|
||||
builder llvm.Builder
|
||||
pointerSize uint32 // cached pointer size from the TargetData
|
||||
dataPtrType llvm.Type // often used type so created in advance
|
||||
uintptrType llvm.Type // equivalent to uintptr in Go
|
||||
maxAlign int // maximum alignment of an object, alignment of runtime.alloc() result
|
||||
byteOrder binary.ByteOrder // big-endian or little-endian
|
||||
debug bool // log debug messages
|
||||
pkgName string // package name of the currently executing package
|
||||
functionCache map[llvm.Value]*function // cache of compiled functions
|
||||
objects []object // slice of objects in memory
|
||||
globals map[llvm.Value]int // map from global to index in objects slice
|
||||
start time.Time
|
||||
timeout time.Duration
|
||||
maxLoopIterations int
|
||||
callsExecuted uint64
|
||||
}
|
||||
|
||||
func newRunner(mod llvm.Module, timeout time.Duration, debug bool) *runner {
|
||||
func newRunner(mod llvm.Module, timeout time.Duration, maxLoopIterations int, debug bool) *runner {
|
||||
r := runner{
|
||||
mod: mod,
|
||||
targetData: llvm.NewTargetData(mod.DataLayout()),
|
||||
byteOrder: llvmutil.ByteOrder(mod.Target()),
|
||||
debug: debug,
|
||||
functionCache: make(map[llvm.Value]*function),
|
||||
objects: []object{{}},
|
||||
globals: make(map[llvm.Value]int),
|
||||
start: time.Now(),
|
||||
timeout: timeout,
|
||||
mod: mod,
|
||||
targetData: llvm.NewTargetData(mod.DataLayout()),
|
||||
byteOrder: llvmutil.ByteOrder(mod.Target()),
|
||||
debug: debug,
|
||||
functionCache: make(map[llvm.Value]*function),
|
||||
objects: []object{{}},
|
||||
globals: make(map[llvm.Value]int),
|
||||
start: time.Now(),
|
||||
timeout: timeout,
|
||||
maxLoopIterations: maxLoopIterations,
|
||||
}
|
||||
r.pointerSize = uint32(r.targetData.PointerSize())
|
||||
r.dataPtrType = llvm.PointerType(mod.Context().Int8Type(), 0)
|
||||
@@ -64,8 +66,8 @@ func (r *runner) dispose() {
|
||||
|
||||
// Run evaluates runtime.initAll function as much as possible at compile time.
|
||||
// Set debug to true if it should print output while running.
|
||||
func Run(mod llvm.Module, timeout time.Duration, debug bool) error {
|
||||
r := newRunner(mod, timeout, debug)
|
||||
func Run(mod llvm.Module, timeout time.Duration, maxLoopIterations int, debug bool) error {
|
||||
r := newRunner(mod, timeout, maxLoopIterations, debug)
|
||||
defer r.dispose()
|
||||
|
||||
initAll := mod.NamedFunction("runtime.initAll")
|
||||
@@ -204,10 +206,10 @@ func Run(mod llvm.Module, timeout time.Duration, debug bool) error {
|
||||
|
||||
// RunFunc evaluates a single package initializer at compile time.
|
||||
// Set debug to true if it should print output while running.
|
||||
func RunFunc(fn llvm.Value, timeout time.Duration, debug bool) error {
|
||||
func RunFunc(fn llvm.Value, timeout time.Duration, maxLoopIterations int, debug bool) error {
|
||||
// Create and initialize *runner object.
|
||||
mod := fn.GlobalParent()
|
||||
r := newRunner(mod, timeout, debug)
|
||||
r := newRunner(mod, timeout, maxLoopIterations, debug)
|
||||
defer r.dispose()
|
||||
initName := fn.Name()
|
||||
if !strings.HasSuffix(initName, ".init") {
|
||||
|
||||
Reference in New Issue
Block a user