@ g Ivan Prisyazhnyy on software engineering

How to read GoLang static single-assignment (SSA) form intermediate representation

We will start off by kicking out a simple example of uncomplicated for-range loop:

    package main

    import "fmt"

    type S struct {
        b [8]byte
    }

    func keys(m map[S]struct{}) [][]byte {
        var z [][]byte
        for k := range m {
            z = append(z, k.b[:])
        }
        return z
    }

    func main() {
        fmt.Println(keys(map[S]struct{}{
            S{b: [8]byte{1}}: struct{}{},
            S{b: [8]byte{2}}: struct{}{},
        }))
    }

Try to guess what will be printed out before running it.

The answer to the results may be found in GoLang specification under the topic .

Let’s take a look into what compiler generates. In order see intermediate representation (IR) of the program execute:

    env GOSSAFUNC=keys go build your_file_name.go

The compiler will print SSA representation, transformation steps and the assembly at the end for function keys. Let’s take a look why we’ve got a result we’ve got.

Open ssa.html.

Take a look at first column:

b1:                                 ; new block label {b1} -
                                    ; program begining (init).
v1 = InitMem <mem>                  ; program heap
v2 = SP <uintptr>                   ; stack pointer
v3 = SB <uintptr>                   ; stack base
v4 = Addr <*map[S]struct {}> {m} v2 ; Get variable address {m}
                                    ; on stack (v2) of type
                                    ; <*map...> and remember it as v4.
                                    ; {m} - is first input argument.
v5 = Addr <*[][]byte> {~r1} v2      ; Get address of function result
                                    ; (return) variable {~r1}
v6 = Arg <map[S]struct {}> {m}      ; Get value of function argument
                                    ; {m}
v7 = ConstSlice <[][]byte>          ; Const slice nil lvalue of type.
v8 = Addr <*uint8> {type."".S} v3   ; Adress of the {S} struct type
                                    ; from global namespace (package).
v9 = OffPtr <**byte> [0] v2         ; New pointer to offset v2[0] =
                                    ; v2 + 0 = SP + 0 = SP
v10 = Store <mem> {*byte} v9 v8 v1  ; Store v8 to v9 at v1 memory.
                                    ; Put <S> type ptr on the stack.
                                    ; Returns memory.
                                    ; Store dstptr srcaddr mem -> mem
; arg0: stack[0] = S.(type)
v11 = StaticCall <mem> {runtime.newobject} [16] v10 ; create new obj
                                    ; of type v10 (type."".S) on heap
v12 = OffPtr <**S> [8] v2           ; Pointer to stack[+8] of <**S>
v13 = Load <*S> v12 v11             ; Pop stack value [0:8].
                                    ; The result of newobject(S.typ)
                                    ; call -> unsafe.Pointer from
                                    ; stack[0:8]

> at this point it is equivalent to
> var z [][]byte
> s := new(S)

v14 = Addr <*map.iter[S]struct {}> {.autotmp_6} v2 ; Get address
                                    ; of the local var {.autotmp_6}
                                    ; which will store map iter ptr.
v15 = VarDef <mem> {.autotmp_6} v11 ; Define new variable {.autotmp_6}
v16 = Zero <mem> {map.iter[S]struct {}} [96] v14 v15 ; Init with zeroe
                                    ; {.autotmp_6} of len 96 byte1

> var .autotmp_6 map.iter[S]struct{}

v17 = Addr <*uint8> {type.map["".S]struct {}} v3 ; Get adress of map
                                    ; type {map[S]struct{}}
v18 = Store <mem> {*byte} v9 v17 v16 ; Put type address on stack v2+0.
v19 = OffPtr <*map[S]struct {}> [8] v2 ; Pointer stack+8.
v20 = Store <mem> {map[S]struct {}} v19 v6 v18 ; [stack+8]={m} map ptr
v21 = Addr <*map.iter[S]struct {}> {.autotmp_6} v2 ; addr of tmp var
v22 = OffPtr <**map.iter[S]struct {}> [16] v2 ; ptr stack+16
v23 = Store <mem> {*map.iter[S]struct {}} v22 v21 v20 ; [stack+16]=
                                    ; val of tmp var = map iter ptr
