Repository: tetratelabs/wazero (go)

Issue:
compiler: mmap per module, instead of per function
currently, the machine code is compiled and memory-mapped as executables per-function basis and that is the current bottleneck on compilation performance; The below is the CPU profiling of [without extern cache benchmark](https://github.com/tetratelabs/wazero/blob/v1.0.0-pre.7/internal/integration_test/bench/bench_test.go#L84-L90) and it indicates that 63.7/73.6% = ~80% of `engine.CompileModule` is spent on mmap (for a TinyGo binary).

<img width="677" alt="image" src="https://user-images.githubusercontent.com/13513977/214443782-6e34ed16-6d68-4b06-9394-11fe8cf4ab26.png">


It should be possible to memory-map per module, which should improve the compilation performance better (Actually that is what I am actually doing in the new backend).

Pull request:
compiler: mmap per module instead of per function
This changes the mmap strategy used in the compiler backend.
Previously, we used mmap syscall once per function and allocated the 
executable pages each time. Basically, mmap can only allocate the 
boundary of the page size of the underlying os. Even if the requested 
executable is smaller than the page size, the entire page is marked as 
executable and won't be reused by Go runtime. Therefore, we wasted 
roughly `(len(body)%osPageSize)*function`.

Even though we still need to align each function on 16 bytes boundary
when mmaping per module, the wasted space is much smaller than before.

The following benchmark results shows that this improves the overall 
compilation performance while showing the heap usage increased. 
However, the increased heap usage is totally offset by the hidden wasted
memory page which is not measured by Go's -benchmem.
Actually, when I did the experiments, I observed that roughly 20~30mb are
wasted on arm64 previously which is larger than the increased heap usage
in this result. More importantly, this increased heap usage is a target of GC
and should be ignorable in the long-running program vs the wasted page 
is persistent until the CompiledModule is closed.

Not only the actual compilation time, the result indicates that this could 
improve the overall Go runtime's performance maybe thanks to not abusing
runtime.Finalizer since you can see this improves the subsequent interpreter 
benchmark results.

```
goos: darwin
goarch: arm64
pkg: github.com/tetratelabs/wazero/internal/integration_test/bench
                                   │   old.txt   │              new.txt              │
                                   │   sec/op    │   sec/op     vs base              │
Compilation_sqlite3/compiler-10      183.4m ± 0%   175.9m ± 2%  -4.10% (p=0.001 n=7)
Compilation_sqlite3/interpreter-10   61.59m ± 0%   59.57m ± 0%  -3.29% (p=0.001 n=7)
geomean                              106.3m        102.4m       -3.69%

                                   │   old.txt    │               new.txt               │
                                   │     B/op     │     B/op      vs base               │
Compilation_sqlite3/compiler-10      42.93Mi ± 0%   54.33Mi ± 0%  +26.56% (p=0.001 n=7)
Compilation_sqlite3/interpreter-10   51.75Mi ± 0%   51.75Mi ± 0%   -0.01% (p=0.001 n=7)
geomean                              47.13Mi        53.02Mi       +12.49%

                                   │   old.txt   │              new.txt              │
                                   │  allocs/op  │  allocs/op   vs base              │
Compilation_sqlite3/compiler-10      26.07k ± 0%   26.06k ± 0%       ~ (p=0.149 n=7)
Compilation_sqlite3/interpreter-10   13.90k ± 0%   13.90k ± 0%       ~ (p=0.421 n=7)
geomean                              19.03k        19.03k       -0.02%


goos: linux
goarch: amd64
pkg: github.com/tetratelabs/wazero/internal/integration_test/bench
cpu: AMD Ryzen 9 3950X 16-Core Processor
                                   │   old.txt   │              new.txt               │
                                   │   sec/op    │   sec/op     vs base               │
Compilation_sqlite3/compiler-32      384.4m ± 2%   373.0m ± 4%   -2.97% (p=0.001 n=7)
Compilation_sqlite3/interpreter-32   86.09m ± 4%   65.05m ± 2%  -24.44% (p=0.001 n=7)
geomean                              181.9m        155.8m       -14.38%

                                   │   old.txt    │               new.txt               │
                                   │     B/op     │     B/op      vs base               │
Compilation_sqlite3/compiler-32      49.40Mi ± 0%   59.91Mi ± 0%  +21.29% (p=0.001 n=7)
Compilation_sqlite3/interpreter-32   51.77Mi ± 0%   51.76Mi ± 0%   -0.02% (p=0.001 n=7)
geomean                              50.57Mi        55.69Mi       +10.12%

                                   │   old.txt   │              new.txt              │
                                   │  allocs/op  │  allocs/op   vs base              │
Compilation_sqlite3/compiler-32      28.70k ± 0%   28.70k ± 0%       ~ (p=0.925 n=7)
Compilation_sqlite3/interpreter-32   14.00k ± 0%   14.00k ± 0%  -0.04% (p=0.010 n=7)
geomean                              20.05k        20.04k       -0.02%
```




resolves #1060 

Patch:
diff --git a/internal/engine/compiler/engine.go b/internal/engine/compiler/engine.go
index 7370824b..f7fbe8f8 100644
--- a/internal/engine/compiler/engine.go
+++ b/internal/engine/compiler/engine.go
@@ -29,7 +29,7 @@ type (
 	// engine is a Compiler implementation of wasm.Engine
 	engine struct {
 		enabledFeatures api.CoreFeatures
-		codes           map[wasm.ModuleID][]*code // guarded by mutex.
+		codes           map[wasm.ModuleID]*compiledModule // guarded by mutex.
 		fileCache       filecache.Cache
 		mux             sync.RWMutex
 		// setFinalizer defaults to runtime.SetFinalizer, but overridable for tests.
@@ -137,6 +137,8 @@ type (
 		// stackIterator provides a way to iterate over the stack for Listeners.
 		// It is setup and valid only during a call to a Listener hook.
 		stackIterator stackIterator
+
+		ensureTermination bool
 	}
 
 	// contextStack is a stack of context.Context.
@@ -274,30 +276,29 @@ type (
 		// def is the api.Function for this function. Created during compilation.
 		def api.FunctionDefinition
 		// parent holds code from which this is created.
-		parent *code
+		parent *compiledFunction
 	}
 
-	// code corresponds to a function in a module (not instantiated one). This holds the machine code
-	// compiled by wazero compiler.
-	code struct {
-		// See note at top of file before modifying this struct.
+	compiledModule struct {
+		executable        []byte
+		functions         []compiledFunction
+		source            *wasm.Module
+		ensureTermination bool
+	}
 
+	// compiledFunction corresponds to a function in a module (not instantiated one). This holds the machine code
+	// compiled by wazero compiler.
+	compiledFunction struct {
 		// codeSegment is holding the compiled native code as a byte slice.
-		codeSegment []byte
+		executableOffset int
 		// See the doc for codeStaticData type.
 		// stackPointerCeil is the max of the stack pointer this function can reach. Lazily applied via maybeGrowStack.
 		stackPointerCeil uint64
 
-		// indexInModule is the index of this function in the module. For logging purpose.
-		indexInModule wasm.Index
-		// sourceModule is the module from which this function is compiled. For logging purpose.
-		sourceModule *wasm.Module
-		// listener holds a listener to notify when this function is called.
-		listener experimental.FunctionListener
-		goFunc   interface{}
-
-		withEnsureTermination bool
-		sourceOffsetMap       sourceOffsetMap
+		goFunc          interface{}
+		listener        experimental.FunctionListener
+		parent          *compiledModule
+		sourceOffsetMap sourceOffsetMap
 	}
 
 	// sourceOffsetMap holds the information to retrieve the original offset in the Wasm binary from the
@@ -468,21 +469,19 @@ func (s nativeCallStatusCode) String() (ret string) {
 	return
 }
 
-// releaseCode is a runtime.SetFinalizer function that munmaps the code.codeSegment.
-func releaseCode(compiledFn *code) {
-	codeSegment := compiledFn.codeSegment
-	if codeSegment == nil {
+// releaseCompiledModule is a runtime.SetFinalizer function that munmaps the compiledModule.executable.
+func releaseCompiledModule(cm *compiledModule) {
+	e := cm.executable
+	if e == nil {
 		return // already released
 	}
 
 	// Setting this to nil allows tests to know the correct finalizer function was called.
-	compiledFn.codeSegment = nil
-	if err := platform.MunmapCodeSegment(codeSegment); err != nil {
+	cm.executable = nil
+	if err := platform.MunmapCodeSegment(e); err != nil {
 		// munmap failure cannot recover, and happen asynchronously on the finalizer thread. While finalizer
-		// functions can return errors, they are ignored. To make these visible for troubleshooting, we panic
-		// with additional context. module+funcidx should be enough, but if not, we can add more later.
-		panic(fmt.Errorf("compiler: failed to munmap code segment for %s.function[%d]: %w", compiledFn.sourceModule.NameSection.ModuleName,
-			compiledFn.indexInModule, err))
+		// functions can return errors, they are ignored.
+		panic(fmt.Errorf("compiler: failed to munmap code segment: %w", err))
 	}
 }
 
@@ -493,7 +492,7 @@ func (e *engine) CompiledModuleCount() uint32 {
 
 // DeleteCompiledModule implements the same method as documented on wasm.Engine.
 func (e *engine) DeleteCompiledModule(module *wasm.Module) {
-	e.deleteCodes(module)
+	e.deleteCompiledModule(module)
 }
 
 // Close implements the same method as documented on wasm.Engine.
@@ -507,7 +506,7 @@ func (e *engine) Close() (err error) {
 
 // CompileModule implements the same method as documented on wasm.Engine.
 func (e *engine) CompileModule(_ context.Context, module *wasm.Module, listeners []experimental.FunctionListener, ensureTermination bool) error {
-	if _, ok, err := e.getCodes(module, listeners); ok { // cache hit!
+	if _, ok, err := e.getCompiledModule(module, listeners); ok { // cache hit!
 		return nil
 	} else if err != nil {
 		return err
@@ -519,8 +518,20 @@ func (e *engine) CompileModule(_ context.Context, module *wasm.Module, listeners
 	}
 
 	var withGoFunc bool
-	importedFuncs := module.ImportFunctionCount
-	funcs := make([]*code, len(module.FunctionSection))
+	localFuncs, importedFuncs := len(module.FunctionSection), module.ImportFunctionCount
+	cm := &compiledModule{
+		functions:         make([]compiledFunction, localFuncs),
+		ensureTermination: ensureTermination,
+		source:            module,
+	}
+
+	if localFuncs == 0 {
+		return e.addCompiledModule(module, cm, withGoFunc)
+	}
+
+	bodies := make([][]byte, localFuncs)
+	// As this uses mmap, we need to munmap on the compiled machine code when it's GCed.
+	e.setFinalizer(cm, releaseCompiledModule)
 	ln := len(listeners)
 	cmp := newCompiler()
 	for i := range module.CodeSection {
@@ -530,37 +541,63 @@ func (e *engine) CompileModule(_ context.Context, module *wasm.Module, listeners
 			lsn = listeners[i]
 		}
 		funcIndex := wasm.Index(i)
-		var compiled *code
+		compiledFn := &cm.functions[i]
+		var body []byte
 		if codeSeg := &module.CodeSection[i]; codeSeg.GoFunc != nil {
 			cmp.Init(typ, nil, lsn != nil)
 			withGoFunc = true
-			if compiled, err = compileGoDefinedHostFunction(cmp); err != nil {
+			if body, err = compileGoDefinedHostFunction(cmp); err != nil {
 				def := module.FunctionDefinitionSection[funcIndex+importedFuncs]
 				return fmt.Errorf("error compiling host go func[%s]: %w", def.DebugName(), err)
 			}
-			compiled.goFunc = codeSeg.GoFunc
+			compiledFn.goFunc = codeSeg.GoFunc
 		} else {
 			ir, err := irCompiler.Next()
 			if err != nil {
 				return fmt.Errorf("failed to lower func[%d]: %v", i, err)
 			}
 			cmp.Init(typ, ir, lsn != nil)
-			if compiled, err = compileWasmFunction(cmp, ir); err != nil {
+
+			body, compiledFn.stackPointerCeil, compiledFn.sourceOffsetMap, err = compileWasmFunction(cmp, ir)
+			if err != nil {
 				def := module.FunctionDefinitionSection[funcIndex+importedFuncs]
 				return fmt.Errorf("error compiling wasm func[%s]: %w", def.DebugName(), err)
 			}
 		}
 
-		// As this uses mmap, we need to munmap on the compiled machine code when it's GCed.
-		e.setFinalizer(compiled, releaseCode)
+		// The `body` here is the view owned by assembler and will be overridden by the next iteration, so copy the body here.
+		bodyCopied := make([]byte, len(body))
+		copy(bodyCopied, body)
+		bodies[i] = bodyCopied
+		compiledFn.listener = lsn
+		compiledFn.parent = cm
+	}
+
+	var executableOffset int
+	for i, b := range bodies {
+		cm.functions[i].executableOffset = executableOffset
+		// Align 16-bytes boundary.
+		executableOffset = (executableOffset + len(b) + 15) &^ 15
+	}
+
+	executable, err := platform.MmapCodeSegment(executableOffset)
+	if err != nil {
+		return err
+	}
+
+	for i, b := range bodies {
+		offset := cm.functions[i].executableOffset
+		copy(executable[offset:], b)
+	}
 
-		compiled.listener = lsn
-		compiled.indexInModule = funcIndex
-		compiled.sourceModule = module
-		compiled.withEnsureTermination = ensureTermination
-		funcs[funcIndex] = compiled
+	if runtime.GOARCH == "arm64" {
+		// On arm64, we cannot give all of rwx at the same time, so we change it to exec.
+		if err = platform.MprotectRX(executable); err != nil {
+			return err
+		}
 	}
-	return e.addCodes(module, funcs, withGoFunc)
+	cm.executable = executable
+	return e.addCompiledModule(module, cm, withGoFunc)
 }
 
 // NewModuleEngine implements the same method as documented on wasm.Engine.
@@ -571,7 +608,7 @@ func (e *engine) NewModuleEngine(module *wasm.Module, instance *wasm.ModuleInsta
 
 	// Note: imported functions are resolved in moduleEngine.ResolveImportedFunction.
 
-	codes, ok, err := e.getCodes(module,
+	cm, ok, err := e.getCompiledModule(module,
 		// listeners arg is not needed here since NewModuleEngine is called after CompileModule which
 		// ensures the association of listener with *code.
 		nil)
@@ -581,11 +618,12 @@ func (e *engine) NewModuleEngine(module *wasm.Module, instance *wasm.ModuleInsta
 		return nil, err
 	}
 
-	for i, c := range codes {
+	for i := range cm.functions {
+		c := &cm.functions[i]
 		offset := int(module.ImportFunctionCount) + i
 		typeIndex := module.FunctionSection[i]
 		me.functions[offset] = function{
-			codeInitialAddress: uintptr(unsafe.Pointer(&c.codeSegment[0])),
+			codeInitialAddress: uintptr(unsafe.Pointer(&cm.executable[c.executableOffset])),
 			moduleInstance:     instance,
 			index:              wasm.Index(offset),
 			typeID:             instance.TypeIDs[typeIndex],
@@ -665,7 +703,7 @@ func (ce *callEngine) Definition() api.FunctionDefinition {
 // Call implements the same method as documented on wasm.ModuleEngine.
 func (ce *callEngine) Call(ctx context.Context, params ...uint64) (results []uint64, err error) {
 	m := ce.initialFn.moduleInstance
-	if ce.fn.parent.withEnsureTermination {
+	if ce.ensureTermination {
 		select {
 		case <-ctx.Done():
 			// If the provided context is already done, close the call context
@@ -697,7 +735,7 @@ func (ce *callEngine) Call(ctx context.Context, params ...uint64) (results []uin
 
 	ce.initializeStack(tp, params)
 
-	if ce.fn.parent.withEnsureTermination {
+	if ce.ensureTermination {
 		done := m.CloseModuleOnCanceledOrTimeout(ctx)
 		defer done()
 	}
@@ -782,10 +820,10 @@ func (ce *callEngine) deferredOnCall(recovered interface{}) (err error) {
 			// sourceInfo holds the source code information corresponding to the frame.
 			// It is not empty only when the DWARF is enabled.
 			var sources []string
-			if p := fn.parent; p.codeSegment != nil {
+			if p := fn.parent; p.parent.executable != nil {
 				if len(fn.parent.sourceOffsetMap.irOperationSourceOffsetsInWasmBinary) != 0 {
 					offset := fn.getSourceOffsetInWasmBinary(pc)
-					sources = p.sourceModule.DWARFLines.Line(offset)
+					sources = p.parent.source.DWARFLines.Line(offset)
 				}
 			}
 			builder.AddFrame(def.DebugName(), def.ParamTypes(), def.ResultTypes(), sources)
@@ -843,7 +881,7 @@ func NewEngine(_ context.Context, enabledFeatures api.CoreFeatures, fileCache fi
 func newEngine(enabledFeatures api.CoreFeatures, fileCache filecache.Cache) *engine {
 	return &engine{
 		enabledFeatures: enabledFeatures,
-		codes:           map[wasm.ModuleID][]*code{},
+		codes:           map[wasm.ModuleID]*compiledModule{},
 		setFinalizer:    runtime.SetFinalizer,
 		fileCache:       fileCache,
 		wazeroVersion:   version.GetWazeroVersion(),
@@ -881,10 +919,11 @@ var initialStackSize uint64 = 512
 
 func (e *moduleEngine) newCallEngine(stackSize uint64, fn *function) *callEngine {
 	ce := &callEngine{
-		stack:         make([]uint64, stackSize),
-		archContext:   newArchContext(),
-		initialFn:     fn,
-		moduleContext: moduleContext{fn: fn},
+		stack:             make([]uint64, stackSize),
+		archContext:       newArchContext(),
+		initialFn:         fn,
+		moduleContext:     moduleContext{fn: fn},
+		ensureTermination: fn.parent.parent.ensureTermination,
 	}
 
 	stackHeader := (*reflect.SliceHeader)(unsafe.Pointer(&ce.stack))
@@ -1087,7 +1126,7 @@ func (si *stackIterator) FunctionDefinition() api.FunctionDefinition {
 	return si.fn.def
 }
 
-// Args implements experimental.StackIterator.
+// Parameters implements experimental.StackIterator.
 func (si *stackIterator) Parameters() []uint64 {
 	return si.stack[si.base : si.base+si.fn.funcType.ParamNumInUint64]
 }
@@ -1111,22 +1150,19 @@ func (ce *callEngine) builtinFunctionFunctionListenerAfter(ctx context.Context,
 	ce.contextStack = ce.contextStack.prev
 }
 
-func compileGoDefinedHostFunction(cmp compiler) (*code, error) {
-	if err := cmp.compileGoDefinedHostFunction(); err != nil {
-		return nil, err
-	}
-
-	c, _, err := cmp.compile()
-	if err != nil {
-		return nil, err
+func compileGoDefinedHostFunction(cmp compiler) (body []byte, err error) {
+	if err = cmp.compileGoDefinedHostFunction(); err != nil {
+		return
 	}
 
-	return &code{codeSegment: c}, nil
+	body, _, err = cmp.compile()
+	return
 }
 
-func compileWasmFunction(cmp compiler, ir *wazeroir.CompilationResult) (*code, error) {
-	if err := cmp.compilePreamble(); err != nil {
-		return nil, fmt.Errorf("failed to emit preamble: %w", err)
+func compileWasmFunction(cmp compiler, ir *wazeroir.CompilationResult) (body []byte, spCeil uint64, sm sourceOffsetMap, err error) {
+	if err = cmp.compilePreamble(); err != nil {
+		err = fmt.Errorf("failed to emit preamble: %w", err)
+		return
 	}
 
 	needSourceOffsets := len(ir.IROperationSourceOffsetsInWasmBinary) > 0
@@ -1158,7 +1194,6 @@ func compileWasmFunction(cmp compiler, ir *wazeroir.CompilationResult) (*code, e
 		if false {
 			fmt.Printf("compiling op=%s: %s\n", op.Kind, cmp)
 		}
-		var err error
 		switch op.Kind {
 		case wazeroir.OperationKindUnreachable:
 			err = cmp.compileUnreachable()
@@ -1442,25 +1477,25 @@ func compileWasmFunction(cmp compiler, ir *wazeroir.CompilationResult) (*code, e
 			err = errors.New("unsupported")
 		}
 		if err != nil {
-			return nil, fmt.Errorf("operation %s: %w", op.Kind.String(), err)
+			err = fmt.Errorf("operation %s: %w", op.Kind.String(), err)
+			return
 		}
 	}
 
-	c, stackPointerCeil, err := cmp.compile()
+	body, spCeil, err = cmp.compile()
 	if err != nil {
-		return nil, fmt.Errorf("failed to compile: %w", err)
+		err = fmt.Errorf("failed to compile: %w", err)
+		return
 	}
 
-	ret := &code{codeSegment: c, stackPointerCeil: stackPointerCeil}
 	if needSourceOffsets {
 		offsetInNativeBin := make([]uint64, len(irOpBegins))
 		for i, nop := range irOpBegins {
 			offsetInNativeBin[i] = nop.OffsetInBinary()
 		}
-		sm := &ret.sourceOffsetMap
 		sm.irOperationOffsetsInNativeBinary = offsetInNativeBin
 		sm.irOperationSourceOffsetsInWasmBinary = make([]uint64, len(ir.IROperationSourceOffsetsInWasmBinary))
 		copy(sm.irOperationSourceOffsetsInWasmBinary, ir.IROperationSourceOffsetsInWasmBinary)
 	}
-	return ret, nil
+	return
 }
diff --git a/internal/engine/compiler/engine_cache.go b/internal/engine/compiler/engine_cache.go
index 1fda9598..70e56dee 100644
--- a/internal/engine/compiler/engine_cache.go
+++ b/internal/engine/compiler/engine_cache.go
@@ -14,7 +14,7 @@ import (
 	"github.com/tetratelabs/wazero/internal/wasm"
 )
 
-func (e *engine) deleteCodes(module *wasm.Module) {
+func (e *engine) deleteCompiledModule(module *wasm.Module) {
 	e.mux.Lock()
 	defer e.mux.Unlock()
 	delete(e.codes, module.ID)
@@ -23,55 +23,54 @@ func (e *engine) deleteCodes(module *wasm.Module) {
 	// the content is up to the implementation of extencache.Cache interface.
 }
 
-func (e *engine) addCodes(module *wasm.Module, codes []*code, withGoFunc bool) (err error) {
-	e.addCodesToMemory(module, codes)
+func (e *engine) addCompiledModule(module *wasm.Module, cm *compiledModule, withGoFunc bool) (err error) {
+	e.addCompiledModuleToMemory(module, cm)
 	if !withGoFunc {
-		err = e.addCodesToCache(module, codes)
+		err = e.addCompiledModuleToCache(module, cm)
 	}
 	return
 }
 
-func (e *engine) getCodes(module *wasm.Module, listeners []experimental.FunctionListener) (codes []*code, ok bool, err error) {
-	codes, ok = e.getCodesFromMemory(module)
+func (e *engine) getCompiledModule(module *wasm.Module, listeners []experimental.FunctionListener) (cm *compiledModule, ok bool, err error) {
+	cm, ok = e.getCompiledModuleFromMemory(module)
 	if ok {
 		return
 	}
-	codes, ok, err = e.getCodesFromCache(module)
+	cm, ok, err = e.getCompiledModuleFromCache(module)
 	if ok {
-		e.addCodesToMemory(module, codes)
-	}
-
-	if len(listeners) > 0 {
-		// Files do not contain the actual listener instances (it's impossible to cache them as files!), so assign each here.
-		for i := range codes {
-			codes[i].listener = listeners[i]
+		e.addCompiledModuleToMemory(module, cm)
+		if len(listeners) > 0 {
+			// Files do not contain the actual listener instances (it's impossible to cache them as files!), so assign each here.
+			for i := range cm.functions {
+				cm.functions[i].listener = listeners[i]
+			}
 		}
 	}
 	return
 }
 
-func (e *engine) addCodesToMemory(module *wasm.Module, codes []*code) {
+func (e *engine) addCompiledModuleToMemory(module *wasm.Module, cm *compiledModule) {
 	e.mux.Lock()
 	defer e.mux.Unlock()
-	e.codes[module.ID] = codes
+	e.codes[module.ID] = cm
 }
 
-func (e *engine) getCodesFromMemory(module *wasm.Module) (codes []*code, ok bool) {
+func (e *engine) getCompiledModuleFromMemory(module *wasm.Module) (cm *compiledModule, ok bool) {
 	e.mux.RLock()
 	defer e.mux.RUnlock()
-	codes, ok = e.codes[module.ID]
+	cm, ok = e.codes[module.ID]
 	return
 }
 
-func (e *engine) addCodesToCache(module *wasm.Module, codes []*code) (err error) {
+func (e *engine) addCompiledModuleToCache(module *wasm.Module, cm *compiledModule) (err error) {
 	if e.fileCache == nil || module.IsHostModule {
 		return
 	}
-	err = e.fileCache.Add(module.ID, serializeCodes(e.wazeroVersion, codes))
+	err = e.fileCache.Add(module.ID, serializeCompiledModule(e.wazeroVersion, cm))
 	return
 }
 
-func (e *engine) getCodesFromCache(module *wasm.Module) (codes []*code, hit bool, err error) {
+func (e *engine) getCompiledModuleFromCache(module *wasm.Module) (cm *compiledModule, hit bool, err error) {
 	if e.fileCache == nil || module.IsHostModule {
 		return
 	}
@@ -87,7 +86,7 @@ func (e *engine) getCodesFromCache(module *wasm.Module) (codes []*code, hit bool
 	// We retrieve *code structures from `cached`.
 	var staleCache bool
 	// Note: cached.Close is ensured to be called in deserializeCodes.
-	codes, staleCache, err = deserializeCodes(e.wazeroVersion, cached)
+	cm, staleCache, err = deserializeCompiledModule(e.wazeroVersion, cached)
 	if err != nil {
 		hit = false
 		return
@@ -95,16 +94,13 @@ func (e *engine) getCodesFromCache(module *wasm.Module) (codes []*code, hit bool
 		return nil, false, e.fileCache.Delete(module.ID)
 	}
 
-	for i, c := range codes {
-		c.indexInModule = wasm.Index(i)
-		c.sourceModule = module
-	}
+	cm.source = module
 	return
 }
 
 var wazeroMagic = "WAZERO" // version must be synced with the tag of the wazero library.
 
-func serializeCodes(wazeroVersion string, codes []*code) io.Reader {
+func serializeCompiledModule(wazeroVersion string, cm *compiledModule) io.Reader {
 	buf := bytes.NewBuffer(nil)
 	// First 6 byte: WAZERO header.
 	buf.WriteString(wazeroMagic)
@@ -112,25 +108,28 @@ func serializeCodes(wazeroVersion string, codes []*code) io.Reader {
 	buf.WriteByte(byte(len(wazeroVersion)))
 	// Version of wazero.
 	buf.WriteString(wazeroVersion)
-	if len(codes) > 0 && codes[0].withEnsureTermination {
+	if cm.ensureTermination {
 		buf.WriteByte(1)
 	} else {
 		buf.WriteByte(0)
 	}
 	// Number of *code (== locally defined functions in the module): 4 bytes.
-	buf.Write(u32.LeBytes(uint32(len(codes))))
-	for _, c := range codes {
+	buf.Write(u32.LeBytes(uint32(len(cm.functions))))
+	for i := 0; i < len(cm.functions); i++ {
+		f := &cm.functions[i]
 		// The stack pointer ceil (8 bytes).
-		buf.Write(u64.LeBytes(c.stackPointerCeil))
-		// The length of code segment (8 bytes).
-		buf.Write(u64.LeBytes(uint64(len(c.codeSegment))))
-		// Append the native code.
-		buf.Write(c.codeSegment)
-	}
+		buf.Write(u64.LeBytes(f.stackPointerCeil))
+		// The offset of this function in the executable (8 bytes).
+		buf.Write(u64.LeBytes(uint64(f.executableOffset)))
+	}
+	// The length of code segment (8 bytes).
+	buf.Write(u64.LeBytes(uint64(len(cm.executable))))
+	// Append the native code.
+	buf.Write(cm.executable)
 	return bytes.NewReader(buf.Bytes())
 }
 
-func deserializeCodes(wazeroVersion string, reader io.ReadCloser) (codes []*code, staleCache bool, err error) {
+func deserializeCompiledModule(wazeroVersion string, reader io.ReadCloser) (cm *compiledModule, staleCache bool, err error) {
 	defer reader.Close()
 	cacheHeaderSize := len(wazeroMagic) + 1 /* version size */ + len(wazeroVersion) + 1 /* ensure termination */ + 4 /* number of functions */
 
@@ -158,63 +157,58 @@ func deserializeCodes(wazeroVersion string, reader io.ReadCloser) (codes []*code
 	}
 
 	ensureTermination := header[cachedVersionEnd] != 0
-
 	functionsNum := binary.LittleEndian.Uint32(header[len(header)-4:])
-	codes = make([]*code, 0, functionsNum)
+	cm = &compiledModule{functions: make([]compiledFunction, functionsNum), ensureTermination: ensureTermination}
 
 	var eightBytes [8]byte
-	var nativeCodeLen uint64
 	for i := uint32(0); i < functionsNum; i++ {
-		c := &code{withEnsureTermination: ensureTermination}
+		f := &cm.functions[i]
+		f.parent = cm
 
 		// Read the stack pointer ceil.
-		if c.stackPointerCeil, err = readUint64(reader, &eightBytes); err != nil {
+		if f.stackPointerCeil, err = readUint64(reader, &eightBytes); err != nil {
 			err = fmt.Errorf("compilationcache: error reading func[%d] stack pointer ceil: %v", i, err)
-			break
+			return
 		}
 
-		// Read (and mmap) the native code.
-		if nativeCodeLen, err = readUint64(reader, &eightBytes); err != nil {
-			err = fmt.Errorf("compilationcache: error reading func[%d] reading native code size: %v", i, err)
-			break
+		// Read the offset of each function in the executable.
+		var offset uint64
+		if offset, err = readUint64(reader, &eightBytes); err != nil {
+			err = fmt.Errorf("compilationcache: error reading func[%d] executable offset: %v", i, err)
+			return
 		}
+		f.executableOffset = int(offset)
+	}
 
-		if c.codeSegment, err = platform.MmapCodeSegment(int(nativeCodeLen)); err != nil {
-			err = fmt.Errorf("compilationcache: error mmapping func[%d] code (len=%d): %v", i, nativeCodeLen, err)
-			break
-		}
+	executableLen, err := readUint64(reader, &eightBytes)
+	if err != nil {
+		err = fmt.Errorf("compilationcache: error reading executable size: %v", err)
+		return
+	}
 
-		codes = append(codes, c)
+	if executableLen > 0 {
+		if cm.executable, err = platform.MmapCodeSegment(int(executableLen)); err != nil {
+			err = fmt.Errorf("compilationcache: error mmapping executable (len=%d): %v", executableLen, err)
+			return
+		}
 
-		_, err = io.ReadFull(reader, c.codeSegment)
+		_, err = io.ReadFull(reader, cm.executable)
 		if err != nil {
-			err = fmt.Errorf("compilationcache: error reading func[%d] code (len=%d): %v", i, nativeCodeLen, err)
-			break
+			err = fmt.Errorf("compilationcache: error reading executable (len=%d): %v", executableLen, err)
+			return
 		}
 
 		if runtime.GOARCH == "arm64" {
 			// On arm64, we cannot give all of rwx at the same time, so we change it to exec.
-			err = platform.MprotectRX(c.codeSegment)
-			if err != nil {
-				break
-			}
-		}
-
-	}
-
-	if err != nil {
-		for _, c := range codes {
-			if errMunmap := platform.MunmapCodeSegment(c.codeSegment); errMunmap != nil {
-				// Munmap failure shouldn't happen.
-				panic(errMunmap)
+			if err = platform.MprotectRX(cm.executable); err != nil {
+				return
 			}
 		}
-		codes = nil
 	}
 	return
 }
 
-// readUint64 strictly reads a uint64 in little-endian byte order, using the
+// readUint64 strictly reads an uint64 in little-endian byte order, using the
 // given array as a buffer. This returns io.EOF if less than 8 bytes were read.
 func readUint64(reader io.Reader, b *[8]byte) (uint64, error) {
 	s := b[0:8]
@@ -225,10 +219,10 @@ func readUint64(reader io.Reader, b *[8]byte) (uint64, error) {
 		return 0, io.EOF
 	}
 
-	// read the u64 from the underlying buffer
+	// Read the u64 from the underlying buffer.
 	ret := binary.LittleEndian.Uint64(s)
 
-	// clear the underlying array
+	// Clear the underlying array.
 	for i := 0; i < 8; i++ {
 		b[i] = 0
 	}
diff --git a/internal/engine/compiler/impl_amd64.go b/internal/engine/compiler/impl_amd64.go
index c367b21b..4690e87d 100644
--- a/internal/engine/compiler/impl_amd64.go
+++ b/internal/engine/compiler/impl_amd64.go
@@ -341,17 +341,7 @@ func (c *amd64Compiler) compile() (code []byte, stackPointerCeil uint64, err err
 	// Note this MUST be called before Assemble() below.
 	c.assignStackPointerCeil(stackPointerCeil)
 
-	var original []byte
-	original, err = c.assembler.Assemble()
-	if err != nil {
-		return
-	}
-
-	code, err = platform.MmapCodeSegment(len(original))
-	if err != nil {
-		return
-	}
-	copy(code, original)
+	code, err = c.assembler.Assemble()
 	return
 }
 
diff --git a/internal/engine/compiler/impl_arm64.go b/internal/engine/compiler/impl_arm64.go
index 72546a37..5bb30563 100644
--- a/internal/engine/compiler/impl_arm64.go
+++ b/internal/engine/compiler/impl_arm64.go
@@ -11,7 +11,6 @@ import (
 
 	"github.com/tetratelabs/wazero/internal/asm"
 	"github.com/tetratelabs/wazero/internal/asm/arm64"
-	"github.com/tetratelabs/wazero/internal/platform"
 	"github.com/tetratelabs/wazero/internal/wasm"
 	"github.com/tetratelabs/wazero/internal/wazeroir"
 )
@@ -154,19 +153,7 @@ func (c *arm64Compiler) compile() (code []byte, stackPointerCeil uint64, err err
 	// Note: this must be called before Assemble() below.
 	c.assignStackPointerCeil(stackPointerCeil)
 
-	var original []byte
-	original, err = c.assembler.Assemble()
-	if err != nil {
-		return
-	}
-
-	code, err = platform.MmapCodeSegment(len(original))
-	if err != nil {
-		return
-	}
-	copy(code, original)
-	// On arm64, we cannot give all of rwx at the same time, so we change it to exec.
-	err = platform.MprotectRX(code)
+	code, err = c.assembler.Assemble()
 	return
 }
 


Test patch:
diff --git a/internal/engine/compiler/compiler_controlflow_test.go b/internal/engine/compiler/compiler_controlflow_test.go
index 6adf4b1f..069a728c 100644
--- a/internal/engine/compiler/compiler_controlflow_test.go
+++ b/internal/engine/compiler/compiler_controlflow_test.go
@@ -703,11 +703,12 @@ func TestCompiler_compileCallIndirect(t *testing.T) {
 			c, _, err := compiler.compile()
 			require.NoError(t, err)
 
+			executable := requireExecutable(c)
+
 			// Now that we've generated the code for this function,
 			// add it to the module engine and assign its pointer to the table index.
 			me.functions[i] = function{
-				parent:             &code{codeSegment: c},
-				codeInitialAddress: uintptr(unsafe.Pointer(&c[0])),
+				codeInitialAddress: uintptr(unsafe.Pointer(&executable[0])),
 				moduleInstance:     env.moduleInstance,
 				typeID:             targetTypeID,
 			}
@@ -784,9 +785,10 @@ func TestCompiler_callIndirect_largeTypeIndex(t *testing.T) {
 		c, _, err := compiler.compile()
 		require.NoError(t, err)
 
+		executable := requireExecutable(c)
 		f := function{
-			parent:             &code{codeSegment: c},
-			codeInitialAddress: uintptr(unsafe.Pointer(&c[0])),
+			parent:             &compiledFunction{parent: &compiledModule{executable: executable}},
+			codeInitialAddress: uintptr(unsafe.Pointer(&executable[0])),
 			moduleInstance:     env.moduleInstance,
 		}
 		me.functions = append(me.functions, f)
@@ -852,9 +854,11 @@ func TestCompiler_compileCall(t *testing.T) {
 
 		c, _, err := compiler.compile()
 		require.NoError(t, err)
+
+		executable := requireExecutable(c)
 		me.functions = append(me.functions, function{
-			parent:             &code{codeSegment: c},
-			codeInitialAddress: uintptr(unsafe.Pointer(&c[0])),
+			parent:             &compiledFunction{parent: &compiledModule{executable: executable}},
+			codeInitialAddress: uintptr(unsafe.Pointer(&executable[0])),
 			moduleInstance:     env.moduleInstance,
 		})
 	}
diff --git a/internal/engine/compiler/compiler_test.go b/internal/engine/compiler/compiler_test.go
index 03ae13f4..a2ff5d7b 100644
--- a/internal/engine/compiler/compiler_test.go
+++ b/internal/engine/compiler/compiler_test.go
@@ -4,6 +4,7 @@ import (
 	"fmt"
 	"math"
 	"os"
+	"runtime"
 	"testing"
 	"unsafe"
 
@@ -200,21 +201,18 @@ func (j *compilerEnv) callEngine() *callEngine {
 	return j.ce
 }
 
-func (j *compilerEnv) newFunction(codeSegment []byte) *function {
-	return &function{
-		parent:             &code{codeSegment: codeSegment},
-		codeInitialAddress: uintptr(unsafe.Pointer(&codeSegment[0])),
+func (j *compilerEnv) exec(machineCode []byte) {
+	executable := requireExecutable(machineCode)
+	f := &function{
+		parent:             &compiledFunction{parent: &compiledModule{executable: executable}},
+		codeInitialAddress: uintptr(unsafe.Pointer(&executable[0])),
 		moduleInstance:     j.moduleInstance,
 	}
-}
-
-func (j *compilerEnv) exec(codeSegment []byte) {
-	f := j.newFunction(codeSegment)
 	j.ce.initialFn = f
 	j.ce.fn = f
 
 	nativecall(
-		uintptr(unsafe.Pointer(&codeSegment[0])),
+		uintptr(unsafe.Pointer(&executable[0])),
 		uintptr(unsafe.Pointer(j.ce)),
 		j.moduleInstance,
 	)
@@ -264,7 +262,7 @@ func newCompilerEnvironment() *compilerEnv {
 			Globals:        []*wasm.GlobalInstance{},
 			Engine:         me,
 		},
-		ce: me.newCallEngine(initialStackSize, nil),
+		ce: me.newCallEngine(initialStackSize, &function{parent: &compiledFunction{parent: &compiledModule{}}}),
 	}
 }
 
@@ -291,3 +289,20 @@ func TestCompileI32WrapFromI64(t *testing.T) {
 func operationPtr(operation wazeroir.UnionOperation) *wazeroir.UnionOperation {
 	return &operation
 }
+
+func requireExecutable(original []byte) (executable []byte) {
+	executable, err := platform.MmapCodeSegment(len(original))
+	if err != nil {
+		panic(err)
+	}
+	copy(executable, original)
+
+	if runtime.GOARCH == "arm64" {
+		err = platform.MprotectRX(executable)
+		if err != nil {
+			panic(err)
+		}
+	}
+
+	return executable
+}
diff --git a/internal/engine/compiler/engine_cache_test.go b/internal/engine/compiler/engine_cache_test.go
index 1fdb27a4..cd71da41 100644
--- a/internal/engine/compiler/engine_cache_test.go
+++ b/internal/engine/compiler/engine_cache_test.go
@@ -26,13 +26,18 @@ func concat(ins ...[]byte) (ret []byte) {
 	return
 }
 
-func TestSerializeCodes(t *testing.T) {
+func TestSerializeCompiledModule(t *testing.T) {
 	tests := []struct {
-		in  []*code
+		in  *compiledModule
 		exp []byte
 	}{
 		{
-			in: []*code{{stackPointerCeil: 12345, codeSegment: []byte{1, 2, 3, 4, 5}}},
+			in: &compiledModule{
+				executable: []byte{1, 2, 3, 4, 5},
+				functions: []compiledFunction{
+					{executableOffset: 0, stackPointerCeil: 12345},
+				},
+			},
 			exp: concat(
 				[]byte(wazeroMagic),
 				[]byte{byte(len(testVersion))},
@@ -40,12 +45,19 @@ func TestSerializeCodes(t *testing.T) {
 				[]byte{0},             // ensure termination.
 				u32.LeBytes(1),        // number of functions.
 				u64.LeBytes(12345),    // stack pointer ceil.
+				u64.LeBytes(0),        // offset.
 				u64.LeBytes(5),        // length of code.
 				[]byte{1, 2, 3, 4, 5}, // code.
 			),
 		},
 		{
-			in: []*code{{withEnsureTermination: true, stackPointerCeil: 12345, codeSegment: []byte{1, 2, 3, 4, 5}}},
+			in: &compiledModule{
+				ensureTermination: true,
+				executable:        []byte{1, 2, 3, 4, 5},
+				functions: []compiledFunction{
+					{executableOffset: 0, stackPointerCeil: 12345},
+				},
+			},
 			exp: concat(
 				[]byte(wazeroMagic),
 				[]byte{byte(len(testVersion))},
@@ -53,47 +65,53 @@ func TestSerializeCodes(t *testing.T) {
 				[]byte{1},             // ensure termination.
 				u32.LeBytes(1),        // number of functions.
 				u64.LeBytes(12345),    // stack pointer ceil.
+				u64.LeBytes(0),        // offset.
 				u64.LeBytes(5),        // length of code.
 				[]byte{1, 2, 3, 4, 5}, // code.
 			),
 		},
 		{
-			in: []*code{
-				{stackPointerCeil: 12345, codeSegment: []byte{1, 2, 3, 4, 5}},
-				{stackPointerCeil: 0xffffffff, codeSegment: []byte{1, 2, 3}},
+			in: &compiledModule{
+				ensureTermination: true,
+				executable:        []byte{1, 2, 3, 4, 5, 1, 2, 3},
+				functions: []compiledFunction{
+					{executableOffset: 0, stackPointerCeil: 12345},
+					{executableOffset: 5, stackPointerCeil: 0xffffffff},
+				},
 			},
 			exp: concat(
 				[]byte(wazeroMagic),
 				[]byte{byte(len(testVersion))},
 				[]byte(testVersion),
-				[]byte{0},      // ensure termination.
+				[]byte{1},      // ensure termination.
 				u32.LeBytes(2), // number of functions.
 				// Function index = 0.
-				u64.LeBytes(12345),    // stack pointer ceil.
-				u64.LeBytes(5),        // length of code.
-				[]byte{1, 2, 3, 4, 5}, // code.
+				u64.LeBytes(12345), // stack pointer ceil.
+				u64.LeBytes(0),     // offset.
 				// Function index = 1.
 				u64.LeBytes(0xffffffff), // stack pointer ceil.
-				u64.LeBytes(3),          // length of code.
-				[]byte{1, 2, 3},         // code.
+				u64.LeBytes(5),          // offset.
+				// Executable.
+				u64.LeBytes(8),                 // length of code.
+				[]byte{1, 2, 3, 4, 5, 1, 2, 3}, // code.
 			),
 		},
 	}
 
 	for i, tc := range tests {
-		actual, err := io.ReadAll(serializeCodes(testVersion, tc.in))
+		actual, err := io.ReadAll(serializeCompiledModule(testVersion, tc.in))
 		require.NoError(t, err, i)
 		require.Equal(t, tc.exp, actual, i)
 	}
 }
 
-func TestDeserializeCodes(t *testing.T) {
+func TestDeserializeCompiledModule(t *testing.T) {
 	tests := []struct {
-		name          string
-		in            []byte
-		expCodes      []*code
-		expStaleCache bool
-		expErr        string
+		name              string
+		in                []byte
+		expCompiledModule *compiledModule
+		expStaleCache     bool
+		expErr            string
 	}{
 		{
 			name:   "invalid header",
@@ -126,14 +144,19 @@ func TestDeserializeCodes(t *testing.T) {
 				[]byte(wazeroMagic),
 				[]byte{byte(len(testVersion))},
 				[]byte(testVersion),
-				[]byte{0},             // ensure termination.
-				u32.LeBytes(1),        // number of functions.
-				u64.LeBytes(12345),    // stack pointer ceil.
-				u64.LeBytes(5),        // length of code.
-				[]byte{1, 2, 3, 4, 5}, // code.
+				[]byte{0},          // ensure termination.
+				u32.LeBytes(1),     // number of functions.
+				u64.LeBytes(12345), // stack pointer ceil.
+				u64.LeBytes(0),     // offset.
+				// Executable.
+				u64.LeBytes(5),        // size.
+				[]byte{1, 2, 3, 4, 5}, // machine code.
 			),
-			expCodes: []*code{
-				{stackPointerCeil: 12345, codeSegment: []byte{1, 2, 3, 4, 5}},
+			expCompiledModule: &compiledModule{
+				executable: []byte{1, 2, 3, 4, 5},
+				functions: []compiledFunction{
+					{executableOffset: 0, stackPointerCeil: 12345},
+				},
 			},
 			expStaleCache: false,
 			expErr:        "",
@@ -147,11 +170,14 @@ func TestDeserializeCodes(t *testing.T) {
 				[]byte{1},             // ensure termination.
 				u32.LeBytes(1),        // number of functions.
 				u64.LeBytes(12345),    // stack pointer ceil.
+				u64.LeBytes(0),        // offset.
 				u64.LeBytes(5),        // length of code.
 				[]byte{1, 2, 3, 4, 5}, // code.
 			),
-			expCodes: []*code{
-				{stackPointerCeil: 12345, codeSegment: []byte{1, 2, 3, 4, 5}, withEnsureTermination: true},
+			expCompiledModule: &compiledModule{
+				ensureTermination: true,
+				executable:        []byte{1, 2, 3, 4, 5},
+				functions:         []compiledFunction{{executableOffset: 0, stackPointerCeil: 12345}},
 			},
 			expStaleCache: false,
 			expErr:        "",
@@ -165,17 +191,21 @@ func TestDeserializeCodes(t *testing.T) {
 				[]byte{0},      // ensure termination.
 				u32.LeBytes(2), // number of functions.
 				// Function index = 0.
-				u64.LeBytes(12345),    // stack pointer ceil.
-				u64.LeBytes(5),        // length of code.
-				[]byte{1, 2, 3, 4, 5}, // code.
+				u64.LeBytes(12345), // stack pointer ceil.
+				u64.LeBytes(0),     // offset.
 				// Function index = 1.
 				u64.LeBytes(0xffffffff), // stack pointer ceil.
-				u64.LeBytes(3),          // length of code.
-				[]byte{1, 2, 3},         // code.
+				u64.LeBytes(7),          // offset.
+				// Executable.
+				u64.LeBytes(10),                       // size.
+				[]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, // machine code.
 			),
-			expCodes: []*code{
-				{stackPointerCeil: 12345, codeSegment: []byte{1, 2, 3, 4, 5}},
-				{stackPointerCeil: 0xffffffff, codeSegment: []byte{1, 2, 3}},
+			expCompiledModule: &compiledModule{
+				executable: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
+				functions: []compiledFunction{
+					{executableOffset: 0, stackPointerCeil: 12345},
+					{executableOffset: 7, stackPointerCeil: 0xffffffff},
+				},
 			},
 			expStaleCache: false,
 			expErr:        "",
@@ -189,15 +219,14 @@ func TestDeserializeCodes(t *testing.T) {
 				[]byte{0},      // ensure termination.
 				u32.LeBytes(2), // number of functions.
 				// Function index = 0.
-				u64.LeBytes(12345),    // stack pointer ceil.
-				u64.LeBytes(5),        // length of code.
-				[]byte{1, 2, 3, 4, 5}, // code.
+				u64.LeBytes(12345), // stack pointer ceil.
+				u64.LeBytes(5),     // offset.
 				// Function index = 1.
 			),
 			expErr: "compilationcache: error reading func[1] stack pointer ceil: EOF",
 		},
 		{
-			name: "reading native code size",
+			name: "reading executable offset",
 			in: concat(
 				[]byte(wazeroMagic),
 				[]byte{byte(len(testVersion))},
@@ -205,13 +234,12 @@ func TestDeserializeCodes(t *testing.T) {
 				[]byte{0},      // ensure termination.
 				u32.LeBytes(2), // number of functions.
 				// Function index = 0.
-				u64.LeBytes(12345),    // stack pointer ceil.
-				u64.LeBytes(5),        // length of code.
-				[]byte{1, 2, 3, 4, 5}, // code.
+				u64.LeBytes(12345), // stack pointer ceil.
+				u64.LeBytes(5),     // offset.
 				// Function index = 1.
 				u64.LeBytes(12345), // stack pointer ceil.
 			),
-			expErr: "compilationcache: error reading func[1] reading native code size: EOF",
+			expErr: "compilationcache: error reading func[1] executable offset: EOF",
 		},
 		{
 			name: "mmapping",
@@ -222,35 +250,45 @@ func TestDeserializeCodes(t *testing.T) {
 				[]byte{0},      // ensure termination.
 				u32.LeBytes(2), // number of functions.
 				// Function index = 0.
-				u64.LeBytes(12345),    // stack pointer ceil.
-				u64.LeBytes(5),        // length of code.
-				[]byte{1, 2, 3, 4, 5}, // code.
+				u64.LeBytes(12345), // stack pointer ceil.
+				u64.LeBytes(0),     // offset.
 				// Function index = 1.
 				u64.LeBytes(12345), // stack pointer ceil.
-				u64.LeBytes(5),     // length of code.
-				// Lack of code here.
+				u64.LeBytes(5),     // offset.
+				// Executable.
+				u64.LeBytes(5), // size of the executable.
+				// Lack of machine code here.
 			),
-			expErr: "compilationcache: error reading func[1] code (len=5): EOF",
+			expErr: "compilationcache: error reading executable (len=5): EOF",
 		},
 	}
 
 	for _, tc := range tests {
 		tc := tc
 		t.Run(tc.name, func(t *testing.T) {
-			codes, staleCache, err := deserializeCodes(testVersion, io.NopCloser(bytes.NewReader(tc.in)))
+			cm, staleCache, err := deserializeCompiledModule(testVersion, io.NopCloser(bytes.NewReader(tc.in)))
+
+			if tc.expCompiledModule != nil {
+				require.Equal(t, len(tc.expCompiledModule.functions), len(cm.functions))
+				for i := 0; i < len(cm.functions); i++ {
+					require.Equal(t, cm, cm.functions[i].parent)
+					tc.expCompiledModule.functions[i].parent = cm
+				}
+			}
+
 			if tc.expErr != "" {
 				require.EqualError(t, err, tc.expErr)
 			} else {
 				require.NoError(t, err)
+				require.Equal(t, tc.expCompiledModule, cm)
 			}
 
-			require.Equal(t, tc.expCodes, codes)
 			require.Equal(t, tc.expStaleCache, staleCache)
 		})
 	}
 }
 
-func TestEngine_getCodesFromCache(t *testing.T) {
+func TestEngine_getCompiledModuleFromCache(t *testing.T) {
 	valid := concat(
 		[]byte(wazeroMagic),
 		[]byte{byte(len(testVersion))},
@@ -258,24 +296,25 @@ func TestEngine_getCodesFromCache(t *testing.T) {
 		[]byte{0},      // ensure termination.
 		u32.LeBytes(2), // number of functions.
 		// Function index = 0.
-		u64.LeBytes(12345),    // stack pointer ceil.
-		u64.LeBytes(5),        // length of code.
-		[]byte{1, 2, 3, 4, 5}, // code.
+		u64.LeBytes(12345), // stack pointer ceil.
+		u64.LeBytes(0),     // offset.
 		// Function index = 1.
 		u64.LeBytes(0xffffffff), // stack pointer ceil.
-		u64.LeBytes(3),          // length of code.
-		[]byte{1, 2, 3},         // code.
+		u64.LeBytes(5),          // offset.
+		// executables.
+		u64.LeBytes(10),                       // length of code.
+		[]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, // code.
 	)
 
 	tests := []struct {
-		name       string
-		ext        map[wasm.ModuleID][]byte
-		key        wasm.ModuleID
-		isHostMod  bool
-		expCodes   []*code
-		expHit     bool
-		expErr     string
-		expDeleted bool
+		name              string
+		ext               map[wasm.ModuleID][]byte
+		key               wasm.ModuleID
+		isHostMod         bool
+		expCompiledModule *compiledModule
+		expHit            bool
+		expErr            string
+		expDeleted        bool
 	}{
 		{name: "extern cache not given"},
 		{
@@ -313,9 +352,14 @@ func TestEngine_getCodesFromCache(t *testing.T) {
 				{}: valid,
 			},
 			expHit: true,
-			expCodes: []*code{
-				{stackPointerCeil: 12345, codeSegment: []byte{1, 2, 3, 4, 5}, indexInModule: 0},
-				{stackPointerCeil: 0xffffffff, codeSegment: []byte{1, 2, 3}, indexInModule: 1},
+			expCompiledModule: &compiledModule{
+				executable: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
+				functions: []compiledFunction{
+					{stackPointerCeil: 12345, executableOffset: 0},
+					{stackPointerCeil: 0xffffffff, executableOffset: 5},
+				},
+				source:            nil,
+				ensureTermination: false,
 			},
 		},
 	}
@@ -324,8 +368,11 @@ func TestEngine_getCodesFromCache(t *testing.T) {
 		tc := tc
 		t.Run(tc.name, func(t *testing.T) {
 			m := &wasm.Module{ID: tc.key, IsHostModule: tc.isHostMod}
-			for _, expC := range tc.expCodes {
-				expC.sourceModule = m
+			if exp := tc.expCompiledModule; exp != nil {
+				exp.source = m
+				for i := range tc.expCompiledModule.functions {
+					tc.expCompiledModule.functions[i].parent = exp
+				}
 			}
 
 			e := engine{}
@@ -338,7 +385,7 @@ func TestEngine_getCodesFromCache(t *testing.T) {
 				}
 			}
 
-			codes, hit, err := e.getCodesFromCache(m)
+			codes, hit, err := e.getCompiledModuleFromCache(m)
 			if tc.expErr != "" {
 				require.EqualError(t, err, tc.expErr)
 			} else {
@@ -346,7 +393,7 @@ func TestEngine_getCodesFromCache(t *testing.T) {
 			}
 
 			require.Equal(t, tc.expHit, hit)
-			require.Equal(t, tc.expCodes, codes)
+			require.Equal(t, tc.expCompiledModule, codes)
 
 			if tc.ext != nil && tc.expDeleted {
 				_, hit, err := e.fileCache.Get(tc.key)
@@ -357,18 +404,18 @@ func TestEngine_getCodesFromCache(t *testing.T) {
 	}
 }
 
-func TestEngine_addCodesToCache(t *testing.T) {
+func TestEngine_addCompiledModuleToCache(t *testing.T) {
 	t.Run("not defined", func(t *testing.T) {
 		e := engine{}
-		err := e.addCodesToCache(nil, nil)
+		err := e.addCompiledModuleToCache(nil, nil)
 		require.NoError(t, err)
 	})
 	t.Run("host module", func(t *testing.T) {
 		tc := filecache.New(t.TempDir())
 		e := engine{fileCache: tc}
-		codes := []*code{{stackPointerCeil: 123, codeSegment: []byte{1, 2, 3}}}
+		cm := &compiledModule{executable: []byte{1, 2, 3}, functions: []compiledFunction{{stackPointerCeil: 123}}}
 		m := &wasm.Module{ID: sha256.Sum256(nil), IsHostModule: true} // Host module!
-		err := e.addCodesToCache(m, codes)
+		err := e.addCompiledModuleToCache(m, cm)
 		require.NoError(t, err)
 		// Check the host module not cached.
 		_, hit, err := tc.Get(m.ID)
@@ -379,8 +426,8 @@ func TestEngine_addCodesToCache(t *testing.T) {
 		tc := filecache.New(t.TempDir())
 		e := engine{fileCache: tc}
 		m := &wasm.Module{}
-		codes := []*code{{stackPointerCeil: 123, codeSegment: []byte{1, 2, 3}}}
-		err := e.addCodesToCache(m, codes)
+		cm := &compiledModule{executable: []byte{1, 2, 3}, functions: []compiledFunction{{stackPointerCeil: 123}}}
+		err := e.addCompiledModuleToCache(m, cm)
 		require.NoError(t, err)
 
 		content, ok, err := tc.Get(m.ID)
@@ -395,8 +442,9 @@ func TestEngine_addCodesToCache(t *testing.T) {
 			[]byte{0},
 			u32.LeBytes(1),   // number of functions.
 			u64.LeBytes(123), // stack pointer ceil.
-			u64.LeBytes(3),   // length of code.
-			[]byte{1, 2, 3},  // code.
+			u64.LeBytes(0),   // offset.
+			u64.LeBytes(3),   // size of executable.
+			[]byte{1, 2, 3},
 		), actual)
 		require.NoError(t, content.Close())
 	})
diff --git a/internal/engine/compiler/engine_test.go b/internal/engine/compiler/engine_test.go
index fbf56af2..a006bbf9 100644
--- a/internal/engine/compiler/engine_test.go
+++ b/internal/engine/compiler/engine_test.go
@@ -145,14 +145,14 @@ func requireSupportedOSArch(t *testing.T) {
 	}
 }
 
-type fakeFinalizer map[*code]func(*code)
+type fakeFinalizer map[*compiledModule]func(module *compiledModule)
 
 func (f fakeFinalizer) setFinalizer(obj interface{}, finalizer interface{}) {
-	cf := obj.(*code)
+	cf := obj.(*compiledModule)
 	if _, ok := f[cf]; ok { // easier than adding a field for testing.T
 		panic(fmt.Sprintf("BUG: %v already had its finalizer set", cf))
 	}
-	f[cf] = finalizer.(func(*code))
+	f[cf] = finalizer.(func(*compiledModule))
 }
 
 func TestCompiler_CompileModule(t *testing.T) {
@@ -182,7 +182,7 @@ func TestCompiler_CompileModule(t *testing.T) {
 
 		compiled, ok := e.codes[okModule.ID]
 		require.True(t, ok)
-		require.Equal(t, len(okModule.FunctionSection), len(compiled))
+		require.Equal(t, len(okModule.FunctionSection), len(compiled.functions))
 
 		// Pretend the finalizer executed, by invoking them one-by-one.
 		for k, v := range ff {
@@ -213,16 +213,9 @@ func TestCompiler_CompileModule(t *testing.T) {
 	})
 }
 
-// TestCompiler_Releasecode_Panic tests that an unexpected panic has some identifying information in it.
 func TestCompiler_Releasecode_Panic(t *testing.T) {
-	captured := require.CapturePanic(func() {
-		releaseCode(&code{
-			indexInModule: 2,
-			sourceModule:  &wasm.Module{NameSection: &wasm.NameSection{ModuleName: t.Name()}},
-			codeSegment:   []byte{wasm.OpcodeEnd}, // never compiled means it was never mapped.
-		})
-	})
-	require.Contains(t, captured.Error(), fmt.Sprintf("compiler: failed to munmap code segment for %[1]s.function[2]", t.Name()))
+	captured := require.CapturePanic(func() { releaseCompiledModule(&compiledModule{executable: []byte{1, 2}}) })
+	require.Contains(t, captured.Error(), "compiler: failed to munmap code segment")
 }
 
 // Ensures that value stack and call-frame stack are allocated on heap which
@@ -374,17 +367,17 @@ func TestCallEngine_deferredOnCall(t *testing.T) {
 	f1 := &function{
 		def:      newMockFunctionDefinition("1"),
 		funcType: &wasm.FunctionType{ParamNumInUint64: 2},
-		parent:   &code{sourceModule: &wasm.Module{}},
+		parent:   &compiledFunction{parent: &compiledModule{source: &wasm.Module{}}},
 	}
 	f2 := &function{
 		def:      newMockFunctionDefinition("2"),
 		funcType: &wasm.FunctionType{ParamNumInUint64: 2, ResultNumInUint64: 3},
-		parent:   &code{sourceModule: &wasm.Module{}},
+		parent:   &compiledFunction{parent: &compiledModule{source: &wasm.Module{}}},
 	}
 	f3 := &function{
 		def:      newMockFunctionDefinition("3"),
 		funcType: &wasm.FunctionType{ResultNumInUint64: 1},
-		parent:   &code{sourceModule: &wasm.Module{}},
+		parent:   &compiledFunction{parent: &compiledModule{source: &wasm.Module{}}},
 	}
 
 	ce := &callEngine{
@@ -599,7 +592,7 @@ func TestCallEngine_builtinFunctionFunctionListenerBefore(t *testing.T) {
 	f := &function{
 		def:      def,
 		funcType: &wasm.FunctionType{ParamNumInUint64: 3},
-		parent: &code{
+		parent: &compiledFunction{
 			listener: mockListener{
 				before: func(ctx context.Context, _ api.Module, def api.FunctionDefinition, paramValues []uint64, stackIterator experimental.StackIterator) context.Context {
 					require.Equal(t, currentContext, ctx)
@@ -627,7 +620,7 @@ func TestCallEngine_builtinFunctionFunctionListenerAfter(t *testing.T) {
 	f := &function{
 		def:      newMockFunctionDefinition("1"),
 		funcType: &wasm.FunctionType{ResultNumInUint64: 1},
-		parent: &code{
+		parent: &compiledFunction{
 			listener: mockListener{
 				after: func(ctx context.Context, mod api.Module, def api.FunctionDefinition, err error, resultValues []uint64) {
 					require.Equal(t, currentContext, ctx)
@@ -718,7 +711,7 @@ func TestFunction_getSourceOffsetInWasmBinary(t *testing.T) {
 		tc := tc
 		t.Run(tc.name, func(t *testing.T) {
 			f := function{
-				parent:             &code{sourceOffsetMap: tc.srcMap},
+				parent:             &compiledFunction{sourceOffsetMap: tc.srcMap},
 				codeInitialAddress: tc.codeInitialAddress,
 			}
 
diff --git a/internal/engine/compiler/impl_amd64_test.go b/internal/engine/compiler/impl_amd64_test.go
index 2956c6f5..5f2540a0 100644
--- a/internal/engine/compiler/impl_amd64_test.go
+++ b/internal/engine/compiler/impl_amd64_test.go
@@ -37,9 +37,10 @@ func TestAmd64Compiler_indirectCallWithTargetOnCallingConvReg(t *testing.T) {
 		c, _, err := compiler.compile()
 		require.NoError(t, err)
 
+		executable := requireExecutable(c)
 		f := function{
-			parent:             &code{codeSegment: c},
-			codeInitialAddress: uintptr(unsafe.Pointer(&c[0])),
+			parent:             &compiledFunction{parent: &compiledModule{executable: executable}},
+			codeInitialAddress: uintptr(unsafe.Pointer(&executable[0])),
 			moduleInstance:     env.moduleInstance,
 			typeID:             0,
 		}
diff --git a/internal/engine/compiler/impl_arm64_test.go b/internal/engine/compiler/impl_arm64_test.go
index bad4f783..4b146257 100644
--- a/internal/engine/compiler/impl_arm64_test.go
+++ b/internal/engine/compiler/impl_arm64_test.go
@@ -34,9 +34,10 @@ func TestArm64Compiler_indirectCallWithTargetOnCallingConvReg(t *testing.T) {
 		c, _, err := compiler.compile()
 		require.NoError(t, err)
 
+		executable := requireExecutable(c)
 		f := function{
-			parent:             &code{codeSegment: c},
-			codeInitialAddress: uintptr(unsafe.Pointer(&c[0])),
+			parent:             &compiledFunction{parent: &compiledModule{executable: executable}},
+			codeInitialAddress: uintptr(unsafe.Pointer(&executable[0])),
 			moduleInstance:     env.moduleInstance,
 		}
 		me.functions = append(me.functions, f)