Quick and Easy GoAsm Code Snippets
GoAsm is a lightweight assembly-like language for writing efficient, low-level routines that integrate smoothly with Go programs. This article gives several short, practical snippets you can copy, run, and adapt. Each example includes a brief explanation and a complete snippet that compiles with a typical Go + GoAsm setup.
Prerequisites
- Go toolchain installed (go1.20+ recommended).
- goasm-compatible assembler (the standard Go assembler supports Plan 9 syntax; this article uses that syntax).
- Basic familiarity with Go calling conventions and Plan 9 assembly registers (RAX, RBX, etc., on x86-64).
1) Hello from GoAsm — calling from Go
A minimal example showing how Go calls an assembly function that returns a string pointer and length.
Assembly (hello.s):
asm
TEXT ·Hello(SB),NOSPLIT,$0 // Return pointer in AX, length in DX (Go uses two-word string header)MOVQ $msg+0(SB), AX MOVQ $lenmsg, DX RETDATA msg+0(SB)/8, \(0 GLOBL msg(SB), RODATA, \)8 DATA msg(SB)/1, “Hi from GoAsm!” GLOBL lenmsg(SB), RODATA, \(8 DATA lenmsg+0(SB)/8, \)13
Go (main.go):
go
package main import “fmt” //go:noescape func Hello() (p byte, n int) func main() { p, n := Hello() s := string(([1 << 30]byte)(p)[:n:n]) fmt.Println(s) }Explanation: The assembly sets up a pointer and length for a Go string and returns them. The Go wrapper reconstructs the string safely.
2) Simple integer add
An assembly function that adds two int64 values and returns the sum.
Assembly (add.s):
asm
TEXT ·Add(SB),NOSPLIT,$0 MOVQ a+0(FP), AXADDQ b+8(FP), AX MOVQ AX, ret+16(FP) RETGLOBL·Add(SB), NOSPLIT
Go (add.go):
go
package main import “fmt” //go:noescape func Add(a, b int64) int64 func main() { fmt.Println(Add(7, 5)) // 12 }Explanation: Parameters are loaded from the stack frame, summed, and stored to the return slot.
3) Looping memory copy
A small memcpy implementation that copies n bytes from src to dst.
Assembly (memcpy.s):
asm
TEXT ·Memcpy(SB),NOSPLIT,$0 MOVQ dst+0(FP), DIMOVQ src+8(FP), SI MOVQ n+16(FP), CX TESTQ CX, CX JE doneloop:
MOVBL (SI), R8B MOVBL R8B, (DI) INCQ SI INCQ DI DECQ CX JNZ loopdone:
RETGo (memcpy.go):
go
package main //go:noescape func Memcpy(dst, src *byte, n uint64) func main() {}Note: For production use prefer Go’s copy() — this is instructional.
4) Compare two byte slices (returns 0 equal, <0 a0 a>b)
Assembly (cmp.s):
asm
TEXT ·Compare(SB),NOSPLIT,$0 MOVQ a+0(FP), SIMOVQ b+8(FP), DI MOVQ n+16(FP), CX TESTQ CX, CX JE equalloop:
MOVBL (SI), AL MOVBL (DI), BL CMPB AL, BL JNE diff INCQ SI INCQ DI DECQ CX JNZ loopequal:
MOVQ $0, ret+24(FP) RETdiff:
MOVQ $1, ret+24(FP) JG end MOVQ $-1, ret+24(FP)end:
RETGo (cmp.go):
go
package main //go:noescape func Compare(a, b *byte, n uint64) int func main() {}5) Fast popcount (byte)
Count set bits in a single byte.
Assembly (popcnt.s):
asm
TEXT ·PopCount8(SB),NOSPLIT,$0 MOVB x+0(FP), ALXORL CX, CXcount:
TESTB AL, AL JE done ANDL $1, AL ADDL AL, CL SHRB $1, AL JMP countdone:
MOVB CL, ret+8(FP) RETGo (popcnt.go):
go
package main //go:noescape func PopCount8(x byte) byte func main() {}Tips and Best Practices
- Use //go:noescape for functions that don’t let Go pointers escape.
- Prefer NOSPLIT on tiny functions when stack growth is unnecessary.
- Benchmark against Go equivalents; Go’s runtime and compiler provide excellent optimizations.
- Keep calling conventions and frame layout consistent with Go’s ABI for your Go version.
Conclusion These snippets show common patterns: returning values, manipulating memory, looping, and byte ops. Use them as learning tools, then benchmark and adapt for performance-critical hotspots.
Leave a Reply