; func mapiterinit(mapType *byte, hmap map[any]any, hiter *any)
; arg0: stack[0] = type.map["".S]struct {}
; arg1: stack[8] = {m} map from func keys arg0
; arg2: stack[16]= map iter ptr (to init)
v24 = StaticCall <mem> {runtime.mapiterinit} [24] v23 ; init map
                                    ; iter struct

> runtime.mapiterinit(map[S]struct{}.(type), m, &.autotmp_6)

v29 = ConstNil <*S>                 ; const nil S ptr lval
v39 = Const64 <int> [0]             ; int64 = 0 lval
v41 = Const64 <int> [8]
v51 = Const64 <int> [1]
v54 = Addr <*uint8> {type.[]uint8} v3 ; addr of []uint8 type
v57 = OffPtr <**[]byte> [8] v2      ; ptr to stack[8] (arg1) - z.len
v59 = OffPtr <*int> [16] v2         ; arg2
v61 = OffPtr <*int> [24] v2         ; arg3
v63 = OffPtr <*int> [32] v2         ; arg4 - z.ptr
v66 = OffPtr <**[]byte> [40] v2     ; arg5 - z.len
v68 = OffPtr <*int> [48] v2         ; arg6 - z.cap
v70 = OffPtr <*int> [56] v2         ; arg7
v90 = OffPtr <**map.iter[S]struct {}> [0] v2
                                    ; arg0 ptr to iter map ref
Plain → b2                          ; go to block b2
                                    ; straight away

; Assembly for block b1

00000 TEXT	"".keys(SB)
00001 FUNCDATA	$0, gclocals·0bc550b6b95948f318d057651e9cddea(SB)
00002 FUNCDATA	$1, gclocals·7ad199d7c1ca183f0a4df6a1e24a2a09(SB)

> s := new(S)
00003  LEAQ	type."".S(SB), AX
00004  MOVQ	AX, (SP)
00005  PCDATA	$0, $0
00006  CALL	runtime.newobject(SB)
00007  MOVQ	8(SP), AX
00008  MOVQ	AX, "".&k-104(SP)

> var .autotmp_6 map.iter[S]struct{}
00009  LEAQ	""..autotmp_6-96(SP), DI
00010  XORPS	X0, X0
00011  ADDQ	$-32, DI
00012  DUFFZERO	$273

> runtime.mapiterinit(map[S]struct{}.(type), m, &.autotmp_6)
00013  LEAQ	type.map["".S]struct {}(SB), CX
00014  MOVQ	CX, (SP)
00015  MOVQ	"".m(SP), CX
00016  MOVQ	CX, 8(SP)
00017  LEAQ	""..autotmp_6-96(SP), CX
00018  MOVQ	CX, 16(SP)
00019  PCDATA	$0, $1
00020  CALL	runtime.mapiterinit(SB)
00021  MOVL	$0, AX
00022  MOVQ	AX, CX
00023  MOVL	$0, DX
00024  JMP	32

======================================================================

b2: ← b1 b4                         ; new block {b2} - loop edge chck
                                    ;   continue if iter.key != nil,
                                    ;   with incoming jumps from {b1}
                                    ;   (init) and {b4}
v27 = Phi <mem> v24 v93             ; v27 = mem state
                                    ;   of runtime.mapiterinit if {b1}
                                    ;   of runtime.mapiternext if {b4}
v99 = Phi <[][]byte> v7 v88         ; v99 = mem state
                                    ;   of nil slice lval if from {b1}
                                    ;   of appended slice if from {b4}
v100 = Phi <*S> v13 v101            ; v3 = mem state
                                    ;   of new &S{} ptr if from {b1}
                                    ;   of copy of next item if {b4}
