Repository: tetratelabs/wazero
Pull request #1878 (2023-12-18 20:11:54)

bench: add stdlib benchmark old vs new compiler
Creating in draft for early feedback

evacchi: notice that currently on CI these won't run as the corresponding `testdata` dir is empty!
evacchi: pushed a new version. It is likely to break due to old cache IDs. It is currently broken on Windows. My guess is because local, Windows-style paths are not compatible with the Unix-style wasm paths.

e.g. `C:\....\testdata\some.file` => while `./testdata/some.file` is expected
evacchi: too many issues with paths to write one test case that works also on Windows, I'll return to this later; let's see if it works on CI this way for now.

Patch:
diff --git a/.github/workflows/integration.yaml b/.github/workflows/integration.yaml
index ce71ab2f..39e88186 100644
--- a/.github/workflows/integration.yaml
+++ b/.github/workflows/integration.yaml
@@ -21,6 +21,8 @@ env:  # Update this prior to requiring a higher minor version in go.mod
   GO_VERSION: "1.21"  # 1.xx == latest patch of 1.xx
   TINYGO_VERSION: "0.30.0"
   ZIG_VERSION: "0.11.0"
+  BINARYEN_VERSION: "116"
+  STDLIB_TESTS: "internal/integration_test/stdlibs"
 
 concurrency:
   # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#example-using-concurrency-to-cancel-any-in-progress-job-or-run
@@ -36,6 +38,7 @@ jobs:
     env:
       ZIG_INSTALL: ~/zig-install
       ZIG_SOURCE: ~/zig-source
+      BINARYEN_INSTALL: ~/binaryen-install
 
     steps:
       - name: Checkout wazero
@@ -48,7 +51,7 @@ jobs:
           enableCrossOsArchive: true
           key: zig-stdlib-test-binary-${{ env.ZIG_VERSION }}
           path:
-            ./zigbin/
+            ${{ env.STDLIB_TESTS }}/testdata/zig
 
       - name: Install Zig build
         if: steps.binary-cache.outputs.cache-hit != 'true'
@@ -62,16 +65,20 @@ jobs:
           mkdir -p ${{ env.ZIG_SOURCE }}
           curl -sSL https://ziglang.org/download/${{ env.ZIG_VERSION }}/zig-${{ env.ZIG_VERSION }}.tar.xz | tar -xJ --strip-components=1 -C ${{ env.ZIG_SOURCE }}
 
+      - name: Install Binaryen build
+        if: steps.binary-cache.outputs.cache-hit != 'true'
+        run: |
+          mkdir -p ${{ env.BINARYEN_INSTALL }}
+          curl -sSL https://github.com/WebAssembly/binaryen/releases/download/version_${{ env.BINARYEN_VERSION }}/binaryen-version_${{ env.BINARYEN_VERSION }}-x86_64-linux.tar.gz | tar -xz --strip-components=1 -C ${{ env.BINARYEN_INSTALL }}
+
       - name: Build Stdlib test binary
         if: steps.binary-cache.outputs.cache-hit != 'true'
         # --test-no-exec allows building of the test Wasm binary without executing command.
         # We use find because the test.wasm will be something like ./zig-cache/o/dd6df1361b2134adc5eee9d027495436/test.wasm
         run: |
-          mkdir ${{ github.workspace }}/zigbin
-          cd ${{ env.ZIG_SOURCE }}
-          ${{ env.ZIG_INSTALL }}/zig test --test-no-exec -target wasm32-wasi --zig-lib-dir ./lib ./lib/std/std.zig
-          _ZIG_TEST_BINARY_PATH=$(find . -name test.wasm)
-          cp ${_ZIG_TEST_BINARY_PATH} ${{ github.workspace }}/zigbin/test.wasm
+          PATH=${{ env.ZIG_INSTALL }}:${{ env.BINARYEN_INSTALL }}/bin:$PATH
+          cd ${{ env.STDLIB_TESTS }}
+          make build.zig zigroot=${{ env.ZIG_SOURCE }}
 
   zig:
     needs: build_zig_test_binary
@@ -101,13 +108,14 @@ jobs:
           fail-on-cache-miss: true
           key: zig-stdlib-test-binary-${{ env.ZIG_VERSION }}
           path:
-            ./zigbin/
+            ${{ env.STDLIB_TESTS }}/testdata/zig
 
       - uses: actions/setup-go@v4
         with:
           go-version: ${{ env.GO_VERSION }}
 
-      - name: Build wazero
+      - if: ${{ matrix.compiler == 'optimizing' }}
+        name: Build wazero
         run: go build -o ./wazerocli ./cmd/wazero
         env:
           GOARCH: ${{ matrix.arch }}
@@ -136,12 +144,15 @@ jobs:
       - name: Run built test binaries (container)
         if: ${{ matrix.compiler == 'optimizing' }}
         run: |
-          docker run --platform linux/${{ matrix.arch }} -v $(pwd)/zigbin:/test -v $(pwd)/wazerocli:/wazero -e WAZEROCLI=/wazero --tmpfs /tmp --rm -t wazero:test \
+          docker run --platform linux/${{ matrix.arch }} -v $(pwd)/${{ env.STDLIB_TESTS }}/testdata/zig:/test -v $(pwd)/wazerocli:/wazero -e WAZEROCLI=/wazero --tmpfs /tmp --rm -t wazero:test \
             /wazero run -optimizing-compiler -mount=:/ ./test/test.wasm
 
       - name: Run built test binaries
         if: ${{ matrix.compiler != 'optimizing' }}
-        run: ./wazerocli run -mount=:/ ./zigbin/test.wasm
+        run: |
+          cd ${{ env.STDLIB_TESTS }}
+          go test -bench='./zig' -benchtime=1x
+
 
   build_tinygo_test_binary:
     name: Build TinyGo test binary
@@ -157,7 +168,7 @@ jobs:
           enableCrossOsArchive: true
           key: tinygo-test-binaries-${{ env.TINYGO_VERSION }}
           path:
-            ./tinygobin/
+            ${{ env.STDLIB_TESTS }}/testdata/tinygo
 
       - name: Install TinyGo
         if: steps.binary-cache.outputs.cache-hit != 'true'
@@ -181,48 +192,8 @@ jobs:
         #  - compress/zlib is skipped as it depends on the local files https://github.com/golang/go/blob/go1.20/src/compress/zlib/writer_test.go#L16-L30
         #  - debug/macho is skipped as it depends on the local files https://github.com/golang/go/blob/go1.20/src/debug/macho/file_test.go#L25
         run: |