v25 = Addr <*map.iter[S]struct {}> {.autotmp_6} v2
                                    ; v25 = addr of {.autotmp_6} on v2
                                    ;   which is ptr to map iterator
v26 = OffPtr <**S> [0] v25          ; v26 = ptr to the ptr to S (map
                                    ;   elelment) (first byte in
                                    ;   map iterator var v25[0])
v28 = Load <*S> v26 v27             ; v28 = load cur {*S} Key from itr
v30 = NeqPtr <bool> v28 v29         ; v30 = v28<*S> != v29<nil>
If v30 → b3 b5 (likely)             ; goto {b3} if v30 == true, and
                                    ; to {b5} if not

> b2:
> var key *S = .autotmp_6.key
> if S != nil {
>   goto {b3}
> } else {
>   goto {b5}
> }

00029 MOVQ	"".z.cap-120(SP), AX
00030 MOVQ	"".z.len-128(SP), CX
00031 MOVQ	"".z.ptr-112(SP), DX
00032 MOVQ	""..autotmp_6-96(SP), BX
00033 TESTQ	BX, BX
00034 JEQ	76

======================================================================

b3: ← b2                            ; new block {b3} - load iter.key
                                    ;   and save it to local var,
                                    ;   with path fr {b2}
v31 = Addr <*map.iter[S]struct {}> {.autotmp_6} v2
                                    ; v31 = addr of {.autotmp_6} at v2
v32 = OffPtr <**S> [0] v31          ; v32 = ptr to v32[0] =
                                    ;   &.autotmp_6.key
v33 = Copy <mem> v27                ; v33 = copy map iter mem state
v34 = Load <*S> v32 v33             ; v34 = load cur map iter .key ptr
v35 = NilCheck <void> v34 v33       ; v35 = check result against <nil>
                                    ;   Panics if arg0 is nil.
v36 = Copy <*S> v100                ; v36 = copy ptr of local var
                                    ;   for holding cur S key val
v37 = Move <mem> {S} [8] v36 v34 v33; put cur key val (v34) to the v36
v38 = OffPtr <*[8]byte> [0] v36     ; v38 = ptr to v36[0] (ptr)
v40 = NilCheck <void> v38 v37       ; v40 = check v38 (s.b) for nil val
                                    ;   Panics if arg0 is nil.
v42 = IsSliceInBounds <bool> v39 v41; 0 <= arg0(0) <= arg1(8). arg1 is
                                    ;   guaranteed >= 0.
If v42 → b6 b7 (likely)             ; continue to {b6}, or
                                    ;   panic in {b7} if !{v42}

> b3:
> if iter.key == nil {
>    panic("key is nil")
> }
> *s = *iter.key                    ; where iter.key is unsafe.Ptr(*S)
> if s.b == nil {
>    panic("no next key")
> }

00035 MOVQ	(BX), BX
00036 MOVQ	"".&k-104(SP), SI
00037 MOVQ	BX, (SI)
00038 TESTB	AX, (SI)
00039 LEAQ	1(CX), BX
00040 CMPQ	BX, AX
00041 JGT	59

======================================================================

b4: ← b9                            ; step iterator next item
v89 = Addr <*map.iter[S]struct {}> {.autotmp_6} v2
                                    ; v89 = addr of {.autotmp_6} at v2
v91 = Copy <mem> v87                ; v91 = copy last mem state
v92 = Store <mem> {*map.iter[S]struct {}} v90 v89 v91
                                    ; put map iter ptr on stack top
v93 = StaticCall <mem> {runtime.mapiternext} [8] v92
                                    ; call for next map iter item
Plain → b2                          ; goto b2

> runtime.mapiternext(&.autotmp_6)

00025 LEAQ	""..autotmp_6-96(SP), AX
00026 MOVQ	AX, (SP)
00027 PCDATA	$0, $2
; func mapiternext(&.autotmp_6)
00028 CALL	runtime.mapiternext(SB)

======================================================================

b5: ← b2                            ; block to return from func
v94 = Copy <mem> v27                ; v94 = copy map iter mem state
v95 = VarKill <mem> {.autotmp_6} v94; dealloc local var {.autotmp_6}
v96 = Copy <[][]byte> v99           ; v96 = copy {z} mem state
v97 = VarDef <mem> {~r1} v95        ; v97 = return return value [][]b
v98 = Store <mem> {[][]byte} v5 v96 v97 
                                    ; put {z} to return val {~r1}
Ret v98                             ; return with mem state v98 {~r1}

> return z

00076 MOVQ	DX, "".~r1+8(SP)
00077 MOVQ	CX, "".~r1+16(SP)
00078 MOVQ	AX, "".~r1+24(SP)
00079 RET

======================================================================

b6: ← b3                            ; make new slice []byte
v45 = Sub64 <int> v41 v39           ; v45 = arg0 - arg1 = 8 - 0 = 8
v46 = SliceMake <[]byte> v38 v45 v45; v46 = new slice of []byte, len 8
                                    ;   pointing to {s.b} memory
v47 = Copy <[][]byte> v99           ; v47 = copy {z} slice [][]byte
v48 = SlicePtr <*[]byte> v47        ; v48 = z.ptr
v49 = SliceLen <int> v47            ; v49 = z.len
v50 = SliceCap <int> v47            ; v50 = z.cap
v52 = Add64 <int> v49 v51           ; v52 = z.len + 1
v53 = Greater64 <bool> v52 v50      ; v53 = (z.len + 1) > z.cap
If v53 → b8 b9 (unlikely)           ; if v53 true
                                    ;   goto {b8} to growslice, else
                                    ;   goto {b9} to append element

> z0 := s.b[:8]
> if len(z) + 1 > cap(z) {
>   goto {b8}
> } else {
>   goto {b9}
> }

00042 MOVQ	DX, "".z.ptr-112(SP)
00043 MOVQ	BX, "".z.len-128(SP)
00044 MOVQ	AX, "".z.cap-120(SP)
00045 LEAQ (CX)(CX*2), CX
00046 MOVQ	$8, 8(DX)(CX*8)
00047 MOVQ	$8, 16(DX)(CX*8)
00048 MOVL runtime.writeBarrier(SB), DI
00049 LEAQ (DX)(CX*8), R8
00050 TESTL DI, DI
00051 JNE	54

======================================================================

b7: ← b3                            ; panic for bad slice
v43 = Copy <mem> v37                ; copy map iter mem state after
                                    ;   reading cur key in {b3}
v44 = StaticCall <mem> {runtime.panicslice} v43 
Exit v44

> runtime.panicslice()

00052 MOVQ	SI, (DX)(CX*8)
00053 JMP	25

00054 MOVQ	R8, (SP)
00055 MOVQ	SI, 8(SP)
00056 PCDATA $0, $2
00057 CALL runtime.writebarrierptr(SB)
00058 JMP	25

======================================================================

b8: ← b6                            ; grow slice
v55 = Copy <mem> v37                ; copy cur key S mem state
v56 = Store <mem> {*uint8} v9 v54 v55 ; put {type.[]uint8} addrto arg0
v58 = Store <mem> {*[]byte} v57 v48 v56 ; put {z.ptr} to arg1
v60 = Store <mem> {int} v59 v49 v58 ; put {z.len} to arg2
v62 = Store <mem> {int} v61 v50 v60 ; put {z.cap} to arg3
v64 = Store <mem> {int} v63 v52 v62 ; put {z.len+1} new size to arg4
; func growslice(typ *byte, old []any, cap int) (ary []any)
v65 = StaticCall <mem> {runtime.growslice} [64] v64
                                    ; call runtime.growslice with args
                                    ; of 64 bytes long