-          mkdir ./tinygobin
-          for value in container/heap \
-            container/list \
-            container/ring \
-            crypto/des \
-            crypto/md5 \
-            crypto/rc4 \
-            crypto/sha1 \
-            crypto/sha256 \
-            crypto/sha512 \
-            embed/internal/embedtest \
-            encoding \
-            encoding/ascii85 \
-            encoding/base32 \
-            encoding/csv \
-            encoding/hex \
-            go/scanner \
-            hash \
-            hash/adler32 \
-            hash/crc64 \
-            hash/fnv \
-            html \
-            internal/itoa \
-            internal/profile \
-            math \
-            math/cmplx \
-            net \
-            net/http/internal/ascii \
-            net/mail \
-            os \
-            path \
-            reflect \
-            sync \
-            testing \
-            testing/iotest \
-            text/scanner \
-            unicode \
-            unicode/utf16 \
-            unicode/utf8
-          do
-            tinygo test -target wasi -c -o ./tinygobin/${value/\//_}.test $value
-          done
+          cd ${{ env.STDLIB_TESTS }}
+          make build.tinygo
 
   tinygo:
     needs: build_tinygo_test_binary
@@ -252,13 +223,14 @@ jobs:
           fail-on-cache-miss: true
           key: tinygo-test-binaries-${{ env.TINYGO_VERSION }}
           path:
-            ./tinygobin/
+            ${{ env.STDLIB_TESTS }}/testdata/tinygo
 
       - uses: actions/setup-go@v4
         with:
           go-version: ${{ env.GO_VERSION }}
 
-      - name: Build wazero
+      - if: ${{ matrix.compiler == 'optimizing' }}
+        name: Build wazero
         run: go build -o ~/wazerocli ./cmd/wazero
         env:
           GOARCH: ${{ matrix.arch }}
@@ -286,7 +258,7 @@ jobs:
         if: ${{ matrix.compiler == 'optimizing' }}
         # This runs all tests compiled above in sequence. Note: This mounts /tmp to allow t.TempDir() in tests.
         run: |
-          cd ./tinygobin
+          cd ${{ env.STDLIB_TESTS }}/testdata/tinygo
           for bin in *.test; do
             echo $bin
             docker run --platform linux/${{ matrix.arch }} -v $(pwd):/test -v ~/wazerocli:/wazero -e WAZEROCLI=/wazero --tmpfs /tmp --rm -t wazero:test \
@@ -296,11 +268,8 @@ jobs:
       - name: Run test binaries
         if: ${{ matrix.compiler != 'optimizing' }}
         run: |
-          cd ./tinygobin
-          for bin in *.test; do
-            echo $bin
-            ~/wazerocli run -mount=:/ -mount=:/tmp $bin -- -test.v
-          done
+          cd ${{ env.STDLIB_TESTS }}
+          go test -bench='./tinygo' -benchtime=1x
 
   wasi-testsuite:
     name: wasi-testsuite
@@ -413,96 +382,30 @@ jobs:
         with:
           go-version: ${{ matrix.go-version }}
 
+      - name: Checkout wazero
+        uses: actions/checkout@v3
+
       - name: Cache Go test binaries
         id: cache-go-test-binaries
         uses: actions/cache@v3
         with:
-          path: ~/tests
+          path:
+            ${{ env.STDLIB_TESTS }}/testdata/go
           # Use precise Go version from setup-go as patch version differences can effect tests.
           key: go-wasip1-binaries-${{ matrix.os }}-${{ steps.setup-go.outputs.go-version }}
 
-      - if: ${{ steps.cache-go-test-binaries.outputs.cache-hit != 'true' }}
-        name: Build Test Binaries
-        run: |
-          mkdir ~/tests
-          cd $(go env GOROOT)
-          # Choose important packages to limit execution time.
-          for value in src/archive/tar \
-            src/bufio \
-            src/bytes \
-            src/context \
-            src/encoding/ascii85 \
-            src/encoding/asn1 \
-            src/encoding/base32 \
-            src/encoding/base64 \
-            src/encoding/binary \
-            src/encoding/csv \
-            src/encoding/gob \
-            src/encoding/hex \
-            src/encoding/json \
-            src/encoding/pem \
-            src/encoding/xml \
-            src/errors \
-            src/expvar \
-            src/flag \
-            src/fmt \
-            src/hash \
-            src/hash/adler32 \
-            src/hash/crc32 \
-            src/hash/crc64 \
-            src/hash/fnv \
-            src/hash/maphash \
-            src/io \
-            src/io/fs \
-            src/io/ioutil \
-            src/log \
-            src/log/syslog \
-            src/maps \
-            src/math \
-            src/math/big \
-            src/math/bits \
-            src/math/cmplx \
-            src/math/rand \
-            src/mime \
-            src/mime/multipart \
-            src/mime/quotedprintable \
-            src/os \
-            src/os/exec \
-            src/os/signal \
-            src/os/user \
-            src/path \
-            src/reflect \
-            src/regexp \
-            src/regexp/syntax \
-            src/runtime \
-            src/runtime/internal/atomic \
-            src/runtime/internal/math \
-            src/runtime/internal/sys \
-            src/slices \
-            src/sort \
-            src/strconv \
-            src/strings \
-            src/sync \
-            src/sync/atomic \
-            src/syscall \
-            src/testing \
-            src/testing/fstest \
-            src/testing/iotest \
-            src/testing/quick \
-            src/time
-          do
-            echo "GOOS=wasip1 GOARCH=wasm go test -v -c -o ~/tests/${value//\//_}.test ./$value"
-            GOOS=wasip1 GOARCH=wasm go test -v -c -o ~/tests/${value//\//_}.test ./$value
-          done
-
-      - name: Checkout wazero
-        uses: actions/checkout@v3
-
-      - name: Build wazero
+      - if: ${{ matrix.compiler == 'optimizing' }}
+        name: Build wazero
         run: go build -o ~/wazerocli ./cmd/wazero
         env:
           GOARCH: ${{ matrix.arch }}
 
+      - if: ${{ steps.cache-go-test-binaries.outputs.cache-hit != 'true' }}
+        name: Build Test Binaries
+        run: |
+          cd ${{ env.STDLIB_TESTS }}
+          make build.gowasip1
+
       - name: Set up QEMU
         if: ${{ matrix.compiler == 'optimizing' && matrix.arch == 'arm64' }}
         uses: docker/setup-qemu-action@v2
@@ -520,12 +423,12 @@ jobs:
       - if: ${{ matrix.compiler == 'optimizing' }}
         name: Run test binaries (container)
         run: |
-          echo "Running $(find ~/tests -name *.test | wc -l) test binaries"
+          echo "Running $(find ${{ env.STDLIB_TESTS }}/testdata/go -name '*.test' | wc -l) test binaries"
 
           # Skip tests that are hideously slow (mostly compilation time) for running inside QEMU.
           skip_targets="src_encoding_json|src_runtime|src_os_exec|src_math_big|src_encoding_gob|src_time|src_archive_tar|src_math_rand|src_expvar|src_testing|src_os"
           # Go tests often look for files relative to the source. Change to the corresponding directory.
-          for bin in $(find ~/tests -name "*.test" | grep -vE "$skip_targets"); do
+          for bin in $(find $PWD/${{ env.STDLIB_TESTS }}/testdata/go -name '*.test' | grep -vE "$skip_targets"); do
             dir=$(basename $bin); dir=${dir%.test}; dir=${dir//_/\/}
             pushd $(go env GOROOT)/$dir
             # Mount / as /ROOT in docker and then remount /ROOT as / in wazero; $bin is now in /ROOT/$bin.
@@ -534,56 +437,8 @@ jobs:
             popd
           done
 
-      - if: ${{ matrix.compiler != 'optimizing' && runner.os != 'Windows' }}
-        name: Run test binaries
-        run: |
-          echo "Running $(find ~/tests -name *.test | wc -l) test binaries"
-
-          # Go tests often look for files relative to the source. Change to the corresponding directory.
-          for bin in ~/tests/*.test; do
-            dir=$(basename $bin); dir=${dir%.test}; dir=${dir//_/\/}
-            pushd $(go env GOROOT)/$dir
-            ~/wazerocli run -mount=/:/ -env PWD=$PWD $bin -- -test.short -test.v
-            popd
-          done
-
-      - if: ${{ runner.os == 'Windows' }}
-        name: Run test binaries (Windows)
-        # Ack failures on Windows. https://github.com/tetratelabs/wazero/issues/1410
-        continue-on-error: true
+      - name: Run built test binaries
+        if: ${{ matrix.compiler != 'optimizing' }}
         run: |
-          GOOS=$(go env GOOS)
-          echo "Running $(find ~/tests -name *.test | wc -l) test binaries"
-
-          MOUNT=c:\\:/
-          SCRIPT="$HOME/tests.cmd"
-          # Trim `/c` from the in-Wasm GOROOT.
-          REAL_GOROOT=$(go env GOROOT)
-          GOROOT=$(cygpath -u $REAL_GOROOT | cut -c 3-)
-          # Append early exit on cmd.
-          POSTFIX="if %errorlevel% neq 0 exit /b %errorlevel%"
-          RUNNER="cmd //c %USERPROFILE%\tests.cmd"
-          EXTRAPARAMS="-mount=%TEMP%:/tmp"
-
-          # Go tests often look for files relative to the source. Change to the corresponding directory.
-          for bin in ~/tests/*.test; do
-            dir=$(basename $bin); dir=${dir%.test}; dir=${dir//_/\/}
-            pushd $REAL_GOROOT/$dir
-
-            # Trim `/c` from the in-Wasm pwd.
-            IN_WASM_PWD=$(pwd | cut -c 3-)
-            # Convert to a Windows path.
-            bin=`cygpath -w $bin`
-
-            # Create a script with all the tests (do not run yet).
-            echo ${MOUNT} ${IN_WASM_PWD} $GOROOT/$dir
-            COMMAND="~/wazerocli run -mount=${MOUNT} ${EXTRAPARAMS} -hostlogging=filesystem -env PWD=${IN_WASM_PWD} -env GOROOT=${GOROOT} -env GOOS=wasip1 $bin -- -test.short -test.v"
-            echo $COMMAND >> $SCRIPT
-            # Uncomment the following line for early exit on error on Windows.
-            # Otherwise the tests will report are successful evne on failure.
-            # echo $POSTFIX >> $SCRIPT
-            popd
-          done
-
-          # Run all the tests in their own shell.
-          $RUNNER
+          cd ${{ env.STDLIB_TESTS }}
+          go test -bench='./wasip1' -benchtime=1x
diff --git a/.gitignore b/.gitignore
index f824fdde..1d635c36 100644
--- a/.gitignore
+++ b/.gitignore
@@ -39,3 +39,6 @@ zig-cache/
 zig-out/
 
 .DS_Store
+
+# Ignore compiled stdlib test cases.
+/internal/integration_test/stdlibs/testdata
diff --git a/internal/engine/wazevo/backend/compiler.go b/internal/engine/wazevo/backend/compiler.go
index 0896ef4f..2c0e3cb8 100644
--- a/internal/engine/wazevo/backend/compiler.go
+++ b/internal/engine/wazevo/backend/compiler.go
@@ -50,7 +50,7 @@ type Compiler interface {
 	RegAlloc()
 
 	// Finalize performs the finalization of the compilation. This must be called after RegAlloc.
-	Finalize()
+	Finalize(ctx context.Context)
 
 	// Encode encodes the machine code to the buffer.
 	Encode()
@@ -150,21 +150,21 @@ type SourceOffsetInfo struct {
 // Compile implements Compiler.Compile.
 func (c *compiler) Compile(ctx context.Context) ([]byte, []RelocationInfo, error) {
 	c.Lower()
-	if wazevoapi.PrintSSAToBackendIRLowering {
+	if wazevoapi.PrintSSAToBackendIRLowering && wazevoapi.PrintEnabledIndex(ctx) {
 		fmt.Printf("[[[after lowering for %s ]]]%s\n", wazevoapi.GetCurrentFunctionName(ctx), c.Format())
 	}
 	if wazevoapi.DeterministicCompilationVerifierEnabled {
 		wazevoapi.VerifyOrSetDeterministicCompilationContextValue(ctx, "After lowering to ISA specific IR", c.Format())
 	}
 	c.RegAlloc()
-	if wazevoapi.PrintRegisterAllocated {
+	if wazevoapi.PrintRegisterAllocated && wazevoapi.PrintEnabledIndex(ctx) {
 		fmt.Printf("[[[after regalloc for %s]]]%s\n", wazevoapi.GetCurrentFunctionName(ctx), c.Format())
 	}
 	if wazevoapi.DeterministicCompilationVerifierEnabled {
 		wazevoapi.VerifyOrSetDeterministicCompilationContextValue(ctx, "After Register Allocation", c.Format())
 	}
-	c.Finalize()
-	if wazevoapi.PrintFinalizedMachineCode {
+	c.Finalize(ctx)
+	if wazevoapi.PrintFinalizedMachineCode && wazevoapi.PrintEnabledIndex(ctx) {
 		fmt.Printf("[[[after finalize for %s]]]%s\n", wazevoapi.GetCurrentFunctionName(ctx), c.Format())
 	}
 	if wazevoapi.DeterministicCompilationVerifierEnabled {
@@ -184,10 +184,10 @@ func (c *compiler) RegAlloc() {
 }
 
 // Finalize implements Compiler.Finalize.
-func (c *compiler) Finalize() {
+func (c *compiler) Finalize(ctx context.Context) {
 	c.mach.SetupPrologue()
 	c.mach.SetupEpilogue()
-	c.mach.ResolveRelativeAddresses()
+	c.mach.ResolveRelativeAddresses(ctx)
 }
 
 // Encode implements Compiler.Encode.
diff --git a/internal/engine/wazevo/backend/isa/arm64/lower_constant.go b/internal/engine/wazevo/backend/isa/arm64/lower_constant.go
index 5500d4cb..5516c0b3 100644
--- a/internal/engine/wazevo/backend/isa/arm64/lower_constant.go
+++ b/internal/engine/wazevo/backend/isa/arm64/lower_constant.go
@@ -59,7 +59,7 @@ func (m *machine) lowerConstantI32(dst regalloc.VReg, c int32) {
 	// https://github.com/golang/go/blob/release-branch.go1.15/src/cmd/internal/obj/arm64/asm7.go#L1637
 	ic := int64(uint32(c))
 	if ic >= 0 && (ic <= 0xfff || (ic&0xfff) == 0 && (uint64(ic>>12) <= 0xfff)) {
-		if isBitMaskImmediate(uint64(c)) {
+		if isBitMaskImmediate(uint64(c), false) {
 			m.lowerConstViaBitMaskImmediate(uint64(uint32(c)), dst, false)
 			return
 		}
@@ -72,7 +72,7 @@ func (m *machine) lowerConstantI32(dst regalloc.VReg, c int32) {
 	} else if t := const16bitAligned(int64(^c)); t >= 0 {
 		// Also, if the inverse of the const can fit within 16-bit range, do the same ^^.
 		m.insertMOVN(dst, uint64(^c>>(16*t)), t, false)
-	} else if isBitMaskImmediate(uint64(uint32(c))) {
+	} else if isBitMaskImmediate(uint64(uint32(c)), false) {
 		m.lowerConstViaBitMaskImmediate(uint64(c), dst, false)
 	} else {
 		// Otherwise, we use MOVZ and MOVK to load it.
@@ -87,7 +87,7 @@ func (m *machine) lowerConstantI64(dst regalloc.VReg, c int64) {
 	// Following the logic here:
 	// https://github.com/golang/go/blob/release-branch.go1.15/src/cmd/internal/obj/arm64/asm7.go#L1798-L1852
 	if c >= 0 && (c <= 0xfff || (c&0xfff) == 0 && (uint64(c>>12) <= 0xfff)) {
-		if isBitMaskImmediate(uint64(c)) {
+		if isBitMaskImmediate(uint64(c), true) {
 			m.lowerConstViaBitMaskImmediate(uint64(c), dst, true)
 			return
 		}
@@ -100,7 +100,7 @@ func (m *machine) lowerConstantI64(dst regalloc.VReg, c int64) {
 	} else if t := const16bitAligned(^c); t >= 0 {
 		// Also, if the reverse of the const can fit within 16-bit range, do the same ^^.
 		m.insertMOVN(dst, uint64(^c)>>(16*t), t, true)
-	} else if isBitMaskImmediate(uint64(c)) {
+	} else if isBitMaskImmediate(uint64(c), true) {
 		m.lowerConstViaBitMaskImmediate(uint64(c), dst, true)
 	} else {
 		m.load64bitConst(c, dst)
@@ -119,9 +119,9 @@ func (m *machine) lowerConstViaBitMaskImmediate(c uint64, dst regalloc.VReg, b64
 //	Each element contains the same sub-pattern: a single run of 1 to e-1 non-zero bits, rotated by 0 to e-1 bits.
 //
 // See https://developer.arm.com/documentation/dui0802/b/A64-General-Instructions/MOV--bitmask-immediate-
-func isBitMaskImmediate(x uint64) bool {
+func isBitMaskImmediate(x uint64, _64 bool) bool {
 	// All zeros and ones are not "bitmask immediate" by definition.
-	if x == 0 || x == 0xffff_ffff_ffff_ffff {
+	if x == 0 || (_64 && x == 0xffff_ffff_ffff_ffff) || (!_64 && x == 0xffff_ffff) {
 		return false
 	}
 
diff --git a/internal/engine/wazevo/backend/isa/arm64/lower_instr.go b/internal/engine/wazevo/backend/isa/arm64/lower_instr.go
index 74484d1d..98af8681 100644
--- a/internal/engine/wazevo/backend/isa/arm64/lower_instr.go
+++ b/internal/engine/wazevo/backend/isa/arm64/lower_instr.go
@@ -1593,17 +1593,25 @@ func (m *machine) lowerShifts(si *ssa.Instruction, ext extMode, aluOp aluOp) {
 
 func (m *machine) lowerBitwiseAluOp(si *ssa.Instruction, op aluOp) {
 	x, y := si.Arg2()
-	if !x.Type().IsInt() {
-		panic("BUG?")
-	}
 
 	xDef, yDef := m.compiler.ValueDefinition(x), m.compiler.ValueDefinition(y)
 	rn := m.getOperand_NR(xDef, extModeNone)
-	rm := m.getOperand_SR_NR(yDef, extModeNone)
 	rd := operandNR(m.compiler.VRegOf(si.Return()))
 
+	_64 := x.Type().Bits() == 64
 	alu := m.allocateInstr()
-	alu.asALU(op, rd, rn, rm, si.Return().Type().Bits() == 64)
+	if instr := yDef.Instr; instr != nil && instr.Constant() {
+		c := instr.ConstantVal()
+		if isBitMaskImmediate(c, _64) {
+			// Constant bit wise operations can be lowered to a single instruction.
+			alu.asALUBitmaskImm(op, rd.nr(), rn.nr(), c, _64)
+			m.insert(alu)
+			return
+		}
+	}
+
+	rm := m.getOperand_SR_NR(yDef, extModeNone)
+	alu.asALU(op, rd, rn, rm, _64)
 	m.insert(alu)
 }
 
diff --git a/internal/engine/wazevo/backend/isa/arm64/machine.go b/internal/engine/wazevo/backend/isa/arm64/machine.go
index ecd9d0cc..821bdfb8 100644
--- a/internal/engine/wazevo/backend/isa/arm64/machine.go
+++ b/internal/engine/wazevo/backend/isa/arm64/machine.go
@@ -1,6 +1,7 @@
 package arm64
 
 import (
+	"context"
 	"fmt"
 	"math"
 	"strings"
@@ -355,7 +356,7 @@ func (m *machine) resolveAddressingMode(arg0offset, ret0offset int64, i *instruc
 }
 
 // ResolveRelativeAddresses implements backend.Machine.
-func (m *machine) ResolveRelativeAddresses() {
+func (m *machine) ResolveRelativeAddresses(ctx context.Context) {
 	if len(m.unresolvedAddressModes) > 0 {
 		arg0offset, ret0offset := m.arg0OffsetFromSP(), m.ret0OffsetFromSP()
 		for _, i := range m.unresolvedAddressModes {
@@ -366,6 +367,18 @@ func (m *machine) ResolveRelativeAddresses() {
 	// Reuse the slice to gather the unresolved conditional branches.
 	cbrs := m.condBrRelocs[:0]
 
+	var fn string
+	var fnIndex int
+	var labelToSSABlockID map[label]ssa.BasicBlockID
+	if wazevoapi.PerfMapEnabled {
+		fn = wazevoapi.GetCurrentFunctionName(ctx)
+		labelToSSABlockID = make(map[label]ssa.BasicBlockID)
+		for i, l := range m.ssaBlockIDToLabels {
+			labelToSSABlockID[l] = ssa.BasicBlockID(i)
+		}
+		fnIndex = wazevoapi.GetCurrentFunctionIndex(ctx)
+	}
+
 	// Next, in order to determine the offsets of relative jumps, we have to calculate the size of each label.
 	var offset int64
 	for i, pos := range m.orderedBlockLabels {
@@ -397,6 +410,20 @@ func (m *machine) ResolveRelativeAddresses() {
 				break
 			}
 		}
+
+		if wazevoapi.PerfMapEnabled {
+			if size > 0 {
+				l := pos.l
+				var labelStr string
+				if blkID, ok := labelToSSABlockID[l]; ok {
+					labelStr = fmt.Sprintf("%s::SSA_Block[%s]", l, blkID)
+				} else {
+					labelStr = l.String()
+				}
+				wazevoapi.PerfMap.AddModuleEntry(fnIndex, offset, uint64(size), fmt.Sprintf("%s:::::%s", fn, labelStr))
+			}
+		}
+
 		pos.binarySize = size
 		offset += size
 	}
@@ -421,7 +448,10 @@ func (m *machine) ResolveRelativeAddresses() {
 		}
 	}
 	if needRerun {
-		m.ResolveRelativeAddresses()
+		m.ResolveRelativeAddresses(ctx)
+		if wazevoapi.PerfMapEnabled {
+			wazevoapi.PerfMap.Clear()
+		}
 		return
 	}
 
diff --git a/internal/engine/wazevo/backend/machine.go b/internal/engine/wazevo/backend/machine.go
index 2772f50d..c94ac577 100644
--- a/internal/engine/wazevo/backend/machine.go
+++ b/internal/engine/wazevo/backend/machine.go
@@ -1,6 +1,8 @@
 package backend
 
 import (
+	"context"
+
 	"github.com/tetratelabs/wazero/internal/engine/wazevo/backend/regalloc"
 	"github.com/tetratelabs/wazero/internal/engine/wazevo/ssa"
 	"github.com/tetratelabs/wazero/internal/engine/wazevo/wazevoapi"
@@ -89,7 +91,7 @@ type (
 
 		// ResolveRelativeAddresses resolves the relative addresses after register allocations and prologue/epilogue setup.
 		// After this, the compiler is finally ready to emit machine code.
-		ResolveRelativeAddresses()
+		ResolveRelativeAddresses(ctx context.Context)
 
 		// ResolveRelocations resolves the relocations after emitting machine code.
 		ResolveRelocations(refToBinaryOffset map[ssa.FuncRef]int, binary []byte, relocations []RelocationInfo)
diff --git a/internal/engine/wazevo/engine.go b/internal/engine/wazevo/engine.go
index aaabf9ef..faa1330f 100644
--- a/internal/engine/wazevo/engine.go
+++ b/internal/engine/wazevo/engine.go
@@ -116,6 +116,11 @@ func NewEngine(ctx context.Context, _ api.CoreFeatures, fc filecache.Cache) wasm
 
 // CompileModule implements wasm.Engine.
 func (e *engine) CompileModule(ctx context.Context, module *wasm.Module, listeners []experimental.FunctionListener, ensureTermination bool) (err error) {
+	if wazevoapi.PerfMapEnabled {
+		wazevoapi.PerfMap.Lock()
+		defer wazevoapi.PerfMap.Unlock()
+	}
+
 	if _, ok, err := e.getCompiledModule(module, listeners, ensureTermination); ok { // cache hit!
 		return nil
 	} else if err != nil {
@@ -165,6 +170,11 @@ func (exec *executables) compileEntryPreambles(m *wasm.Module, machine backend.M
 		buf := machine.CompileEntryPreamble(&sig)
 		executable := mmapExecutable(buf)
 		exec.entryPreambles[i] = executable
+
+		if wazevoapi.PerfMapEnabled {
+			wazevoapi.PerfMap.AddEntry(uintptr(unsafe.Pointer(&executable[0])),
+				uint64(len(executable)), fmt.Sprintf("entry_preamble::type=%s", typ.String()))
+		}
 	}
 }
 
@@ -217,7 +227,7 @@ func (e *engine) compileModule(ctx context.Context, module *wasm.Module, listene
 			if len(def.ExportNames()) > 0 {
 				name = def.ExportNames()[0]
 			}
-			ctx = wazevoapi.SetCurrentFunctionName(ctx, fmt.Sprintf("[%d/%d] \"%s\"", i, len(module.CodeSection)-1, name))
+			ctx = wazevoapi.SetCurrentFunctionName(ctx, i, fmt.Sprintf("[%d/%d]%s", i, len(module.CodeSection)-1, name))
 		}
 
 		needListener := len(listeners) > 0 && listeners[i] != nil
@@ -271,6 +281,10 @@ func (e *engine) compileModule(ctx context.Context, module *wasm.Module, listene
 		copy(executable[offset:], b)
 	}
 
+	if wazevoapi.PerfMapEnabled {
+		wazevoapi.PerfMap.Flush(uintptr(unsafe.Pointer(&executable[0])), cm.functionOffsets)
+	}
+
 	if needSourceInfo {
 		for i := range cm.sourceMap.executableOffsets {
 			cm.sourceMap.executableOffsets[i] += uintptr(unsafe.Pointer(&cm.executable[0]))
@@ -310,7 +324,7 @@ func (e *engine) compileLocalWasmFunction(
 
 	// Lower Wasm to SSA.
 	fe.LowerToSSA()
-	if wazevoapi.PrintSSA {
+	if wazevoapi.PrintSSA && wazevoapi.PrintEnabledIndex(ctx) {
 		fmt.Printf("[[[SSA for %s]]]%s\n", wazevoapi.GetCurrentFunctionName(ctx), ssaBuilder.Format())
 	}
 
@@ -321,7 +335,7 @@ func (e *engine) compileLocalWasmFunction(
 	// Run SSA-level optimization passes.
 	ssaBuilder.RunPasses()
 
-	if wazevoapi.PrintOptimizedSSA {
+	if wazevoapi.PrintOptimizedSSA && wazevoapi.PrintEnabledIndex(ctx) {
 		fmt.Printf("[[[Optimized SSA for %s]]]%s\n", wazevoapi.GetCurrentFunctionName(ctx), ssaBuilder.Format())
 	}
 
@@ -332,7 +346,7 @@ func (e *engine) compileLocalWasmFunction(
 	// Finalize the layout of SSA blocks which might use the optimization results.
 	ssaBuilder.LayoutBlocks()
 
-	if wazevoapi.PrintBlockLaidOutSSA {
+	if wazevoapi.PrintBlockLaidOutSSA && wazevoapi.PrintEnabledIndex(ctx) {
 		fmt.Printf("[[[Laidout SSA for %s]]]%s\n", wazevoapi.GetCurrentFunctionName(ctx), ssaBuilder.Format())
 	}
 
@@ -411,6 +425,14 @@ func (e *engine) compileHostModule(ctx context.Context, module *wasm.Module, lis
 		be.Encode()
 		body := be.Buf()
 
+		if wazevoapi.PerfMapEnabled {
+			name := module.FunctionDefinition(wasm.Index(i)).DebugName()
+			wazevoapi.PerfMap.AddModuleEntry(i,
+				int64(totalSize),
+				uint64(len(body)),
+				fmt.Sprintf("trampoline:%s", name))
+		}
+
 		// TODO: optimize as zero copy.
 		copied := make([]byte, len(body))
 		copy(copied, body)
@@ -430,6 +452,10 @@ func (e *engine) compileHostModule(ctx context.Context, module *wasm.Module, lis
 		copy(executable[offset:], b)
 	}
 
+	if wazevoapi.PerfMapEnabled {
+		wazevoapi.PerfMap.Flush(uintptr(unsafe.Pointer(&executable[0])), cm.functionOffsets)
+	}
+
 	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 {
@@ -556,6 +582,10 @@ func (e *engine) compileSharedFunctions() {
 			Results: []ssa.Type{ssa.TypeI32},
 		}, false)
 		e.sharedFunctions.memoryGrowExecutable = mmapExecutable(src)
+		if wazevoapi.PerfMapEnabled {
+			exe := e.sharedFunctions.memoryGrowExecutable
+			wazevoapi.PerfMap.AddEntry(uintptr(unsafe.Pointer(&exe[0])), uint64(len(exe)), "memory_grow_trampoline")
+		}
 	}
 
 	e.be.Init()
@@ -565,6 +595,10 @@ func (e *engine) compileSharedFunctions() {
 			Results: []ssa.Type{ssa.TypeI32},
 		}, false)
 		e.sharedFunctions.tableGrowExecutable = mmapExecutable(src)
+		if wazevoapi.PerfMapEnabled {
+			exe := e.sharedFunctions.tableGrowExecutable
+			wazevoapi.PerfMap.AddEntry(uintptr(unsafe.Pointer(&exe[0])), uint64(len(exe)), "table_grow_trampoline")
+		}
 	}
 
 	e.be.Init()
@@ -574,6 +608,10 @@ func (e *engine) compileSharedFunctions() {
 			Results: []ssa.Type{ssa.TypeI32},
 		}, false)
 		e.sharedFunctions.checkModuleExitCode = mmapExecutable(src)
+		if wazevoapi.PerfMapEnabled {
+			exe := e.sharedFunctions.checkModuleExitCode
+			wazevoapi.PerfMap.AddEntry(uintptr(unsafe.Pointer(&exe[0])), uint64(len(exe)), "check_module_exit_code_trampoline")
+		}
 	}
 
 	e.be.Init()
@@ -583,12 +621,20 @@ func (e *engine) compileSharedFunctions() {
 			Results: []ssa.Type{ssa.TypeI64}, // returns the function reference.
 		}, false)
 		e.sharedFunctions.refFuncExecutable = mmapExecutable(src)
+		if wazevoapi.PerfMapEnabled {
+			exe := e.sharedFunctions.refFuncExecutable
+			wazevoapi.PerfMap.AddEntry(uintptr(unsafe.Pointer(&exe[0])), uint64(len(exe)), "ref_func_trampoline")
+		}
 	}
 
 	e.be.Init()
 	{
 		src := e.machine.CompileStackGrowCallSequence()
 		e.sharedFunctions.stackGrowExecutable = mmapExecutable(src)
+		if wazevoapi.PerfMapEnabled {
+			exe := e.sharedFunctions.stackGrowExecutable
+			wazevoapi.PerfMap.AddEntry(uintptr(unsafe.Pointer(&exe[0])), uint64(len(exe)), "stack_grow_trampoline")
+		}
 	}
 
 	e.setFinalizer(e.sharedFunctions, sharedFunctionsFinalizer)
diff --git a/internal/engine/wazevo/frontend/frontend.go b/internal/engine/wazevo/frontend/frontend.go
index 1c54c8b4..ac92a376 100644
--- a/internal/engine/wazevo/frontend/frontend.go
+++ b/internal/engine/wazevo/frontend/frontend.go
@@ -50,9 +50,17 @@ type Compiler struct {
 	br            *bytes.Reader
 	loweringState loweringState
 
+	knownSafeBounds    []knownSafeBound
+	knownSafeBoundsSet []ssa.ValueID
+
 	execCtxPtrValue, moduleCtxPtrValue ssa.Value
 }
 
+type knownSafeBound struct {
+	bound        uint64
+	absoluteAddr ssa.Value
+}
+
 // NewFrontendCompiler returns a frontend Compiler.
 func NewFrontendCompiler(m *wasm.Module, ssaBuilder ssa.Builder, offset *wazevoapi.ModuleContextOffsetData, ensureTermination bool, listenerOn bool, sourceInfo bool) *Compiler {
 	c := &Compiler{
@@ -354,3 +362,42 @@ func SignatureForListener(wasmSig *wasm.FunctionType) (*ssa.Signature, *ssa.Sign
 	}
 	return beforeSig, afterSig
 }
+
+// isBoundSafe returns true if the given value is known to be safe to access up to the given bound.
+func (c *Compiler) getKnownSafeBound(v ssa.ValueID) *knownSafeBound {
+	if int(v) >= len(c.knownSafeBounds) {
+		return nil
+	}
+	return &c.knownSafeBounds[v]
+}
+
+// recordKnownSafeBound records the given safe bound for the given value.
+func (c *Compiler) recordKnownSafeBound(v ssa.ValueID, safeBound uint64, absoluteAddr ssa.Value) {
+	if int(v) >= len(c.knownSafeBounds) {
+		c.knownSafeBounds = append(c.knownSafeBounds, make([]knownSafeBound, v+1)...)
+	}
+
+	if exiting := c.knownSafeBounds[v]; exiting.bound == 0 {
+		c.knownSafeBounds[v] = knownSafeBound{
+			bound:        safeBound,
+			absoluteAddr: absoluteAddr,
+		}
+		c.knownSafeBoundsSet = append(c.knownSafeBoundsSet, v)
+	} else if safeBound > exiting.bound {
+		c.knownSafeBounds[v].bound = safeBound
+	}
+}
+
+// clearSafeBounds clears the known safe bounds. This must be called
+// after the compilation of each block.
+func (c *Compiler) clearSafeBounds() {
+	for _, v := range c.knownSafeBoundsSet {
+		ptr := &c.knownSafeBounds[v]
+		ptr.bound = 0
+	}
+	c.knownSafeBoundsSet = c.knownSafeBoundsSet[:0]
+}
+
+func (k *knownSafeBound) valid() bool {
+	return k != nil && k.bound > 0
+}
diff --git a/internal/engine/wazevo/frontend/lower.go b/internal/engine/wazevo/frontend/lower.go
index 0816910e..2b2de518 100644
--- a/internal/engine/wazevo/frontend/lower.go
+++ b/internal/engine/wazevo/frontend/lower.go
@@ -1416,6 +1416,8 @@ func (c *Compiler) lowerCurrentOpcode() {
 		builder.Seal(thenBlk)
 		builder.Seal(elseBlk)
 	case wasm.OpcodeElse:
+		c.clearSafeBounds() // Reset the safe bounds since we are entering the Else block.
+
 		ifctrl := state.ctrlPeekAt(0)
 		if unreachable := state.unreachable; unreachable && state.unreachableDepth > 0 {
 			// If it is currently in unreachable and is a nested if,
@@ -1443,6 +1445,8 @@ func (c *Compiler) lowerCurrentOpcode() {
 		builder.SetCurrentBlock(elseBlk)
 
 	case wasm.OpcodeEnd:
+		c.clearSafeBounds() // Reset the safe bounds since we are exiting the block.
+
 		if state.unreachableDepth > 0 {
 			state.unreachableDepth--
 			break
@@ -3368,24 +3372,35 @@ func (c *Compiler) lowerCallIndirect(typeIndex, tableIndex uint32) {
 
 // memOpSetup inserts the bounds check and calculates the address of the memory operation (loads/stores).
 func (c *Compiler) memOpSetup(baseAddr ssa.Value, constOffset, operationSizeInBytes uint64) (address ssa.Value) {
+	address = ssa.ValueInvalid
 	builder := c.ssaBuilder
 
+	baseAddrID := baseAddr.ID()
 	ceil := constOffset + operationSizeInBytes
+	if known := c.getKnownSafeBound(baseAddrID); known.valid() {
+		// We reuse the calculated absolute address even if the bound is not known to be safe.
+		address = known.absoluteAddr
+		if ceil <= known.bound {
+			return
+		}
+	}
+
 	ceilConst := builder.AllocateInstruction()
 	ceilConst.AsIconst64(ceil)
 	builder.InsertInstruction(ceilConst)
 
 	// We calculate the offset in 64-bit space.
-	extBaseAddr := builder.AllocateInstruction()
-	extBaseAddr.AsUExtend(baseAddr, 32, 64)
-	builder.InsertInstruction(extBaseAddr)
+	extBaseAddr := builder.AllocateInstruction().
+		AsUExtend(baseAddr, 32, 64).
+		Insert(builder).
+		Return()
 
 	// Note: memLen is already zero extended to 64-bit space at the load time.
 	memLen := c.getMemoryLenValue(false)
 
 	// baseAddrPlusCeil = baseAddr + ceil
 	baseAddrPlusCeil := builder.AllocateInstruction()
-	baseAddrPlusCeil.AsIadd(extBaseAddr.Return(), ceilConst.Return())
+	baseAddrPlusCeil.AsIadd(extBaseAddr, ceilConst.Return())
 	builder.InsertInstruction(baseAddrPlusCeil)
 
 	// Check for out of bounds memory access: `memLen >= baseAddrPlusCeil`.
@@ -3397,11 +3412,15 @@ func (c *Compiler) memOpSetup(baseAddr ssa.Value, constOffset, operationSizeInBy
 	builder.InsertInstruction(exitIfNZ)
 
 	// Load the value from memBase + extBaseAddr.
-	memBase := c.getMemoryBaseValue(false)
-	addrCalc := builder.AllocateInstruction()
-	addrCalc.AsIadd(memBase, extBaseAddr.Return())
-	builder.InsertInstruction(addrCalc)
-	return addrCalc.Return()
+	if address == ssa.ValueInvalid { // Reuse the value if the memBase is already calculated at this point.
+		memBase := c.getMemoryBaseValue(false)
+		address = builder.AllocateInstruction().
+			AsIadd(memBase, extBaseAddr).Insert(builder).Return()
+	}
+
+	// Record the bound ceil for this baseAddr is known to be safe for the subsequent memory access in the same block.
+	c.recordKnownSafeBound(baseAddrID, ceil, address)
+	return
 }
 
 func (c *Compiler) callMemmove(dst, src, size ssa.Value) {
@@ -3434,6 +3453,10 @@ func (c *Compiler) reloadAfterCall() {
 func (c *Compiler) reloadMemoryBaseLen() {
 	_ = c.getMemoryBaseValue(true)
 	_ = c.getMemoryLenValue(true)
+
+	// This function being called means that the memory base might have changed.
+	// Therefore, we need to clear the known safe bounds because we cache the absolute address of the memory access per each base offset.
+	c.clearSafeBounds()
 }
 
 // globalInstanceValueOffset is the offsetOf .Value field of wasm.GlobalInstance.
diff --git a/internal/engine/wazevo/wazevoapi/debug_options.go b/internal/engine/wazevo/wazevoapi/debug_options.go
index 01bfff50..73b69c0d 100644
--- a/internal/engine/wazevo/wazevoapi/debug_options.go
+++ b/internal/engine/wazevo/wazevoapi/debug_options.go
@@ -40,6 +40,18 @@ const (
 	PrintMachineCodeHexPerFunctionDisassemblable = false
 )
 
+// printTarget is the function index to print the machine code. This is used for debugging to print the machine code
+// of a specific function.
+const printTarget = -1
+
+// PrintEnabledIndex returns true if the current function index is the print target.
+func PrintEnabledIndex(ctx context.Context) bool {
+	if printTarget == -1 {
+		return true
+	}
+	return GetCurrentFunctionIndex(ctx) == printTarget
+}
+
 // ----- Validations -----
 const (
 	// SSAValidationEnabled enables the SSA validation. This is disabled by default since the operation is expensive.
@@ -84,6 +96,7 @@ type (
 	}
 	verifierStateContextKey struct{}
 	currentFunctionNameKey  struct{}
+	currentFunctionIndexKey struct{}
 )
 
 // NewDeterministicCompilationVerifierContext creates a new context with the deterministic compilation verifier used per wasm.Module.
@@ -162,16 +175,26 @@ const NeedFunctionNameInContext = PrintSSA ||
 	PrintRegisterAllocated ||
 	PrintFinalizedMachineCode ||
 	PrintMachineCodeHexPerFunction ||
-	DeterministicCompilationVerifierEnabled
+	DeterministicCompilationVerifierEnabled ||
+	PerfMapEnabled
 
 // SetCurrentFunctionName sets the current function name to the given `functionName`.
-func SetCurrentFunctionName(ctx context.Context, functionName string) context.Context {
-	return context.WithValue(ctx, currentFunctionNameKey{}, functionName)
+func SetCurrentFunctionName(ctx context.Context, index int, functionName string) context.Context {
+	ctx = context.WithValue(ctx, currentFunctionNameKey{}, functionName)
+	ctx = context.WithValue(ctx, currentFunctionIndexKey{}, index)
+	return ctx
 }
 
 // GetCurrentFunctionName returns the current function name.
 func GetCurrentFunctionName(ctx context.Context) string {
-	return ctx.Value(currentFunctionNameKey{}).(string)
+	ret, _ := ctx.Value(currentFunctionNameKey{}).(string)
+	return ret
+}
+
+// GetCurrentFunctionIndex returns the current function index.
+func GetCurrentFunctionIndex(ctx context.Context) int {
+	ret, _ := ctx.Value(currentFunctionIndexKey{}).(int)
+	return ret
 }
 
 // ----- High Register Pressure -----
diff --git a/internal/engine/wazevo/wazevoapi/perfmap.go b/internal/engine/wazevo/wazevoapi/perfmap.go
new file mode 100644
index 00000000..642c7f75
--- /dev/null
+++ b/internal/engine/wazevo/wazevoapi/perfmap.go
@@ -0,0 +1,96 @@
+package wazevoapi
+
+import (
+	"fmt"
+	"os"
+	"strconv"
+	"sync"
+)
+
+var PerfMap *Perfmap
+
+func init() {
+	if PerfMapEnabled {
+		pid := os.Getpid()
+		filename := "/tmp/perf-" + strconv.Itoa(pid) + ".map"
+
+		fh, err := os.OpenFile(filename, os.O_APPEND|os.O_RDWR|os.O_CREATE, 0o644)
+		if err != nil {
+			panic(err)
+		}
+
+		PerfMap = &Perfmap{fh: fh}
+	}
+}
+
+// Perfmap holds perfmap entries to be flushed into a perfmap file.
+type Perfmap struct {
+	entries []entry
+	mux     sync.Mutex
+	fh      *os.File
+}
+
+type entry struct {
+	index  int
+	offset int64
+	size   uint64
+	name   string
+}
+
+func (f *Perfmap) Lock() {
+	f.mux.Lock()
+}
+
+func (f *Perfmap) Unlock() {
+	f.mux.Unlock()
+}
+
+// AddModuleEntry adds a perfmap entry into the perfmap file.
+// index is the index of the function in the module, offset is the offset of the function in the module,
+// size is the size of the function, and name is the name of the function.
+//
+// Note that the entries are not flushed into the perfmap file until Flush is called,
+// and the entries are module-scoped; Perfmap must be locked until Flush is called.
+func (f *Perfmap) AddModuleEntry(index int, offset int64, size uint64, name string) {
+	e := entry{index: index, offset: offset, size: size, name: name}
+	if f.entries == nil {
+		f.entries = []entry{e}
+		return
+	}
+	f.entries = append(f.entries, e)
+}
+
+// Flush writes the perfmap entries into the perfmap file where the entries are adjusted by the given `addr` and `functionOffsets`.
+func (f *Perfmap) Flush(addr uintptr, functionOffsets []int) {
+	defer func() {
+		_ = f.fh.Sync()
+	}()
+
+	for _, e := range f.entries {
+		if _, err := f.fh.WriteString(fmt.Sprintf("%x %s %s\n",
+			uintptr(e.offset)+addr+uintptr(functionOffsets[e.index]),
+			strconv.FormatUint(e.size, 16),
+			e.name,
+		)); err != nil {
+			panic(err)
+		}
+	}
+	f.entries = f.entries[:0]
+}
+
+// Clear clears the perfmap entries not yet flushed.
+func (f *Perfmap) Clear() {
+	f.entries = f.entries[:0]
+}
+
+// AddEntry writes a perfmap entry directly into the perfmap file, not using the entries.
+func (f *Perfmap) AddEntry(addr uintptr, size uint64, name string) {
+	_, err := f.fh.WriteString(fmt.Sprintf("%x %s %s\n",
+		addr,
+		strconv.FormatUint(size, 16),
+		name,
+	))
+	if err != nil {
+		panic(err)
+	}
+}
diff --git a/internal/engine/wazevo/wazevoapi/perfmap_disabled.go b/internal/engine/wazevo/wazevoapi/perfmap_disabled.go
new file mode 100644
index 00000000..bcc4e545
--- /dev/null
+++ b/internal/engine/wazevo/wazevoapi/perfmap_disabled.go
@@ -0,0 +1,5 @@
+//go:build !perfmap
+
+package wazevoapi
+
+const PerfMapEnabled = false
diff --git a/internal/engine/wazevo/wazevoapi/perfmap_enabled.go b/internal/engine/wazevo/wazevoapi/perfmap_enabled.go
new file mode 100644
index 00000000..2a39879e
--- /dev/null
+++ b/internal/engine/wazevo/wazevoapi/perfmap_enabled.go
@@ -0,0 +1,5 @@
+//go:build perfmap
+
+package wazevoapi
+
+const PerfMapEnabled = true


Test patch:
diff --git a/internal/engine/wazevo/backend/backend_test.go b/internal/engine/wazevo/backend/backend_test.go
index 592e20a8..8ecdaee2 100644
--- a/internal/engine/wazevo/backend/backend_test.go
+++ b/internal/engine/wazevo/backend/backend_test.go
@@ -2231,7 +2231,7 @@ L1 (SSA Block: blk0):
 				fmt.Println(be.Format())
 			}
 
-			be.Finalize()
+			be.Finalize(context.Background())
 			if verbose {
 				fmt.Println("============ finalization result ============")
 				fmt.Println(be.Format())
diff --git a/internal/engine/wazevo/backend/isa/arm64/util_test.go b/internal/engine/wazevo/backend/isa/arm64/util_test.go
index 77f51de1..4f432373 100644
--- a/internal/engine/wazevo/backend/isa/arm64/util_test.go
+++ b/internal/engine/wazevo/backend/isa/arm64/util_test.go
@@ -82,11 +82,11 @@ func (m *mockCompiler) Buf() []byte { return m.buf }
 func (m *mockCompiler) TypeOf(v regalloc.VReg) (ret ssa.Type) {
 	return m.typeOf[v.ID()]
 }
-func (m *mockCompiler) Finalize()      {}
-func (m *mockCompiler) RegAlloc()      {}
-func (m *mockCompiler) Lower()         {}
-func (m *mockCompiler) Format() string { return "" }
-func (m *mockCompiler) Init()          {}
+func (m *mockCompiler) Finalize(context.Context) {}
+func (m *mockCompiler) RegAlloc()                {}
+func (m *mockCompiler) Lower()                   {}
+func (m *mockCompiler) Format() string           { return "" }
+func (m *mockCompiler) Init()                    {}
 
 func newMockCompilationContext() *mockCompiler {
 	return &mockCompiler{
diff --git a/internal/engine/wazevo/backend/machine_test.go b/internal/engine/wazevo/backend/machine_test.go
index e9a2ba0f..fde03353 100644
--- a/internal/engine/wazevo/backend/machine_test.go
+++ b/internal/engine/wazevo/backend/machine_test.go
@@ -1,6 +1,8 @@
 package backend
 
 import (
+	"context"
+
 	"github.com/tetratelabs/wazero/internal/engine/wazevo/backend/regalloc"
 	"github.com/tetratelabs/wazero/internal/engine/wazevo/ssa"
 	"github.com/tetratelabs/wazero/internal/engine/wazevo/wazevoapi"
@@ -50,7 +52,7 @@ func (m mockMachine) SetupPrologue() {}
 func (m mockMachine) SetupEpilogue() {}
 
 // ResolveRelativeAddresses implements Machine.ResolveRelativeAddresses.
-func (m mockMachine) ResolveRelativeAddresses() {}
+func (m mockMachine) ResolveRelativeAddresses(ctx context.Context) {}
 
 // Function implements Machine.Function.
 func (m mockMachine) Function() (f regalloc.Function) { return }
diff --git a/internal/engine/wazevo/frontend/frontend_test.go b/internal/engine/wazevo/frontend/frontend_test.go
index f50d3218..34d17ec4 100644
--- a/internal/engine/wazevo/frontend/frontend_test.go
+++ b/internal/engine/wazevo/frontend/frontend_test.go
@@ -1019,14 +1019,8 @@ blk0: (exec_ctx:i64, module_ctx:i64, v2:i32, v3:i32)
 	v9:i64 = Load module_ctx, 0x8
 	v10:i64 = Iadd v9, v5
 	Store v3, v10, 0x0
-	v11:i64 = Iconst_64 0x4
-	v12:i64 = UExtend v2, 32->64
-	v13:i64 = Iadd v12, v11
-	v14:i32 = Icmp lt_u, v6, v13
-	ExitIfTrue v14, exec_ctx, memory_out_of_bounds
-	v15:i64 = Iadd v9, v12
-	v16:i32 = Load v15, 0x0
-	Jump blk_ret, v16
+	v11:i32 = Load v10, 0x0
+	Jump blk_ret, v11
 `,
 		},
 		{
@@ -1142,191 +1136,44 @@ blk0: (exec_ctx:i64, module_ctx:i64, v2:i32)
 	v13:i64 = Iadd v12, v11
 	v14:i32 = Icmp lt_u, v5, v13
 	ExitIfTrue v14, exec_ctx, memory_out_of_bounds
-	v15:i64 = Iadd v8, v12
-	v16:i64 = Load v15, 0x0
-	v17:i64 = Iconst_64 0x4
-	v18:i64 = UExtend v2, 32->64
-	v19:i64 = Iadd v18, v17
-	v20:i32 = Icmp lt_u, v5, v19
-	ExitIfTrue v20, exec_ctx, memory_out_of_bounds
-	v21:i64 = Iadd v8, v18
-	v22:f32 = Load v21, 0x0
-	v23:i64 = Iconst_64 0x8
+	v15:i64 = Load v9, 0x0
+	v16:f32 = Load v9, 0x0
+	v17:f64 = Load v9, 0x0
+	v18:i64 = Iconst_64 0x13
+	v19:i64 = UExtend v2, 32->64
+	v20:i64 = Iadd v19, v18
+	v21:i32 = Icmp lt_u, v5, v20
+	ExitIfTrue v21, exec_ctx, memory_out_of_bounds
+	v22:i32 = Load v9, 0xf
+	v23:i64 = Iconst_64 0x17
 	v24:i64 = UExtend v2, 32->64
 	v25:i64 = Iadd v24, v23
 	v26:i32 = Icmp lt_u, v5, v25
 	ExitIfTrue v26, exec_ctx, memory_out_of_bounds
-	v27:i64 = Iadd v8, v24
-	v28:f64 = Load v27, 0x0
-	v29:i64 = Iconst_64 0x13
-	v30:i64 = UExtend v2, 32->64
-	v31:i64 = Iadd v30, v29
-	v32:i32 = Icmp lt_u, v5, v31
-	ExitIfTrue v32, exec_ctx, memory_out_of_bounds
-	v33:i64 = Iadd v8, v30
-	v34:i32 = Load v33, 0xf
-	v35:i64 = Iconst_64 0x17
-	v36:i64 = UExtend v2, 32->64
-	v37:i64 = Iadd v36, v35
-	v38:i32 = Icmp lt_u, v5, v37
-	ExitIfTrue v38, exec_ctx, memory_out_of_bounds
-	v39:i64 = Iadd v8, v36
-	v40:i64 = Load v39, 0xf
-	v41:i64 = Iconst_64 0x13
-	v42:i64 = UExtend v2, 32->64
-	v43:i64 = Iadd v42, v41
-	v44:i32 = Icmp lt_u, v5, v43
-	ExitIfTrue v44, exec_ctx, memory_out_of_bounds
-	v45:i64 = Iadd v8, v42
-	v46:f32 = Load v45, 0xf
-	v47:i64 = Iconst_64 0x17
-	v48:i64 = UExtend v2, 32->64
-	v49:i64 = Iadd v48, v47
-	v50:i32 = Icmp lt_u, v5, v49
-	ExitIfTrue v50, exec_ctx, memory_out_of_bounds
-	v51:i64 = Iadd v8, v48
-	v52:f64 = Load v51, 0xf
-	v53:i64 = Iconst_64 0x1
-	v54:i64 = UExtend v2, 32->64
-	v55:i64 = Iadd v54, v53
-	v56:i32 = Icmp lt_u, v5, v55
-	ExitIfTrue v56, exec_ctx, memory_out_of_bounds
-	v57:i64 = Iadd v8, v54
-	v58:i32 = Sload8 v57, 0x0
-	v59:i64 = Iconst_64 0x10
-	v60:i64 = UExtend v2, 32->64
-	v61:i64 = Iadd v60, v59
-	v62:i32 = Icmp lt_u, v5, v61
-	ExitIfTrue v62, exec_ctx, memory_out_of_bounds
-	v63:i64 = Iadd v8, v60
-	v64:i32 = Sload8 v63, 0xf
-	v65:i64 = Iconst_64 0x1
-	v66:i64 = UExtend v2, 32->64
-	v67:i64 = Iadd v66, v65
-	v68:i32 = Icmp lt_u, v5, v67
-	ExitIfTrue v68, exec_ctx, memory_out_of_bounds
-	v69:i64 = Iadd v8, v66
-	v70:i32 = Uload8 v69, 0x0
-	v71:i64 = Iconst_64 0x10
-	v72:i64 = UExtend v2, 32->64
-	v73:i64 = Iadd v72, v71
-	v74:i32 = Icmp lt_u, v5, v73
-	ExitIfTrue v74, exec_ctx, memory_out_of_bounds
-	v75:i64 = Iadd v8, v72
-	v76:i32 = Uload8 v75, 0xf
-	v77:i64 = Iconst_64 0x2
-	v78:i64 = UExtend v2, 32->64
-	v79:i64 = Iadd v78, v77
-	v80:i32 = Icmp lt_u, v5, v79
-	ExitIfTrue v80, exec_ctx, memory_out_of_bounds
-	v81:i64 = Iadd v8, v78
-	v82:i32 = Sload16 v81, 0x0
-	v83:i64 = Iconst_64 0x11
-	v84:i64 = UExtend v2, 32->64
-	v85:i64 = Iadd v84, v83
-	v86:i32 = Icmp lt_u, v5, v85
-	ExitIfTrue v86, exec_ctx, memory_out_of_bounds
-	v87:i64 = Iadd v8, v84
-	v88:i32 = Sload16 v87, 0xf
-	v89:i64 = Iconst_64 0x2
-	v90:i64 = UExtend v2, 32->64
-	v91:i64 = Iadd v90, v89
-	v92:i32 = Icmp lt_u, v5, v91
-	ExitIfTrue v92, exec_ctx, memory_out_of_bounds
-	v93:i64 = Iadd v8, v90
-	v94:i32 = Uload16 v93, 0x0
-	v95:i64 = Iconst_64 0x11
-	v96:i64 = UExtend v2, 32->64
-	v97:i64 = Iadd v96, v95
-	v98:i32 = Icmp lt_u, v5, v97
-	ExitIfTrue v98, exec_ctx, memory_out_of_bounds
-	v99:i64 = Iadd v8, v96
-	v100:i32 = Uload16 v99, 0xf
-	v101:i64 = Iconst_64 0x1
-	v102:i64 = UExtend v2, 32->64
-	v103:i64 = Iadd v102, v101
-	v104:i32 = Icmp lt_u, v5, v103
-	ExitIfTrue v104, exec_ctx, memory_out_of_bounds
-	v105:i64 = Iadd v8, v102
-	v106:i64 = Sload8 v105, 0x0
-	v107:i64 = Iconst_64 0x10
-	v108:i64 = UExtend v2, 32->64
-	v109:i64 = Iadd v108, v107
-	v110:i32 = Icmp lt_u, v5, v109
-	ExitIfTrue v110, exec_ctx, memory_out_of_bounds
-	v111:i64 = Iadd v8, v108
-	v112:i64 = Sload8 v111, 0xf
-	v113:i64 = Iconst_64 0x1
-	v114:i64 = UExtend v2, 32->64
-	v115:i64 = Iadd v114, v113
-	v116:i32 = Icmp lt_u, v5, v115
-	ExitIfTrue v116, exec_ctx, memory_out_of_bounds
-	v117:i64 = Iadd v8, v114
-	v118:i64 = Uload8 v117, 0x0
-	v119:i64 = Iconst_64 0x10
-	v120:i64 = UExtend v2, 32->64
-	v121:i64 = Iadd v120, v119
-	v122:i32 = Icmp lt_u, v5, v121
-	ExitIfTrue v122, exec_ctx, memory_out_of_bounds
-	v123:i64 = Iadd v8, v120
-	v124:i64 = Uload8 v123, 0xf
-	v125:i64 = Iconst_64 0x2
-	v126:i64 = UExtend v2, 32->64
-	v127:i64 = Iadd v126, v125
-	v128:i32 = Icmp lt_u, v5, v127
-	ExitIfTrue v128, exec_ctx, memory_out_of_bounds
-	v129:i64 = Iadd v8, v126
-	v130:i64 = Sload16 v129, 0x0
-	v131:i64 = Iconst_64 0x11
-	v132:i64 = UExtend v2, 32->64
-	v133:i64 = Iadd v132, v131
-	v134:i32 = Icmp lt_u, v5, v133
-	ExitIfTrue v134, exec_ctx, memory_out_of_bounds
-	v135:i64 = Iadd v8, v132
-	v136:i64 = Sload16 v135, 0xf
-	v137:i64 = Iconst_64 0x2
-	v138:i64 = UExtend v2, 32->64
-	v139:i64 = Iadd v138, v137
-	v140:i32 = Icmp lt_u, v5, v139
-	ExitIfTrue v140, exec_ctx, memory_out_of_bounds
-	v141:i64 = Iadd v8, v138
-	v142:i64 = Uload16 v141, 0x0
-	v143:i64 = Iconst_64 0x11
-	v144:i64 = UExtend v2, 32->64
-	v145:i64 = Iadd v144, v143
-	v146:i32 = Icmp lt_u, v5, v145
-	ExitIfTrue v146, exec_ctx, memory_out_of_bounds
-	v147:i64 = Iadd v8, v144
-	v148:i64 = Uload16 v147, 0xf
-	v149:i64 = Iconst_64 0x4
-	v150:i64 = UExtend v2, 32->64
-	v151:i64 = Iadd v150, v149
-	v152:i32 = Icmp lt_u, v5, v151
-	ExitIfTrue v152, exec_ctx, memory_out_of_bounds
-	v153:i64 = Iadd v8, v150
-	v154:i64 = Sload32 v153, 0x0
-	v155:i64 = Iconst_64 0x13
-	v156:i64 = UExtend v2, 32->64
-	v157:i64 = Iadd v156, v155
-	v158:i32 = Icmp lt_u, v5, v157
-	ExitIfTrue v158, exec_ctx, memory_out_of_bounds
-	v159:i64 = Iadd v8, v156
-	v160:i64 = Sload32 v159, 0xf
-	v161:i64 = Iconst_64 0x4
-	v162:i64 = UExtend v2, 32->64
-	v163:i64 = Iadd v162, v161
-	v164:i32 = Icmp lt_u, v5, v163
-	ExitIfTrue v164, exec_ctx, memory_out_of_bounds
-	v165:i64 = Iadd v8, v162
-	v166:i64 = Uload32 v165, 0x0
-	v167:i64 = Iconst_64 0x13
-	v168:i64 = UExtend v2, 32->64
-	v169:i64 = Iadd v168, v167
-	v170:i32 = Icmp lt_u, v5, v169
-	ExitIfTrue v170, exec_ctx, memory_out_of_bounds
-	v171:i64 = Iadd v8, v168
-	v172:i64 = Uload32 v171, 0xf
-	Jump blk_ret, v10, v16, v22, v28, v34, v40, v46, v52, v58, v64, v70, v76, v82, v88, v94, v100, v106, v112, v118, v124, v130, v136, v142, v148, v154, v160, v166, v172
+	v27:i64 = Load v9, 0xf
+	v28:f32 = Load v9, 0xf
+	v29:f64 = Load v9, 0xf
+	v30:i32 = Sload8 v9, 0x0
+	v31:i32 = Sload8 v9, 0xf
+	v32:i32 = Uload8 v9, 0x0
+	v33:i32 = Uload8 v9, 0xf
+	v34:i32 = Sload16 v9, 0x0
+	v35:i32 = Sload16 v9, 0xf
+	v36:i32 = Uload16 v9, 0x0
+	v37:i32 = Uload16 v9, 0xf
+	v38:i64 = Sload8 v9, 0x0
+	v39:i64 = Sload8 v9, 0xf
+	v40:i64 = Uload8 v9, 0x0
+	v41:i64 = Uload8 v9, 0xf
+	v42:i64 = Sload16 v9, 0x0
+	v43:i64 = Sload16 v9, 0xf
+	v44:i64 = Uload16 v9, 0x0
+	v45:i64 = Uload16 v9, 0xf
+	v46:i64 = Sload32 v9, 0x0
+	v47:i64 = Sload32 v9, 0xf
+	v48:i64 = Uload32 v9, 0x0
+	v49:i64 = Uload32 v9, 0xf
+	Jump blk_ret, v10, v15, v16, v17, v22, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49
 `,
 		},
 		{
@@ -1934,3 +1781,49 @@ func TestCompiler_declareSignatures(t *testing.T) {
 		}
 	})
 }
+
+func TestCompiler_recordKnownSafeBound(t *testing.T) {
+	c := &Compiler{}
+	c.recordKnownSafeBound(1, 99, 9999)
+	require.Equal(t, 1, len(c.knownSafeBoundsSet))
+	require.True(t, c.getKnownSafeBound(1).valid())
+	require.Equal(t, uint64(99), c.getKnownSafeBound(1).bound)
+	require.Equal(t, ssa.Value(9999), c.getKnownSafeBound(1).absoluteAddr)
+
+	c.recordKnownSafeBound(1, 150, 9999)
+	require.Equal(t, 1, len(c.knownSafeBoundsSet))
+	require.Equal(t, uint64(150), c.getKnownSafeBound(1).bound)
+
+	c.recordKnownSafeBound(5, 666, 54321)
+	require.Equal(t, 2, len(c.knownSafeBoundsSet))
+	require.Equal(t, uint64(666), c.getKnownSafeBound(5).bound)
+	require.Equal(t, ssa.Value(54321), c.getKnownSafeBound(5).absoluteAddr)
+}
+
+func TestCompiler_getKnownSafeBound(t *testing.T) {
+	c := &Compiler{
+		knownSafeBounds: []knownSafeBound{
+			{}, {bound: 2134},
+		},
+	}
+	require.Nil(t, c.getKnownSafeBound(5))
+	require.Nil(t, c.getKnownSafeBound(12345))
+	require.False(t, c.getKnownSafeBound(0).valid())
+	require.True(t, c.getKnownSafeBound(1).valid())
+}
+
+func TestCompiler_clearSafeBounds(t *testing.T) {
+	c := &Compiler{}
+	c.knownSafeBounds = []knownSafeBound{{bound: 1}, {}, {bound: 2}, {}, {}, {bound: 3}}
+	c.knownSafeBoundsSet = []ssa.ValueID{0, 2, 5}
+	c.clearSafeBounds()
+	require.Equal(t, 0, len(c.knownSafeBoundsSet))
+	require.Equal(t, []knownSafeBound{{}, {}, {}, {}, {}, {}}, c.knownSafeBounds)
+}
+
+func TestKnownSafeBound_valid(t *testing.T) {
+	k := &knownSafeBound{bound: 10, absoluteAddr: 12345}
+	require.True(t, k.valid())
+	k.bound = 0
+	require.False(t, k.valid())
+}
diff --git a/internal/integration_test/stdlibs/Makefile b/internal/integration_test/stdlibs/Makefile
new file mode 100644
index 00000000..5199cccf
--- /dev/null
+++ b/internal/integration_test/stdlibs/Makefile
@@ -0,0 +1,148 @@
+# Note: Some tests might fail if Go has been installed with homebrew because
+# the file system layout is different than what the tests expect.
+# Easiest fix is to install without using brew.
+goroot?=$(shell go env GOROOT)
+# Note: The standard binary zig distribution does not ship some testdata.
+# You should override with the zig source code path, otherwise some tests will fail.
+zigroot?=$(shell dirname $(shell which zig))
+
+zig_bin=$(shell pwd)/testdata/zig
+tinygo_bin=$(shell pwd)/testdata/tinygo
+gowasip1_bin=$(shell pwd)/testdata/go
+
+
+# embed/internal/embedtest deals with some paths that are not available here, ignore.
+tinygo_tests=container/heap \
+	container/list \
+	container/ring \
+	crypto/des \
+	crypto/md5 \
+	crypto/rc4 \
+	crypto/sha1 \
+	crypto/sha256 \
+	crypto/sha512 \
+	encoding \
+	encoding/ascii85 \
+	encoding/base32 \
+	encoding/csv \
+	encoding/hex \
+	go/scanner \
+	hash \
+	hash/adler32 \
+	hash/crc64 \
+	hash/fnv \
+	html \
+	internal/itoa \
+	internal/profile \
+	math \
+	math/cmplx \
+	net \
+	net/http/internal/ascii \
+	net/mail \
+	os \
+	path \
+	reflect \
+	sync \
+	testing \
+	testing/iotest \
+	text/scanner \
+	unicode \
+	unicode/utf16 \
+	unicode/utf8
+
+gowasip1_tests=src/archive/tar \
+	src/bufio \
+	src/bytes \
+	src/context \
+	src/encoding/ascii85 \
+	src/encoding/asn1 \
+	src/encoding/base32 \
+	src/encoding/base64 \
+	src/encoding/binary \
+	src/encoding/csv \
+	src/encoding/gob \
+	src/encoding/hex \
+	src/encoding/json \
+	src/encoding/pem \
+	src/encoding/xml \
+	src/errors \
+	src/expvar \
+	src/flag \
+	src/fmt \
+	src/hash \
+	src/hash/adler32 \
+	src/hash/crc32 \
+	src/hash/crc64 \
+	src/hash/fnv \
+	src/hash/maphash \
+	src/io \
+	src/io/fs \
+	src/io/ioutil \
+	src/log \
+	src/log/syslog \
+	src/maps \
+	src/math \
+	src/math/big \
+	src/math/bits \
+	src/math/cmplx \
+	src/math/rand \
+	src/mime \
+	src/mime/multipart \
+	src/mime/quotedprintable \
+	src/os \
+	src/os/exec \
+	src/os/signal \
+	src/os/user \
+	src/path \
+	src/reflect \
+	src/regexp \
+	src/regexp/syntax \
+	src/runtime \
+	src/runtime/internal/atomic \
+	src/runtime/internal/math \
+	src/runtime/internal/sys \
+	src/slices \
+	src/sort \
+	src/strconv \
+	src/strings \
+	src/sync \
+	src/sync/atomic \
+	src/syscall \
+	src/testing \
+	src/testing/fstest \
+	src/testing/iotest \
+	src/testing/quick \
+	src/time
+
+all: build.zig build.tinygo build.gowasip1
+
+run:
+	go test -bench=. -benchtime=1x
+
+
+.PHONY: build.zig
+build.zig:
+	# --test-no-exec allows building of the test Wasm binary without executing command.
+	# We use find because the test.wasm will be something like ./zig-cache/o/dd6df1361b2134adc5eee9d027495436/test.wasm
+	@mkdir -p $(zig_bin)
+	@cd $(zigroot) && zig test --test-no-exec -target wasm32-wasi --zig-lib-dir $(zigroot)/lib $(zigroot)/lib/std/std.zig
+	@cp $$(find $(zigroot) -name test.wasm) $(zig_bin)/test.wasm
+	# The generated test binary is large and produces skewed results in favor of the optimized compiler.
+	# We also generate a stripped, optimized binary with wasm-opt.
+	@wasm-opt $(zig_bin)/test.wasm -O --strip-dwarf -o $(zig_bin)/test-opt.wasm
+
+.PHONY: build.tinygo
+build.tinygo:
+	@mkdir -p $(tinygo_bin)
+	for value in $(tinygo_tests); do\
+		echo Building $${value}... ;\
+		tinygo test -target wasi -c -o $(tinygo_bin)/$$(echo $$value | sed -e 's/\//_/g').test $${value} 2>&1 >/dev/null  ;\
+	done
+
+.PHONY: build.gowasip1
+build.gowasip1:
+	@mkdir -p $(gowasip1_bin)
+	@cd '$(goroot)'; \
+	for value in $(gowasip1_tests); do\
+	  	GOOS=wasip1 GOARCH=wasm go test -v -c -o $(gowasip1_bin)/$$(echo $$value | sed -e 's/\//_/g').test ./$${value}; \
+	done
diff --git a/internal/integration_test/stdlibs/README.md b/internal/integration_test/stdlibs/README.md
new file mode 100644
index 00000000..68fd0cd3
--- /dev/null
+++ b/internal/integration_test/stdlibs/README.md
@@ -0,0 +1,32 @@
+# Stdlibs benchmarks
+
+This directory contains a Makefile to build (a subset of) the stdlibs for Zig, TinyGo and Go (wasip1)
+and test them against the baseline compiler and the optimizing compiler.
+
+## Requirements
+
+- Zig 0.11.0 in PATH, source code to zig 0.11.0
+- TinyGo in PATH
+- Go in PATH
+
+## Usage
+
+First, build the test suite (the Zig source root has to be set explicitly):
+
+    make all zigroot=/path/to/zig/source
+
+Then you can run the test suite against the baseline compiler and the optimizing compiler; e.g.:
+
+    go test -bench=. --benchtime=1x
+
+If you want to run with the default settings, use `make run`.
+
+## Caveats
+
+* The standard binary zig distribution does not ship some testdata.
+  You should override with the zig source code path, otherwise some tests will fail.
+
+* Some tests might fail if Go has been installed with homebrew because
+  the file system layout is different than what the tests expect.
+  Easiest fix is to install Go without using brew.
+
diff --git a/internal/integration_test/stdlibs/bench_test.go b/internal/integration_test/stdlibs/bench_test.go
new file mode 100644
index 00000000..268eeb0f
--- /dev/null
+++ b/internal/integration_test/stdlibs/bench_test.go
@@ -0,0 +1,189 @@
+package wazevo_test
+
+import (
+	"context"
+	"crypto/rand"
+	"os"
+	"path/filepath"
+	"runtime"
+	"strings"
+	"testing"
+
+	"github.com/tetratelabs/wazero"
+	"github.com/tetratelabs/wazero/experimental/opt"
+	"github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1"
+	"github.com/tetratelabs/wazero/internal/testing/require"
+	"github.com/tetratelabs/wazero/sys"
+)
+
+func BenchmarkStdlibs(b *testing.B) {
+	type testConfig struct {
+		name   string
+		config wazero.RuntimeConfig
+	}
+	configs := []testConfig{
+		{name: "baseline", config: wazero.NewRuntimeConfigCompiler()},
+	}
+	if runtime.GOARCH == "arm64" {
+		configs = append(configs, testConfig{name: "optimizing", config: opt.NewRuntimeConfigOptimizingCompiler()})
+	}
+
+	cwd, _ := os.Getwd()
+	defer os.Chdir(cwd) //nolint
+	ctx := context.Background()
+
+	testCases := []struct {
+		name, dir    string
+		readTestCase func(fpath string, fname string) ([]byte, wazero.ModuleConfig, error)
+	}{
+		{
+			name: "zig",
+			dir:  "testdata/zig/",
+			readTestCase: func(fpath string, fname string) ([]byte, wazero.ModuleConfig, error) {
+				bin, err := os.ReadFile(fpath)
+				modCfg := defaultModuleConfig().
+					WithFSConfig(wazero.NewFSConfig().WithDirMount(".", "/")).
+					WithArgs("test.wasm")
+
+				return bin, modCfg, err
+			},
+		},
+		{
+			name: "tinygo",
+			dir:  "testdata/tinygo/",
+			readTestCase: func(fpath string, fname string) ([]byte, wazero.ModuleConfig, error) {
+				if !strings.HasSuffix(fname, ".test") {
+					return nil, nil, nil
+				}
+				bin, err := os.ReadFile(fpath)
+				fsconfig := wazero.NewFSConfig().
+					WithDirMount(".", "/").
+					WithDirMount(os.TempDir(), "/tmp")
+				modCfg := defaultModuleConfig().
+					WithFSConfig(fsconfig).
+					WithArgs(fname, "-test.v")
+
+				return bin, modCfg, err
+			},
+		},
+		{
+			name: "wasip1",
+			dir:  "testdata/go/",
+			readTestCase: func(fpath string, fname string) ([]byte, wazero.ModuleConfig, error) {
+				if !strings.HasSuffix(fname, ".test") {
+					return nil, nil, nil
+				}
+				bin, err := os.ReadFile(fpath)
+				if err != nil {
+					return nil, nil, err
+				}
+				fsuffixstripped := strings.ReplaceAll(fname, ".test", "")
+				inferredpath := strings.ReplaceAll(fsuffixstripped, "_", "/")
+				testdir := filepath.Join(runtime.GOROOT(), inferredpath)
+				err = os.Chdir(testdir)
+
+				sysroot := filepath.VolumeName(testdir) + string(os.PathSeparator)
+				normalizedTestdir := normalizeOsPath(testdir)
+
+				modCfg := defaultModuleConfig().
+					WithFSConfig(
+						wazero.NewFSConfig().
+							WithDirMount(sysroot, "/").
+							WithDirMount(os.TempDir(), "/tmp")).
+					WithEnv("PWD", normalizedTestdir)
+
+				args := []string{fname, "-test.short", "-test.v"}
+
+				// Skip tests that are fragile on Windows.
+				if runtime.GOOS == "windows" {
+					modCfg = modCfg.
+						WithEnv("GOROOT", normalizeOsPath(runtime.GOROOT()))
+
+					args = append(args,
+						"-test.skip=TestRenameCaseDifference/dir|"+
+							"TestDirFSPathsValid|TestDirFS|TestDevNullFile|"+
+							"TestOpenError|TestSymlinkWithTrailingSlash")
+				}
+				modCfg = modCfg.WithArgs(args...)
+
+				return bin, modCfg, err
+			},
+		},
+	}
+
+	for _, tc := range testCases {
+		b.Run(tc.name, func(b *testing.B) {
+			files, err := os.ReadDir(tc.dir)
+			require.NoError(b, err)
+			for _, f := range files {
+				fname := f.Name()
+				// Ensure we are on root dir.
+				err = os.Chdir(cwd)
+				require.NoError(b, err)
+
+				fpath := filepath.Join(cwd, tc.dir, fname)
+				bin, modCfg, err := tc.readTestCase(fpath, fname)
+				require.NoError(b, err)
+				if bin == nil {
+					// skip
+					continue
+				}
+
+				b.Run(fname, func(b *testing.B) {
+					for _, cfg := range configs {
+						r := wazero.NewRuntimeWithConfig(ctx, cfg.config)
+						wasi_snapshot_preview1.MustInstantiate(ctx, r)
+						b.Cleanup(func() { r.Close(ctx) })
+
+						m, err := r.CompileModule(ctx, bin)
+						require.NoError(b, err)
+
+						b.Run(cfg.name, func(b *testing.B) {
+							b.Run("Compile", func(b *testing.B) {
+								_, err := r.CompileModule(ctx, bin)
+								require.NoError(b, err)
+							})
+							im, err := r.InstantiateModule(ctx, m, modCfg)
+							require.NoError(b, err)
+							b.Run("Run", func(b *testing.B) {
+								_, err := im.ExportedFunction("_start").Call(ctx)
+								requireZeroExitCode(b, err)
+							})
+						})
+					}
+				})
+			}
+		})
+	}
+}
+
+// Normalize an absolute path to a Unix-style path, regardless if it is a Windows path.
+func normalizeOsPath(path string) string {
+	// Remove volume name. This is '/' on *Nix and 'C:' (with C being any letter identifier).
+	root := filepath.VolumeName(path)
+	testdirnoprefix := path[len(root):]
+	// Normalizes all the path separators to a Unix separator.
+	testdirnormalized := strings.ReplaceAll(testdirnoprefix, string(os.PathSeparator), "/")
+	return testdirnormalized
+}
+
+func defaultModuleConfig() wazero.ModuleConfig {
+	return wazero.NewModuleConfig().
+		WithSysNanosleep().
+		WithSysNanotime().
+		WithSysWalltime().
+		WithRandSource(rand.Reader).
+		// Some tests require Stdout and Stderr to be present.
+		WithStdout(os.Stdout).
+		WithStderr(os.Stderr).
+		WithStartFunctions()
+}
+
+func requireZeroExitCode(b *testing.B, err error) {
+	b.Helper()
+	if se, ok := err.(*sys.ExitError); ok {
+		if se.ExitCode() != 0 { // Don't err on success.
+			require.NoError(b, err)
+		}
+	}
+}