v67 = Load <*[]byte> v66 v65        ; load new z.ptr
v69 = Load <int> v68 v65            ; load new z.len
v71 = Load <int> v70 v65            ; load new z.cap
v72 = Add64 <int> v69 v51           ; v72 = z.len + 1
Plain → b9                          ; goto to append

> runtime.growslice([]uint8.(type), z, len(z) + 1)

00059 MOVQ	CX, "".z.len-128(SP)
00060 LEAQ type.[]uint8(SB), SI
00061 MOVQ	SI, (SP)
00062 MOVQ	DX, 8(SP)
00063 MOVQ	CX, 16(SP)
00064 MOVQ	AX, 24(SP)
00065 MOVQ	BX, 32(SP)
00066 PCDATA $0, $1
00067 CALL runtime.growslice(SB)

======================================================================

b9: ← b6 b8                         ; append element
v74 = Phi <*[]byte> v48 v67         ; v74 = z.ptr
v75 = Phi <int> v52 v72             ; v75 = z.len + 1
v76 = Phi <int> v50 v71             ; v76 = z.cap
v81 = Phi <mem> v37 v65             ; latest mem state
v73 = Copy <[]byte> v46             ; copy new slice of len 8
v77 = PtrIndex <*[]byte> v74 v49    ; v77 = z[z.len] ptr of el ix
v78 = PtrIndex <*[]byte> v77 v39    ; v78 = z[z.len]+0 ptr of el ix
v79 = SliceLen <int> v73            ; v79 = new slice len (8)
v80 = OffPtr <*int> [8] v78         ; v80 = ptr to z last el
v82 = Store <mem> {int} v80 v79 v81 ; z[z.len]+8 = z0.len
v83 = SliceCap <int> v73            ; z0.cap 8
v84 = OffPtr <*int> [16] v78        ; v84 = z[z.len]+16 ptr
v85 = Store <mem> {int} v84 v83 v82 ; z[z.len]+16 = z0.cap 8
v86 = SlicePtr <*uint8> v73         ; v86 = z0.ptr
v87 = Store <mem> {*uint8} v78 v86 v85
                                    ; z[z.len]+0 = z0.ptr
v88 = SliceMake <[][]byte> v74 v75 v76
                                    ; make {z} grow by 1
v101 = Copy <*S> v36                ; copy {s} local key var
Plain → b4                          ; goto {b4}

> z[z.len] = z0
> z = z[:len(z)+1]

name z[[][]byte]: v7 v47 v88 v96 v99
name &k[*S]: v13 v36 v100 v101

00068 MOVQ 40(SP), DX
00069 MOVQ 48(SP), AX
00070 MOVQ 56(SP), CX
00071 LEAQ 1(AX), BX
00072 MOVQ "".&k-104(SP), SI
00073 MOVQ	CX, AX
00074 MOVQ "".z.len-128(SP), CX
00075 JMP	42

Conclusion

The original keys function rolls out into something like:

func keys(m map[S]struct{}) [][]byte {
    var z [][]byte

    var iter map.iter[S]struct{}
    runtime.mapiterinit(map[S]struct{}.(type), m, &iter)

    s := new(S)
    for iter.key != nil {
        if iter.key == nil {
           panic("key is nil")
        }
        *s = *iter.key
        if s.b == nil {
           panic("no next key")
        }

        runtime.mapiternext(&iter)

        z0 := s.b[:8]
        if len(z) + 1 > cap(z) {
            runtime.growslice([]uint8.(type), z, len(z) + 1)
        }

        z[len(z)] = z0
        z = z[:len(z)+1]
    }

    return z
}

Compiler allocated separate local variable for keeping local state of the next item from the iterator on each iteration of for-range loop. Slice which is appending to the {z} slice is created against the same memory area {s.b}. That is why function ends up with a slice {z} containing the same values.

Go SSA does not differ much from the target assembly, although blocks and instructions got reduced and sorted during various optimization passes.

Links

blog comments powered by Disqus