├── go.mod ├── go.sum ├── .gitignore ├── .devcontainer └── devcontainer.json ├── .dockerignore ├── docs ├── 01-prereqs │ ├── README.md │ ├── native │ │ ├── README.md │ │ ├── 03-vscode.md │ │ ├── 01-go1.18beta2.md │ │ └── 02-delve.md │ ├── 01-native.md │ └── 02-docker.md ├── 03-escape-analysis │ ├── README.md │ ├── examples │ │ ├── ex5 │ │ │ └── main.go │ │ ├── ex6 │ │ │ └── main.go │ │ ├── ex4 │ │ │ └── main.go │ │ ├── ex3 │ │ │ └── main.go │ │ ├── ex2 │ │ │ └── main.go │ │ └── ex1 │ │ │ └── main.go │ ├── 01-overview.md │ ├── 05-tests.md │ ├── 04-move.md │ ├── 03-escape.md │ └── 02-leak.md ├── 02-interface-values │ ├── examples │ │ └── ex1 │ │ │ ├── main.go │ │ │ └── ex1.go │ ├── 01-storing-an-interface-value.md │ ├── README.md │ ├── images │ │ ├── 09-on-the-stack-fig1.ascii │ │ ├── 09-on-the-stack-fig2.ascii │ │ ├── 09-on-the-stack-fig3.ascii │ │ ├── 09-on-the-stack-fig4.ascii │ │ ├── 09-on-the-stack-fig5.ascii │ │ ├── 09-on-the-stack-fig6.ascii │ │ └── 09-on-the-stack-fig1.svg │ ├── 02-using-an-interface-value.md │ ├── 03-type-assertion.md │ ├── 08-copy-on-store.md │ ├── 06-underlying-value.md │ ├── 04-a-pair-of-pointers.md │ ├── 07-special-values.md │ └── 05-underlying-type.md ├── 04-missing-mallocs │ ├── examples │ │ └── ex1 │ │ │ ├── ex1.go │ │ │ └── ex1_test.go │ ├── README.md │ ├── 01-shut-up.md │ ├── images │ │ ├── 02-why-fig1.ascii │ │ ├── 02-why-fig2.ascii │ │ ├── 02-why-fig3.ascii │ │ ├── 02-why-fig4.ascii │ │ ├── 02-why-fig5.ascii │ │ ├── 02-why-fig6.ascii │ │ ├── 02-why-fig10.ascii │ │ ├── 02-why-fig7.ascii │ │ ├── 02-why-fig8.ascii │ │ ├── 02-why-fig9.ascii │ │ ├── 02-why-fig1.svg │ │ ├── 02-why-fig2.svg │ │ └── 02-why-fig3.svg │ └── 04-what.md ├── 05-lessons-learned │ └── README.md └── 99-appendix │ ├── assembly.md │ └── two-integers.md ├── .github └── workflows │ ├── lint.yml │ ├── test.yml │ ├── image-push-all.yml │ └── image-build-all.yml ├── .markdownlint.yaml ├── tests ├── lem │ ├── lem_test.go │ ├── lem_move_test.go │ ├── lem_noescape_test.go │ ├── lem_leak_test.go │ └── lem_escape_test.go └── mem │ ├── mem_test.go │ └── types_test.go ├── Dockerfile ├── hack ├── b2md.py └── asm2md.py ├── README.md └── Makefile /go.mod: -------------------------------------------------------------------------------- 1 | module go-interface-values 2 | 3 | go 1.17 4 | 5 | require github.com/akutz/lem v0.1.1 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/akutz/lem v0.1.1 h1:o39hP668OURQyILMZGeXh8B3oeqijVBRhsAIWl3MTkg= 2 | github.com/akutz/lem v0.1.1/go.mod h1:t4d3aqIZedAScewe5vXQQMH1/4uG2Z2pdsNzU+nnr20= 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.a 2 | *.o 3 | *.bin 4 | *.out 5 | *.test 6 | profile*.svg 7 | *.pid 8 | *.class 9 | *.profile 10 | *.ex* 11 | .DS_Store 12 | .vscode/ 13 | bin/ 14 | obj/ 15 | 16 | /lem 17 | 18 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Go interface values DevContainer", 3 | "context": "..", 4 | "dockerFile": "../Dockerfile", 5 | "settings": {}, 6 | "extensions": ["golang.go"] 7 | } 8 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git/ 2 | .vscode/ 3 | 4 | *.a 5 | *.bin 6 | *.ex* 7 | *.out 8 | *.test 9 | profile*.svg 10 | *.pid 11 | *.class 12 | .DS_Store 13 | 14 | **/*.a 15 | **/*.bin 16 | **/*.ex* 17 | **/*.out 18 | **/*.test 19 | **/profile*.svg 20 | **/*.pid 21 | **/*.class 22 | **/*.profile 23 | **/.DS_Store 24 | 25 | **/bin/ 26 | **/obj/ 27 | 28 | /lem -------------------------------------------------------------------------------- /docs/01-prereqs/README.md: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | 3 | This section reviews the prerequisites necessary to run the examples in this repository. 4 | 5 | * [**Native**](./01-native.md): run the examples natively on the local system 6 | * [**Docker**](./02-docker.md): run the examples in a Docker container 7 | 8 | --- 9 | 10 | Next: [Native](./01-native.md) 11 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: lint 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Check out code 10 | uses: actions/checkout@v2 11 | - name: README.md 12 | uses: avto-dev/markdown-lint@v1.5.0 13 | with: 14 | config: './.markdownlint.yaml' 15 | args: './README.md' 16 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | 10 | - uses: actions/checkout@v2 11 | 12 | - name: Install Go 13 | uses: actions/setup-go@v2 14 | with: 15 | stable: 'false' 16 | go-version: '1.18.0-beta2' 17 | 18 | - name: Test 19 | run: make test 20 | -------------------------------------------------------------------------------- /docs/01-prereqs/native/README.md: -------------------------------------------------------------------------------- 1 | # Native 2 | 3 | This section provides help for installing the following tooling required to run the examples from this repository. 4 | 5 | --- 6 | 7 | :warning: **Please note** this section is incomplete and will be completed when I have time or by PRs from others. 8 | 9 | --- 10 | 11 | * [**Go 1.18beta2**](./01-go1.18beta2.md) 12 | * [**delve 1.18**](./02-delve.md) 13 | * [**VS Code**](./03-vscode.md) 14 | 15 | --- 16 | 17 | Next: [Go 1.18beta2](./01-go1.18beta2.md) 18 | -------------------------------------------------------------------------------- /docs/03-escape-analysis/README.md: -------------------------------------------------------------------------------- 1 | # Escape analysis 2 | 3 | Before we dig into why storing a value in an interface can result in a heap allocation, we need to discuss escape analysis: 4 | 5 | * **[An overview](./01-overview.md)**: an overview of escape analysis 6 | * **[Leak](./02-leak.md)**: potential for increased pressure on the garbage collector 7 | * **[Escape](./03-escape.md)**: when go is unable to store a reference type on the stack 8 | * **[Move](./04-move.md)**: when go moves a value type into the heap 9 | * **[Tests](./05-tests.md)**: a test suite to validate what we have learned 10 | 11 | --- 12 | 13 | Next: [An overview](./01-overview.md) 14 | -------------------------------------------------------------------------------- /.markdownlint.yaml: -------------------------------------------------------------------------------- 1 | # The complete config reference is available at 2 | # https://github.com/DavidAnson/markdownlint/blob/main/schema/.markdownlint.yaml. 3 | 4 | default: true 5 | 6 | # MD010/no-hard-tabs - Hard tabs 7 | MD010: 8 | # Include code blocks 9 | code_blocks: false 10 | 11 | # MD012/no-multiple-blanks - Multiple consecutive blank lines 12 | MD012: 13 | # Consecutive blank lines 14 | maximum: 2 15 | 16 | # MD013/line-length - Line length 17 | MD013: 18 | # Number of characters 19 | line_length: -1 20 | 21 | # MD022/blanks-around-headings/blanks-around-headers - Headings should be surrounded by blank lines 22 | MD022: 23 | # Blank lines above heading 24 | lines_above: 2 25 | 26 | -------------------------------------------------------------------------------- /docs/02-interface-values/examples/ex1/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | func main() { 20 | ex1() 21 | } 22 | -------------------------------------------------------------------------------- /docs/02-interface-values/01-storing-an-interface-value.md: -------------------------------------------------------------------------------- 1 | # Storing an interface value 2 | 3 | The first question people probably have is _What is an interface value?_ To that I say _Dang, I was hoping you would tell me!_. Jokes aside, please consider the following ([Golang playground](https://go.dev/play/p/q6LRdv5H2Rw)): 4 | 5 | ```go 6 | var x int64 7 | var y interface{} 8 | x = 2 9 | y = x 10 | ``` 11 | 12 | What happens when `y` is assigned the value of `x`? This is known as _storing a value in an interface_, or just _an interface value_. 13 | 14 | But how do we _use_ a value once it is stored in an interface? Keep reading to find out! 15 | 16 | --- 17 | 18 | Next: [Using an interface value](./02-using-an-interface-value.md) 19 | -------------------------------------------------------------------------------- /docs/04-missing-mallocs/examples/ex1/ex1.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package ex1 18 | 19 | func ex1() { 20 | var x int64 21 | x = 2 22 | sink = x 23 | } 24 | 25 | var sink interface{} 26 | -------------------------------------------------------------------------------- /docs/02-interface-values/examples/ex1/ex1.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | func ex1() { 20 | var x int64 21 | var i interface{} 22 | x = 2 23 | i = x 24 | _ = i 25 | } 26 | -------------------------------------------------------------------------------- /docs/03-escape-analysis/examples/ex5/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | var sink *int32 20 | 21 | //go:noinline 22 | func main() { 23 | var id int32 = 4096 24 | sink = &id 25 | } 26 | -------------------------------------------------------------------------------- /docs/03-escape-analysis/examples/ex6/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | var sink interface{} 20 | 21 | //go:noinline 22 | func main() { 23 | var id int32 = 4096 24 | sink = id 25 | } 26 | -------------------------------------------------------------------------------- /docs/01-prereqs/01-native.md: -------------------------------------------------------------------------------- 1 | # Native 2 | 3 | All of the below software must be installed and configured correctly to run the examples in this repository: 4 | 5 | * [**Go 1.18beta2+**](https://go.dev/dl/#go1.18beta2): the development version of Go 6 | * [**delve 1.18+**](https://github.com/go-delve/delve): the Go debugger 7 | * [**Python 3**](https://python.org): for parsing Go benchmark results 8 | 9 | While it _is_ possible to do so successfully both on macOS and Linux, it is highly recommended to use Docker. 10 | 11 | * For those who _really_ want to run the examples on their local system, please proceed to the [_Native_](./native/) section. 12 | * For those who just want to run the examples without fuss or muss, please proceed normally. 13 | 14 | --- 15 | 16 | Next: [Docker](./02-docker.md) 17 | -------------------------------------------------------------------------------- /docs/04-missing-mallocs/examples/ex1/ex1_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package ex1 18 | 19 | import "testing" 20 | 21 | func BenchmarkEscapeNoMalloc(b *testing.B) { 22 | for i := 0; i < b.N; i++ { 23 | ex1() 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /docs/03-escape-analysis/examples/ex4/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | var sink *int32 20 | 21 | //go:noinline 22 | func main() { 23 | id1 := new(int32) // new(int32) escapes to the heap 24 | *id1 = 4096 25 | sink = id1 26 | } 27 | -------------------------------------------------------------------------------- /.github/workflows/image-push-all.yml: -------------------------------------------------------------------------------- 1 | name: image-push-all 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'main' 7 | 8 | jobs: 9 | upload: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Set up QEMU 13 | uses: docker/setup-qemu-action@v1 14 | 15 | - name: Set up Docker Buildx 16 | uses: docker/setup-buildx-action@v1 17 | 18 | - name: Login to DockerHub 19 | uses: docker/login-action@v1 20 | with: 21 | username: ${{ secrets.DOCKERHUB_USERNAME }} 22 | password: ${{ secrets.DOCKERHUB_TOKEN }} 23 | 24 | - name: Build and push 25 | uses: docker/build-push-action@v2 26 | with: 27 | platforms: linux/amd64,linux/arm64 28 | push: true 29 | tags: ${{ secrets.DOCKERHUB_USERNAME }}/go-interface-values:latest 30 | -------------------------------------------------------------------------------- /.github/workflows/image-build-all.yml: -------------------------------------------------------------------------------- 1 | name: image-build-all 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches-ignore: 7 | - main 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Set up QEMU 14 | uses: docker/setup-qemu-action@v1 15 | 16 | - name: Set up Docker Buildx 17 | uses: docker/setup-buildx-action@v1 18 | 19 | - name: Login to DockerHub 20 | uses: docker/login-action@v1 21 | with: 22 | username: ${{ secrets.DOCKERHUB_USERNAME }} 23 | password: ${{ secrets.DOCKERHUB_TOKEN }} 24 | 25 | - name: Build 26 | uses: docker/build-push-action@v2 27 | with: 28 | platforms: linux/amd64,linux/arm64 29 | push: false 30 | tags: ${{ secrets.DOCKERHUB_USERNAME }}/go-interface-values:latest 31 | -------------------------------------------------------------------------------- /tests/lem/lem_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package lem_test 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/akutz/lem" 23 | ) 24 | 25 | var lemFuncs = map[string]func(*testing.B){} 26 | 27 | func TestLem(t *testing.T) { 28 | lem.SetBenchmem("true") 29 | lem.SetBenchtime("1000x") 30 | lem.RunWithBenchmarks(t, lemFuncs) 31 | } 32 | -------------------------------------------------------------------------------- /docs/03-escape-analysis/examples/ex3/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | var sink *int32 20 | 21 | //go:noinline 22 | func recordID(id *int32) { // leaking param: id 23 | sink = id 24 | } 25 | 26 | //go:noinline 27 | func main() { 28 | id1 := new(int32) // new(int32) escapes to the heap 29 | *id1 = 4096 30 | recordID(id1) 31 | } 32 | -------------------------------------------------------------------------------- /docs/04-missing-mallocs/README.md: -------------------------------------------------------------------------------- 1 | # Missing mallocs 2 | 3 | You are probably saying to yourself "Bury the lead much?" Heh. The thing is, it is really crucial to understand how storing values in an interface and escape analysis works in order to make sense of why _sometimes_ it is possible to store a value in an interface that escapes to the heap...and does not need any new memory to do so. 4 | 5 | _Say what!?_ 6 | 7 | Yep, you heard me..._magic_. Okay, not really, but it is due to something about which I certainly had no clue until diving down this rabbit hole. This section reviews: 8 | 9 | * [**Shut up and prove it**](./01-shut-up.md): an example of when this situation will occur 10 | * [**Looking at the assembly**](./02-why.md): looking closely at why this happens 11 | * [**When it will happen?**](./03-when.md): all the scenarios where we won't see mallocs 12 | * [**Overall impact**](./04-what.md): what is the impact of this behavior? 13 | 14 | --- 15 | 16 | Next: [Shut up and prove it](./01-shut-up.md) 17 | -------------------------------------------------------------------------------- /docs/02-interface-values/README.md: -------------------------------------------------------------------------------- 1 | # Interface values 2 | 3 | This section answers that age-old question _What **is** an interface value?_ 4 | 5 | * [**Storing an interface value**](./01-storing-an-interface-value.md): how to store a value in an interface 6 | * [**Using an interface value**](./02-using-an-interface-value.md): _what's in the box? **what's in the box!?**_ 7 | * [**Type assertions**](./03-type-assertions.md): type assertions reveal more than just type 8 | * [**A pair of pointers**](./04-a-pair-of-pointers.md): interfaces under the covers 9 | * [**Underlying type**](./05-underlying-type.md): they are just my type! 10 | * [**Underlying value**](./06-underlying-value.md): what lies beneath 11 | * [**Special values**](./07-special-values.md): mr. rogers would say we are _all_ special... 12 | * [**Copy on store**](./08-copy-on-store.md): addressing the situation with copies 13 | * [**On the stack**](./09-on-the-stack.md): stacks on stacks! 14 | 15 | --- 16 | 17 | Next: [Storing an interface value](./01-storing-an-interface-value.md) 18 | -------------------------------------------------------------------------------- /docs/03-escape-analysis/examples/ex2/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import "os" 20 | 21 | //go:noinline 22 | func validateID(id *int32) *int32 { // leaking param: id to result ~r1 level=0 23 | return id 24 | } 25 | 26 | func main() { 27 | var id1 int32 = 4096 28 | if validateID(&id1) == nil { 29 | os.Exit(1) 30 | } 31 | 32 | var id2 *int32 = new(int32) // new(int32) does not escape 33 | *id2 = 4096 34 | validID := validateID(id2) 35 | if validID == nil { 36 | os.Exit(1) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /docs/02-interface-values/images/09-on-the-stack-fig1.ascii: -------------------------------------------------------------------------------- 1 | bottom of stack frame +-> local variables 2 | high address +---------------------------------------+ | +---------------------------------------+ <-+ 3 | | arguments cFFF | | | name꞉ x | |07 4 | | +---------------------------------------+ | | type꞉ int64 | |06 5 | | | return address (PC) c9F9 | | | size꞉ 8 bytes | |05 6 | | +---------------------------------------+ | | | |04 7 | | | frame pointer c6C6 | | | | |03 8 | | +---------------------------------------+ | | | |02 9 | | | return values c696 | | | c336 | |01 10 | v +---------------------------------------+ | +---------------------------------------+ <-+00 0(SP) 11 | | local variables c363 | -+-> 12 | low address +---------------------------------------+ 13 | stack pointer (SP) 14 | top of stack frame -------------------------------------------------------------------------------- /tests/lem/lem_move_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package lem_test 18 | 19 | import ( 20 | "testing" 21 | ) 22 | 23 | // lem.move1.name=pointing to stack from heap 24 | // lem.move1.alloc=1 25 | // lem.move1.bytes=4 26 | func move1(b *testing.B) { 27 | var sink *int32 28 | for i := 0; i < b.N; i++ { 29 | var x int32 = 4096 // lem.move1.m=moved to heap: x 30 | sink = &x 31 | } 32 | _ = sink 33 | } 34 | func init() { 35 | lemFuncs["move1"] = move1 36 | } 37 | 38 | // lem.move2.name=too large for stack 39 | // lem.move2.alloc=1 40 | // lem.move2.bytes=15728640-15728700 41 | func move2(b *testing.B) { 42 | for i := 0; i < b.N; i++ { 43 | var buf [15 * 1024 * 1024]byte // lem.move2.m=moved to heap: buf 44 | _ = buf 45 | } 46 | } 47 | func init() { 48 | lemFuncs["move2"] = move2 49 | } 50 | -------------------------------------------------------------------------------- /docs/02-interface-values/02-using-an-interface-value.md: -------------------------------------------------------------------------------- 1 | # Using an interface value 2 | 3 | The previous page showed how to store a value in an interface, but how do we use the value stored in `y`? There are _some_ operations that we can perform without needing to know what the value of `y` is or its type, ex. ([Golang playground](https://go.dev/play/p/TULVVw36Q82)): 4 | 5 | ```go 6 | var x int64 7 | var y interface{} 8 | x = 2 9 | y = x 10 | fmt.Println(x, y) 11 | ``` 12 | 13 | The above code prints the expected output: 14 | 15 | ```bash 16 | 2 2 17 | ``` 18 | 19 | But what about an operation where the type of multiple operands matter, such as the sum of `x` and `y` ([Golang playground](https://go.dev/play/p/EOSUArFfjIP))? 20 | 21 | ```go 22 | fmt.Println(x + y) 23 | ``` 24 | 25 | Instead of emitting `4`, the program will fail to compile with the following error: 26 | 27 | ```bash 28 | invalid operation: x + y (mismatched types int64 and interface {}) 29 | ``` 30 | 31 | In order to sum `x` and `y`, they must be the same type. Let's try the example one more time ([Golang playground](https://go.dev/play/p/izfHznJns2F)): 32 | 33 | ```go 34 | fmt.Println(x + y.(int64)) 35 | ``` 36 | 37 | Now the code compiles, runs, and emits the expected output of `4`. 38 | 39 | The syntax `y.(int64)` is known as _type assertion_. Yet, how do we know the type assertion only worked because the literal value of `2` is a valid value for the type `int64`? 40 | 41 | Please continue to the next page to learn how type assertion reveals the inner-workings of interface values... 42 | 43 | --- 44 | 45 | Next: [Type assertion](./03-type-assertion.md) 46 | -------------------------------------------------------------------------------- /docs/03-escape-analysis/examples/ex1/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | var lastLoginAttemptUsername *string 20 | 21 | //go:noinline 22 | func Login( 23 | username *string, // leaking param 24 | password *string, // does not escape 25 | token interface{}, // leaking param to result ~r3 level=0 26 | ) interface{} { 27 | 28 | lastLoginAttemptUsername = username 29 | switch { 30 | case username != nil && password != nil: 31 | return "newLoginToken" // escapes to heap 32 | case token != nil: 33 | return token 34 | default: 35 | return nil 36 | } 37 | } 38 | 39 | //go:noinline 40 | func main() { 41 | var ( 42 | username1 = "fake" // moved to heap 43 | username2 = new(string) // escapes to heap 44 | password = "fake" // does not escape 45 | cookieJar [15 * 1024 * 1024]byte // moved to heap 46 | ) 47 | 48 | token := Login(&username1, &password, nil) 49 | if token != nil { 50 | _ = cookieJar 51 | } 52 | 53 | *username2 = username1 54 | Login(username2, nil, token) 55 | } 56 | -------------------------------------------------------------------------------- /docs/01-prereqs/native/03-vscode.md: -------------------------------------------------------------------------------- 1 | # Visual Studio Code 2 | 3 | Setting up VS Code to use Go 1.18beta2 is fairly straight-forward: 4 | 5 | * [**Update the Go language server**](#update-the-go-language-server): build `gopls` with Go 1.18beta2 6 | * [**Configure `GOROOT`**](#configure-goroot): update VS Code to use Go 1.18beta2 7 | 8 | 9 | ## Update the Go language server 10 | 11 | VS Code uses the Go language server, `gopls`, for dot completion, validating the source code, etc. Suffice it to say, for VS Code to function correctly with Go 1.18beta2, the Go language server needs to know about Go 1.18beta2. To update `gopls`, please follow the commands below: 12 | 13 | 1. Open a console window. 14 | 15 | 1. Execute the following command: 16 | 17 | ```bash 18 | go install golang.org/x/tools/gopls@latest 19 | ``` 20 | 21 | ## Configure `GOROOT` 22 | 23 | Now that the language server is updated, we can configure VS Code to use Go 1.18beta2: 24 | 25 | 1. Open up a console window. 26 | 27 | --- 28 | 29 | :warning: **Please note** the next step assumes the `go` program is 1.18beta2, which it _should_ be if the instructions in the previous section were followed. 30 | 31 | --- 32 | 33 | 1. Execute the following command: 34 | 35 | ```bash 36 | go env GOROOT 37 | ``` 38 | 39 | 1. Write down the path printed by the above command. 40 | 41 | 1. Open VS Code's preferences/settings. 42 | 43 | 1. Search for `GOROOT` 44 | 45 | 1. Click on `Edit in settings.json` 46 | 47 | 1. Modify or add the property `go.goroot` and set it to the directory recorded up above. 48 | 49 | Voilá, VS Code should now be able to sucessfully handle Go 1.18beta2 code, including generics! 50 | 51 | --- 52 | 53 | Next: [Interface values](../../02-interface-values/) 54 | -------------------------------------------------------------------------------- /docs/01-prereqs/native/01-go1.18beta2.md: -------------------------------------------------------------------------------- 1 | # Installing Go 1.18beta2 2 | 3 | Go 1.18beta2 may be installed a number of ways: 4 | 5 | * [**On a clean system**](#on-a-clean-system): install Go 1.18beta2 on a system without Go 6 | * [**Using Go**](#using-go): or use an earlier version of Go to install Go 1.18beta2 7 | 8 | ## On a clean system 9 | 10 | 1. Download the appropriate file from the Go download page under the section [\#go1.18beta2](https://go.dev/dl/#go1.18beta2). 11 | 12 | 1. Follow the [normal instructions](https://go.dev/doc/install), subsituting the above file for the one from the instructions. 13 | 14 | ## Using Go 15 | 16 | If the system already has a recent version of Go installed, Go 1.18beta2 may be installed with the following commands: 17 | 18 | 1. Download the Go 1.18beta2 installer: 19 | 20 | ```bash 21 | go install golang.org/dl/go1.18beta2@latest 22 | ``` 23 | 24 | 1. Depending on how your system is configured, the installer was downloaded to three possible locations. The following command will ensure the path to the installer is part of the shell's path: 25 | 26 | ```bash 27 | { [ -d "${GOBIN}" ] && _D="${GOBIN}"; } || \ 28 | { [ -d "${GOPATH}" ] && [ -d "${GOPATH}/bin" ] && _D="${GOPATH}/bin"; } || \ 29 | _D="${HOME}/go/bin" && _F="${_D}/go1.18beta2" && \ 30 | { [ -f "${_F}" ] && export PATH="${_D}:${PATH}"; } || 31 | echo "Could not locate go1.18beta2 program" 1>&2 32 | ``` 33 | 34 | 1. Run the following command to install Go 1.18beta2: 35 | 36 | ```bash 37 | go1.18beta2 download 38 | ``` 39 | 40 | 1. Verify `go1.18beta2` is available: 41 | 42 | ```bash 43 | $ go1.18beta2 version 44 | go version go1.18beta2 linux/amd64 45 | ``` 46 | 47 | --- 48 | 49 | Next: [Installing delve](./02-delve.md) 50 | -------------------------------------------------------------------------------- /tests/lem/lem_noescape_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package lem_test 18 | 19 | import ( 20 | "testing" 21 | ) 22 | 23 | // lem.noescape1.name=pointer does not outlive its call stack 24 | // lem.noescape1.alloc=0 25 | // lem.noescape1.bytes=0 26 | func noescape1(b *testing.B) { 27 | f := func(p *int32) *int32 { // lem.noescape1.m=leaking param: p to result ~r[0-1] level=0 28 | return p 29 | } 30 | b.ResetTimer() 31 | for i := 0; i < b.N; i++ { 32 | f(new(int32)) // lem.noescape1.m=new\(int32\) does not escape 33 | } 34 | } 35 | func init() { 36 | lemFuncs["noescape1"] = noescape1 37 | } 38 | 39 | // lem.noescape2.name=value types do not escape 40 | // lem.noescape2.alloc=0 41 | // lem.noescape2.bytes=0 42 | func noescape2(b *testing.B) { 43 | var sink int32 44 | b.ResetTimer() 45 | for i := 0; i < b.N; i++ { 46 | var x int32 47 | x = 256 48 | sink = x 49 | } 50 | _ = sink 51 | } 52 | func init() { 53 | lemFuncs["noescape2"] = noescape2 54 | } 55 | 56 | // lem.noescape3.name=value types do not leak 57 | // lem.noescape3.alloc=0 58 | // lem.noescape3.bytes=0 59 | func noescape3(b *testing.B) { 60 | var sink int32 61 | f := func(x int32) { 62 | sink = x 63 | } 64 | b.ResetTimer() 65 | for i := 0; i < b.N; i++ { 66 | f(0) 67 | } 68 | _ = sink 69 | } 70 | func init() { 71 | lemFuncs["noescape3"] = noescape3 72 | } 73 | -------------------------------------------------------------------------------- /docs/02-interface-values/03-type-assertion.md: -------------------------------------------------------------------------------- 1 | # Type assertion 2 | 3 | So far the examples have revolved around storing an `int64` in an interface: 4 | 5 | ```go 6 | var x int64 7 | var y interface{} 8 | x = 2 9 | y = x 10 | ``` 11 | 12 | We also now know that in order to use `y` in situations where type matters, such as `x + y`, we must assert that `y` _is_ an `int64` just like `x`: 13 | 14 | ```go 15 | fmt.Println(x + y.(int64)) 16 | ``` 17 | 18 | However, the previous page asked the following question: _How do we know the type assertion only worked because the literal value of `2` is a valid value for the type `int64`?_ 19 | 20 | One way to answer the quesiton is by attempting to assert `y` is a `string` ([Golang playground](https://go.dev/play/p/jEqgcZWXKH9)): 21 | 22 | ```go 23 | var s string 24 | s = y.(string) 25 | ``` 26 | 27 | The above code compiles fine, but running the example results in a panic: 28 | 29 | ```bash 30 | panic: interface conversion: interface {} is int64, not string 31 | ``` 32 | 33 | We cannot assert `y` is a `string` because the value stored in `y` is an `int64`. In fact, if you think about it, there is an even stricter constraint if we were to remove interfaces from the equation altogether ([Golang playground](https://go.dev/play/p/w24uFcZppRh)): 34 | 35 | ```golang 36 | var x int64 37 | var s string 38 | x = 2 39 | s = x.(string) 40 | ``` 41 | 42 | Forget a runtime panic, the above example does not even compile: 43 | 44 | ```bash 45 | invalid type assertion: x.(string) (non-interface type int64 on left) 46 | ``` 47 | 48 | The above examples demonstrate: 49 | 50 | * There are runtime checks to disallow invalid type assertions for interface values. 51 | * There are compile-type checks to disallow invalid type assertions. 52 | 53 | Taking these facts into account, we can assume that storing a value in an interface must still preserve the value's type in some way. Keep reading to find out how! 54 | 55 | --- 56 | 57 | Next: [A pair of pointers](./04-a-pair-of-pointers.md) 58 | -------------------------------------------------------------------------------- /docs/02-interface-values/08-copy-on-store.md: -------------------------------------------------------------------------------- 1 | # Copy on store 2 | 3 | The examples have gotten pretty complex, but that has been necessary in order to ask this question: _What is being assigned to `y` in the following example?_ 4 | 5 | ```go 6 | var x int64 7 | var y interface{} 8 | x = 2048 9 | y = x 10 | ``` 11 | 12 | Because of how interfaces "wrap" underlying types and values, we might be tempted to say _The variable `y` is assigned `x`._ Except that is not entirely accurate. Let's use a different example: 13 | 14 | ```go 15 | var x int64 16 | var y int64 17 | x = 2048 18 | y = x 19 | ``` 20 | 21 | Same question. I think we would all answer _The variable `y` is assigned the _**value**_ of the variable `x`_. That would be correct. And we know that modifying `x` later would not affect `y` ([Golang playground](https://go.dev/play/p/jYC8OO5a02a)): 22 | 23 | ```go 24 | package main 25 | 26 | import "fmt" 27 | 28 | func main() { 29 | var x int64 30 | var y int64 31 | x = 2048 32 | y = x 33 | fmt.Printf("x=%d, y=%d\n", x, y) 34 | 35 | x = 4096 36 | fmt.Printf("x=%d, y=%d\n", x, y) 37 | } 38 | ``` 39 | 40 | The above example of course emits: 41 | 42 | ```bash 43 | x=2048, y=2048 44 | x=4096, y=2048 45 | ``` 46 | 47 | So why would think anything different should happen when `y` is an interface ([Golang playground](https://go.dev/play/p/Gcs91tBp09T))? 48 | 49 | ```go 50 | var y interface{} 51 | ``` 52 | 53 | The program emits the same output as before: 54 | 55 | ```bash 56 | x=2048, y=2048 57 | x=4096, y=2048 58 | ``` 59 | 60 | This is because storing a value in an interface results in a copy of that value, just like when assigning a value to another variable of the same type. Even if the value is a pointer, you are creating a copy of that pointer. 61 | 62 | This _does_ raise the question though -- if an interface is just two addresses to the underlying type and value, where is the copied value of `y`? We know it's: 63 | 64 | * not in the interface itself 65 | * not in `staticuint64s` (see [_Special values_](./07-special-values.md)) as the value is greater than `255` 66 | 67 | So where is it? It is in one of two locations, and we are about to take a look at the first one... 68 | 69 | --- 70 | 71 | Next: [On the stack](./09-on-the-stack.md) 72 | -------------------------------------------------------------------------------- /docs/03-escape-analysis/01-overview.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | Go reclaims memory allocated on the heap via garbage collection, but this can be a very expensive process. Compare that to the [stack](https://github.com/golang/go/blob/master/src/runtime/stack.go) where memory is "cheap" and is freed automatically when its stack frame is destroyed. In order to allocate memory on the stack the Go compiler must evaluate several, determining factors: 4 | 5 | * pointers to stack objects cannot be stored in the heap 6 | * pointers to stack objects cannot outlive the object's stack frame 7 | * stack objects cannot exceed the size of the stack, ex. a 15 MiB buffer `[15 * 1024 * 1024]byte` 8 | 9 | The compile-time process Go use to determine whether memory is dynamically managed on the heap or can be allocated on the stack is known as [_escape analysis_](https://github.com/golang/go/blob/master/src/cmd/compile/internal/escape/escape.go). Escape analysis walks a program's [abstract syntax tree](https://pkg.go.dev/go/ast) (AST) to build a graph of all the variables encountered. 10 | 11 | It is possible to see which variables end up on the heap by using the compiler flag `-m` when building (or testing) Go code. For example, let's build the following program: 12 | 13 | ```bash 14 | docker run -it --rm go-interface-values \ 15 | go tool compile -m ./docs/03-escape-analysis/examples/ex1/main.go 16 | ``` 17 | 18 | The output should be similar to the following: 19 | 20 | ```bash 21 | ./docs/03-escape-analysis/examples/ex1/main.go:23:2: leaking param: username 22 | ./docs/03-escape-analysis/examples/ex1/main.go:24:2: password does not escape 23 | ./docs/03-escape-analysis/examples/ex1/main.go:25:2: leaking param: token to result ~r0 level=0 24 | ./docs/03-escape-analysis/examples/ex1/main.go:31:10: "newLoginToken" escapes to heap 25 | ./docs/03-escape-analysis/examples/ex1/main.go:42:3: moved to heap: username1 26 | ./docs/03-escape-analysis/examples/ex1/main.go:45:3: moved to heap: cookieJar 27 | ./docs/03-escape-analysis/examples/ex1/main.go:43:18: new(string) escapes to heap 28 | ``` 29 | 30 | This tiny program has variables that _leak_, _escape_, and get _moved_ to the heap. Please keep reading to find out what these terms mean and when and why they occur. 31 | 32 | --- 33 | 34 | Next: [Leak](./02-leak.md) 35 | -------------------------------------------------------------------------------- /docs/01-prereqs/native/02-delve.md: -------------------------------------------------------------------------------- 1 | # Delve 2 | 3 | The [delve](https://github.com/go-delve/delve) program is a debugger for Golang and should feel at home to people familiar with `gdb`. 4 | 5 | * [**Install delve**](#install-delve): install the go debugger 6 | * [**Configure delve**](#configure-delve): ensure delve uses Go 1.18beta2 7 | 8 | ## Install delve 9 | 10 | Install the latest release (1.18+) to support Go 1.18beta2 with the following command: 11 | 12 | ```bash 13 | go install github.com/go-delve/delve/cmd/dlv@latest 14 | ``` 15 | 16 | ## Configure delve 17 | 18 | Even with the latest release, `dlv debug` may not work as expected" 19 | 20 | 1. Consider the following Go program: 21 | 22 | ```go 23 | package main 24 | 25 | import "fmt" 26 | 27 | func print[T any](t T) { 28 | fmt.Println(t) 29 | } 30 | 31 | func main() { 32 | print("Hello, world.") 33 | } 34 | ``` 35 | 36 | 1. Save the program as `main.go` 37 | 38 | 1. Run the program through the debugger: 39 | 40 | ```bash 41 | $ dlv debug main.go 42 | # command-line-arguments 43 | ./main.go:5:6: missing function body 44 | ./main.go:5:11: syntax error: unexpected [, expecting ( 45 | exit status 2 46 | ``` 47 | 48 | This fails because delve [uses the `go` binary from the shell's `PATH`](https://github.com/go-delve/delve/blob/master/pkg/gobuild/gobuild.go) to build a binary suitable for debugging. To fix this issue the Go 1.18 binary needs to be the program called when delve invokes the `go` command: 49 | 50 | 1. If Go 1.18beta2 was installed from the instructions on the previous page, then use the following command to ensure that Go 1.18beta2's `go` binary appears first in the shell's `PATH`: 51 | 52 | ```bash 53 | export PATH="$(go1.18beta2 env GOROOT)/bin:${PATH}" 54 | ``` 55 | 56 | 2. Verify that `go version` shows Go 1.18beta2: 57 | 58 | ```bash 59 | $ go version 60 | go version go1.18beta2 linux/amd64 61 | ``` 62 | 63 | 3. Run the delve debugger again: 64 | 65 | ```bash 66 | $ dlv debug main.go 67 | Type 'help' for list of commands. 68 | (dlv) 69 | ``` 70 | 71 | And that's it! Delve should now be set up to successfully build and debug Go1.18 programs! 72 | 73 | --- 74 | 75 | Next: [Configuring VS Code](./03-vscode.md) 76 | -------------------------------------------------------------------------------- /docs/04-missing-mallocs/01-shut-up.md: -------------------------------------------------------------------------------- 1 | # Shut up and prove it 2 | 3 | So how do we demonstrate that a value escaping to the heap does not result in a heap allocation? With a Go benchmark of course! Consider the following: 4 | 5 | ```go 6 | var sink interface{} 7 | 8 | func ex1() { 9 | var x int64 10 | x = 3 11 | sink = x 12 | } 13 | ``` 14 | 15 | Based on all that we have learned, we can be reasonably certain when it is stored in `sink`, the value of `x` will escape to the heap. Let's find out! 16 | 17 | ```bash 18 | docker run -it --rm go-interface-values \ 19 | go tool compile -l -m ./docs/04-missing-mallocs/examples/ex1/ex1.go 20 | ``` 21 | 22 | The output should look similar to this: 23 | 24 | ```bash 25 | ./docs/04-missing-mallocs/examples/ex1/ex1.go:25:2: x escapes to heap 26 | ``` 27 | 28 | Sure enough, `x` escapes to the heap! But does it result in a heap allocation, that is the question! To find out we can a benchmark that invokes the above `ex1` function and track the allocations/bytes per operation: 29 | 30 | ```bash 31 | docker run -it --rm go-interface-values \ 32 | go test -gcflags -l -bench . -benchmem -v ./docs/04-missing-mallocs/examples/ex1/ 33 | ``` 34 | 35 | The output will vary, but it is one line in particular from below that matters: 36 | 37 | ```bash 38 | goos: linux 39 | goarch: amd64 40 | pkg: go-interface-values/docs/04-missing-mallocs/examples/ex1 41 | cpu: Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz 42 | BenchmarkEscapeNoMalloc 43 | BenchmarkEscapeNoMalloc-8 663767829 1.846 ns/op 0 B/op 0 allocs/op 44 | PASS 45 | ok go-interface-values/docs/04-missing-mallocs/examples/ex1 1.421s 46 | ``` 47 | 48 | That's right, this one: 49 | 50 | ```bash 51 | BenchmarkEscapeNoMalloc-8 663767829 1.846 ns/op 0 B/op 0 allocs/op 52 | ``` 53 | 54 | There were _zero_ allocations and _zero_ bytes allocated on the heap. But how can that be? We have learned that: 55 | 56 | * storing a value in an interface results in a copy of that value 57 | * `x` escapes to the heap because its value outlives its stack frame by being assigned to the `sink` 58 | 59 | This _should_ mean a new `int64` is allocated on the heap to store the copy of `x`, right? To find out what is happening we need to look a little closer... 60 | 61 | --- 62 | 63 | Next: [Looking at the assembly](./02-why.md) 64 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ## -------------------------------------- 2 | ## Build diagrams from ASCII art (ditaa) 3 | ## -------------------------------------- 4 | FROM openjdk:11 as ditaa-builder 5 | 6 | RUN apt-get update -y && \ 7 | apt-get install -y leiningen 8 | RUN git clone https://github.com/akutz/ditaa.git /ditaa && \ 9 | git -C /ditaa checkout feature/set-font 10 | RUN cd /ditaa && lein uberjar 11 | 12 | 13 | ## -------------------------------------- 14 | ## Go interface values 15 | ## -------------------------------------- 16 | FROM golang:1.18beta2 17 | 18 | 19 | ## -------------------------------------- 20 | ## Authorship 21 | ## -------------------------------------- 22 | LABEL org.opencontainers.image.authors="sakutz@gmail.com" 23 | 24 | 25 | ## -------------------------------------- 26 | ## Apt and standard packages 27 | ## -------------------------------------- 28 | 29 | # Update the local apt cache. 30 | RUN apt-get update -y 31 | 32 | # Install vim. 33 | RUN apt-get install -y vim python3 34 | 35 | 36 | ## -------------------------------------- 37 | ## Golang 38 | ## -------------------------------------- 39 | 40 | # Install graphviz so "go tool pprof" can export images. 41 | RUN apt-get install -y graphviz 42 | 43 | # Install the Go debugger. 44 | RUN go install github.com/go-delve/delve/cmd/dlv@latest 45 | 46 | 47 | ## -------------------------------------- 48 | ## Java 49 | ## -------------------------------------- 50 | 51 | # Install the OpenJRE. 52 | RUN apt-get install -y openjdk-11-jre-headless 53 | 54 | # Configure the Java environment variables. 55 | ARG TARGETARCH 56 | ENV JAVA_HOME="/usr/lib/jvm/java-11-openjdk-${TARGETARCH}" 57 | 58 | 59 | ## -------------------------------------- 60 | ## Diagrams from ASCII art (ditaa) 61 | ## -------------------------------------- 62 | COPY --from=ditaa-builder \ 63 | /ditaa/target/ditaa-0.11.0-standalone.jar \ 64 | /ditaa.jar 65 | ENV DITAA="java -jar /ditaa.jar" 66 | ENV LANG="C.UTF-8" 67 | 68 | 69 | ## -------------------------------------- 70 | ## Main 71 | ## -------------------------------------- 72 | 73 | # Create the working directory. 74 | RUN mkdir -p /go-interface-values 75 | WORKDIR /go-interface-values/ 76 | 77 | # Copy the current repo into the working directory. 78 | COPY . /go-interface-values/ 79 | 80 | # Cache the project's Go modules in the Docker container. 81 | RUN go mod download 82 | -------------------------------------------------------------------------------- /docs/04-missing-mallocs/images/02-why-fig1.ascii: -------------------------------------------------------------------------------- 1 | registers data 2 | /-----------------\ /-----------------\ /---------------------------------------+ 3 | | AX | | CX | | staticuint64s c666 | 4 | | | | | | +-----------------------------------+ | 5 | | | | | | | 01 02 03 04 05 06 07 08 09 10 11..| | 6 | | | | | | +-----------------------------------+ | 7 | | | | | | sink | 8 | | | | | | +-----------------------------------+ | 9 | | | | | | | type꞉ | | 10 | | | | | | | valu꞉ | | 11 | | c000 | | c333 | | +-----------------------------------+ | 12 | \-----------------/ \-----------------/ +---------------------------------------/ 13 | 14 | bottom of stack frame +-> local variables 15 | high address +---------------------------------------+ | +---------------------------------------+ <-+ 16 | | arguments cFFF | | | name꞉ x | |07 17 | + +---------------------------------------+ | | type꞉ int64 | |06 18 | | | return address (PC) c9F9 | | | size꞉ 8 bytes | |05 19 | | +---------------------------------------+ | | | |04 20 | | | frame pointer c6C6 | | | | |03 21 | | +---------------------------------------+ | | | |02 22 | | | return values c696 | | | c336 | |01 23 | v +---------------------------------------+ | +---------------------------------------+ <-+00 0(SP) 24 | | local variables c363 | -+-> 25 | low address +---------------------------------------+ 26 | stack pointer (SP) 27 | top of stack frame 28 | -------------------------------------------------------------------------------- /docs/04-missing-mallocs/images/02-why-fig2.ascii: -------------------------------------------------------------------------------- 1 | registers data 2 | /-----------------\ /-----------------\ /---------------------------------------+ 3 | | AX | | CX | | staticuint64s c666 | 4 | | | | | | +-----------------------------------+ | 5 | | | | | | | 01 02 03 04 05 06 07 08 09 10 11..| | 6 | | | | | | +-----------------------------------+ | 7 | | | | | | sink | 8 | | | | | | +-----------------------------------+ | 9 | | | | | | | type꞉ | | 10 | | | | | | | valu꞉ | | 11 | | c000 | | c333 | | +-----------------------------------+ | 12 | \-----------------/ \-----------------/ +---------------------------------------/ 13 | 14 | bottom of stack frame +-> local variables 15 | high address +---------------------------------------+ | +---------------------------------------+ <-+ 16 | | arguments cFFF | | | name꞉ x | |07 17 | + +---------------------------------------+ | | type꞉ int64 | |06 18 | | | return address (PC) c9F9 | | | size꞉ 8 bytes | |05 19 | | +---------------------------------------+ | | valu꞉ 2 | |04 20 | | | frame pointer c6C6 | | | | |03 21 | | +---------------------------------------+ | | | |02 22 | | | return values c696 | | | c336 | |01 23 | v +---------------------------------------+ | +---------------------------------------+ <-+00 0(SP) 24 | | local variables c363 | -+-> 25 | low address +---------------------------------------+ 26 | stack pointer (SP) 27 | top of stack frame 28 | -------------------------------------------------------------------------------- /docs/04-missing-mallocs/images/02-why-fig3.ascii: -------------------------------------------------------------------------------- 1 | registers data 2 | /-----------------\ /-----------------\ /---------------------------------------+ 3 | | AX | | CX | | staticuint64s c666 | 4 | | | | | | +-----------------------------------+ | 5 | | 2 | | | | | 01 02 03 04 05 06 07 08 09 10 11..| | 6 | | | | | | +-----------------------------------+ | 7 | | | | | | sink | 8 | | | | | | +-----------------------------------+ | 9 | | | | | | | type꞉ | | 10 | | | | | | | valu꞉ | | 11 | | c000 | | c333 | | +-----------------------------------+ | 12 | \-----------------/ \-----------------/ +---------------------------------------/ 13 | 14 | bottom of stack frame +-> local variables 15 | high address +---------------------------------------+ | +---------------------------------------+ <-+ 16 | | arguments cFFF | | | name꞉ x | |07 17 | + +---------------------------------------+ | | type꞉ int64 | |06 18 | | | return address (PC) c9F9 | | | size꞉ 8 bytes | |05 19 | | +---------------------------------------+ | | valu꞉ 2 | |04 20 | | | frame pointer c6C6 | | | | |03 21 | | +---------------------------------------+ | | | |02 22 | | | return values c696 | | | c336 | |01 23 | v +---------------------------------------+ | +---------------------------------------+ <-+00 0(SP) 24 | | local variables c363 | -+-> 25 | low address +---------------------------------------+ 26 | stack pointer (SP) 27 | top of stack frame 28 | -------------------------------------------------------------------------------- /docs/04-missing-mallocs/images/02-why-fig4.ascii: -------------------------------------------------------------------------------- 1 | registers data 2 | /-----------------\ /-----------------\ /---------------------------------------+ 3 | | AX | | CX | | staticuint64s c666 | 4 | | | | | | +-----------------------------------+ | 5 | | 2 | | &staticuint64s | | | 01 02 03 04 05 06 07 08 09 10 11..| | 6 | | | | | | +-----------------------------------+ | 7 | | | | | | sink | 8 | | | | | | +-----------------------------------+ | 9 | | | | | | | type꞉ | | 10 | | | | | | | valu꞉ | | 11 | | c000 | | c333 | | +-----------------------------------+ | 12 | \-----------------/ \-----------------/ +---------------------------------------/ 13 | 14 | bottom of stack frame +-> local variables 15 | high address +---------------------------------------+ | +---------------------------------------+ <-+ 16 | | arguments cFFF | | | name꞉ x | |07 17 | + +---------------------------------------+ | | type꞉ int64 | |06 18 | | | return address (PC) c9F9 | | | size꞉ 8 bytes | |05 19 | | +---------------------------------------+ | | valu꞉ 2 | |04 20 | | | frame pointer c6C6 | | | | |03 21 | | +---------------------------------------+ | | | |02 22 | | | return values c696 | | | c336 | |01 23 | v +---------------------------------------+ | +---------------------------------------+ <-+00 0(SP) 24 | | local variables c363 | -+-> 25 | low address +---------------------------------------+ 26 | stack pointer (SP) 27 | top of stack frame 28 | -------------------------------------------------------------------------------- /docs/04-missing-mallocs/images/02-why-fig5.ascii: -------------------------------------------------------------------------------- 1 | registers data 2 | /-----------------\ /-----------------\ /---------------------------------------+ 3 | | AX | | CX | | staticuint64s c666 | 4 | | | | | | +-----------------------------------+ | 5 | | 2 | | &staticuint64s+ | | | 01 02 03 04 05 06 07 08 09 10 11..| | 6 | | | | 16 | | +-----------------------------------+ | 7 | | | | | | sink | 8 | | | | | | +-----------------------------------+ | 9 | | | | | | | type꞉ | | 10 | | | | | | | valu꞉ | | 11 | | c000 | | c333 | | +-----------------------------------+ | 12 | \-----------------/ \-----------------/ +---------------------------------------/ 13 | 14 | bottom of stack frame +-> local variables 15 | high address +---------------------------------------+ | +---------------------------------------+ <-+ 16 | | arguments cFFF | | | name꞉ x | |07 17 | + +---------------------------------------+ | | type꞉ int64 | |06 18 | | | return address (PC) c9F9 | | | size꞉ 8 bytes | |05 19 | | +---------------------------------------+ | | valu꞉ 2 | |04 20 | | | frame pointer c6C6 | | | | |03 21 | | +---------------------------------------+ | | | |02 22 | | | return values c696 | | | c336 | |01 23 | v +---------------------------------------+ | +---------------------------------------+ <-+00 0(SP) 24 | | local variables c363 | -+-> 25 | low address +---------------------------------------+ 26 | stack pointer (SP) 27 | top of stack frame 28 | -------------------------------------------------------------------------------- /docs/04-missing-mallocs/images/02-why-fig6.ascii: -------------------------------------------------------------------------------- 1 | registers data 2 | /-----------------\ /-----------------\ /---------------------------------------+ 3 | | AX | | CX | | staticuint64s c666 | 4 | | | | | | +-----------------------------------+ | 5 | | &staticuint64s+ | | &staticuint64s+ | | | 01 02 03 04 05 06 07 08 09 10 11..| | 6 | | 16 | | 16 | | +-----------------------------------+ | 7 | | | | | | sink | 8 | | | | | | +-----------------------------------+ | 9 | | | | | | | type꞉ | | 10 | | | | | | | valu꞉ | | 11 | | c000 | | c333 | | +-----------------------------------+ | 12 | \-----------------/ \-----------------/ +---------------------------------------/ 13 | 14 | bottom of stack frame +-> local variables 15 | high address +---------------------------------------+ | +---------------------------------------+ <-+ 16 | | arguments cFFF | | | name꞉ x | |07 17 | + +---------------------------------------+ | | type꞉ int64 | |06 18 | | | return address (PC) c9F9 | | | size꞉ 8 bytes | |05 19 | | +---------------------------------------+ | | valu꞉ 2 | |04 20 | | | frame pointer c6C6 | | | | |03 21 | | +---------------------------------------+ | | | |02 22 | | | return values c696 | | | c336 | |01 23 | v +---------------------------------------+ | +---------------------------------------+ <-+00 0(SP) 24 | | local variables c363 | -+-> 25 | low address +---------------------------------------+ 26 | stack pointer (SP) 27 | top of stack frame 28 | -------------------------------------------------------------------------------- /tests/mem/mem_test.go: -------------------------------------------------------------------------------- 1 | //go:generate python3 ../../hack/gen.py 2 | 3 | /* 4 | Copyright 2022 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | // Package mem_test is used to benchmark the possible mallocs that can occur 20 | // when storing interface values. 21 | package mem_test 22 | 23 | import ( 24 | "math/rand" 25 | "strconv" 26 | ) 27 | 28 | // nonZeroRandInt returns an integer between zero and the max size for the 29 | // integer at the specified size/width (ex. 8, 16, 32, 64). 30 | // 31 | // Please note this function is also used with the other number types and 32 | // the result cast into the appropriate type. 33 | // 34 | // For unsigned integers this is because the math/rand package does not 35 | // implement UintN. Using Uint32 or Uint64 to try and get random values for 36 | // uint8 and uint16 can cause stack overflows before a value that fits into the 37 | // width of those types is found. 38 | // 39 | // For floating point numbers this is just convienent as math/rand *does* 40 | // provide Float32() and Float64() to generate pseudo-random numbers for those 41 | // types. 42 | // 43 | // For complex numbers this is because the math/rand package does not offer a 44 | // random generation function for complex numbers. 45 | func nonZeroRandInt(n int) int { 46 | if i := rand.Intn(1<<(n-1) - 1); i > 0 { 47 | return i 48 | } 49 | return nonZeroRandInt(n) 50 | } 51 | 52 | // nonConstBoolTrue returns a boolean true value that is not a constant. 53 | func nonConstBoolTrue() bool { 54 | b, _ := strconv.ParseBool(strconv.Itoa(nonZeroRandInt(8))) 55 | return b 56 | } 57 | 58 | // nonZeroString returns a string of random characters at the specified length. 59 | func nonZeroString(n int) string { 60 | b := make([]rune, n) 61 | for i := range b { 62 | b[i] = _letters[rand.Intn(len(_letters))] 63 | } 64 | return string(b) 65 | } 66 | 67 | const _int_size = 32 << (^uint(0) >> 63) 68 | 69 | var ( 70 | _i interface{} 71 | _letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") 72 | ) 73 | -------------------------------------------------------------------------------- /docs/02-interface-values/06-underlying-value.md: -------------------------------------------------------------------------------- 1 | # Underlying value 2 | 3 | Last time on _Go interface values_: 4 | 5 | * Go interfaces are really two `uintptr` values that address the underlying type and value stored in the interface 6 | * The address of the type points to an internal Go type where an equivalent to a `reflect.Kind` value is stored 7 | * Type information is global and shared by all values stored in interfaces 8 | * The address of the value may sometimes be shared even across distinct types such as `int32` and `int64`? 9 | 10 | Okay, now that we are all caught up, let's take a quick look at the example from the previous page ([Golang playground](https://go.dev/play/p/ewZtZafue19)): 11 | 12 | ```go 13 | package main 14 | 15 | func main() { 16 | println(interface{}(int32(3))) 17 | println(interface{}(int32(5))) 18 | println(interface{}(int64(3))) 19 | println(interface{}(int64(5))) 20 | } 21 | ``` 22 | 23 | We learned the value addresses for `int32(3)` and `int64(3)` are the same as well as `int32(5)` and `int64(5)`: 24 | 25 | * **`0x476598`**: value address for `int32(3)` and `int64(3)` 26 | * **`0x4765a0`**: value address for `int32(5)` and `int64(5)` 27 | 28 | The reason for this is because we stored _constant_ values in interfaces, and Go's compiler is smart enough to reference the same value multiple times when it is small enough to fit into the width of a larger type (as both `3` and `5` fit into both an `int32` and `int64`). If we were to refactor things a bit so `3` and `5` are no longer constant ([Golang playground](https://go.dev/play/p/qBBwCOhp4II)): 29 | 30 | ```go 31 | package main 32 | 33 | func main() { 34 | a, b := int32(3), int32(5) 35 | x, y := int64(3), int64(5) 36 | println(interface{}(a)) 37 | println(interface{}(b)) 38 | println(interface{}(x)) 39 | println(interface{}(y)) 40 | } 41 | ``` 42 | 43 | We see all the value addresses are distinct from one another: 44 | 45 | ```bash 46 | (0x459d80,0xc00003475c) 47 | (0x459d80,0xc000034758) 48 | (0x459dc0,0xc000034760) 49 | (0x459dc0,0xc000034768) 50 | ``` 51 | 52 | There's _one more thing_. Consider the following example ([Golang playground](https://go.dev/play/p/pAegxrruNvR)): 53 | 54 | ```go 55 | package main 56 | 57 | func main() { 58 | a, b := byte(3), byte(5) 59 | x, y := uint8(3), uint8(5) 60 | println(interface{}(a)) 61 | println(interface{}(b)) 62 | println(interface{}(x)) 63 | println(interface{}(y)) 64 | } 65 | ``` 66 | 67 | What do you think will happen? Join me on the next page and we will find out together! 68 | 69 | --- 70 | 71 | Next: [Special values](./07-special-values.md) 72 | -------------------------------------------------------------------------------- /docs/02-interface-values/04-a-pair-of-pointers.md: -------------------------------------------------------------------------------- 1 | # A pair of pointers 2 | 3 | Through type assertion we have ascertained that storing a value in interface preserves the value's type. That is because under the hood an `interface{}` is really a pair of `uintptr` values: 4 | 5 | * an address to the type stored in the interface 6 | * an address to the value stored in the interface 7 | 8 | --- 9 | 10 | :wave: **The last word** 11 | 12 | There used to be an optimization where a value stored in an interface was stored directly in the last/second word as long as the size of that value's type was smaller than a `uintptr`. However, this optimization was removed in [github.com/golang/go#8405](https://golang.org/issue/8405) due to the introduction of concurrent garbage collection. 13 | 14 | --- 15 | 16 | There are a couple of ways to access this information: 17 | 18 | * The built-in [`println`](https://github.com/golang/go/blob/d588f487703e773ba4a2f0a04f2d4141610bff6b/src/builtin/builtin.go#L261-L266) function 19 | * An [`unsafe.Pointer`](https://pkg.go.dev/unsafe#Pointer) and a `[2]uintptr` 20 | * An [`unsafe.Pointer`](https://pkg.go.dev/unsafe#Pointer) and a `struct{ ptyp, pval uintptr }` 21 | 22 | Here is an example that uses all three methods ([Golang playground](https://go.dev/play/p/JRU-xZDNvBf)): 23 | 24 | ```go 25 | package main 26 | 27 | import ( 28 | "fmt" 29 | "unsafe" 30 | ) 31 | 32 | func main() { 33 | // Store an int64 as an interface value. 34 | iface := interface{}(int64(2)) 35 | 36 | // Get an unsafe pointer for "iface". 37 | ptrIface := unsafe.Pointer(&iface) 38 | 39 | // Cast the pointer to a *[2]uintptr. 40 | ptrList := (*[2]uintptr)(ptrIface) 41 | 42 | // Cast the pointer to a *struct{uintptr, uintptr} 43 | ptrData := (*struct{ ptyp, pval uintptr })(ptrIface) 44 | 45 | // Print the addresses using: 46 | // * the builtin println function 47 | // * the array of two uintptrs 48 | // * the struct with the uintptrs 49 | println(iface) 50 | fmt.Printf("(0x%x,0x%x)\n", ptrList[0], ptrList[1]) 51 | fmt.Printf("(0x%x,0x%x)\n", ptrData.ptyp, ptrData.pval) 52 | } 53 | ``` 54 | 55 | The above program produces output that is similar, but not identical, to: 56 | 57 | ```bash 58 | (0x486920,0x4b1f88) 59 | (0x486920,0x4b1f88) 60 | (0x486920,0x4b1f88) 61 | ``` 62 | 63 | Based on the knowledge of what an interface actually is, we can infer: 64 | 65 | * `0x486920` is the address to the type stored in the interface 66 | * `0x4b1f88` is the address to the value stored in the interface 67 | 68 | Keep reading to find out how to use the above addresses to access the underlying type and value. 69 | 70 | --- 71 | 72 | Next: [Underlying type](./05-underlying-type.md) 73 | -------------------------------------------------------------------------------- /docs/05-lessons-learned/README.md: -------------------------------------------------------------------------------- 1 | # Lessons learned 2 | 3 | Wow, what a ride! What I meant to be a quick detour to help me better understand what performance improvements generics might bring compared to storing values in interfaces turned into quite a deep-dive. What did we learn along the way? 4 | 5 | * While storing a value in an interface is conceptually similar to "boxing," this term has a specific meaning for object oriented languages and may confuse people. 6 | * A Go interface is really just a pair of `uintptr` values that point to the interface's underlying type and value. 7 | * Escape analysis occurs before optimizations for storing values in an interface. 8 | * Just because a value is marked as "escapes to heap" does not mean memory is allocated on the heap. 9 | 10 | And remember, if you ever want to see if an assignment allocates memory on the heap, all you have to do is take a quick trip to the [Go playground](https://go.dev/play/p/OoN-qxRHqsi): 11 | 12 | ```go 13 | package main 14 | 15 | import ( 16 | "flag" 17 | "testing" 18 | ) 19 | 20 | func TestStoringInterfaceValues(t *testing.T) { 21 | flag.Lookup("test.benchmem").Value.Set("true") 22 | flag.Lookup("test.benchtime").Value.Set("100x") 23 | 24 | testCases := []struct { 25 | name string 26 | fvnc func(*testing.B) 27 | expAlloc int64 28 | expBytes int64 29 | }{ 30 | { 31 | name: "0 malloc, 0 bytes", 32 | fvnc: func(b *testing.B) { 33 | var sink interface{} 34 | b.ResetTimer() 35 | for i := 0; i < b.N; i++ { 36 | var x int32 37 | x = 255 38 | sink = x 39 | } 40 | _ = sink 41 | }, 42 | }, 43 | { 44 | name: "1 malloc, 4 bytes", 45 | fvnc: func(b *testing.B) { 46 | var sink interface{} 47 | b.ResetTimer() 48 | for i := 0; i < b.N; i++ { 49 | var x int32 50 | x = 256 51 | sink = x 52 | } 53 | _ = sink 54 | }, 55 | expAlloc: 1, 56 | expBytes: 4, 57 | }, 58 | } 59 | 60 | for i := 0; i < len(testCases); i++ { 61 | tc := testCases[i] 62 | t.Run(tc.name, func(t *testing.T) { 63 | r := testing.Benchmark(tc.fvnc) 64 | if ea, aa := tc.expAlloc, r.AllocsPerOp(); ea != aa { 65 | t.Errorf("expAlloc=%d, actAlloc=%d", ea, aa) 66 | } 67 | if eb, ab := tc.expBytes, r.AllocedBytesPerOp(); eb != ab { 68 | t.Errorf("expBytes=%d, actBytes=%d", eb, ab) 69 | } 70 | }) 71 | } 72 | } 73 | ``` 74 | 75 | ```bash 76 | === RUN TestStoringInterfaceValues 77 | === RUN TestStoringInterfaceValues/0_malloc,_0_bytes 78 | === RUN TestStoringInterfaceValues/1_malloc,_4_bytes 79 | --- PASS: TestStoringInterfaceValues (0.00s) 80 | --- PASS: TestStoringInterfaceValues/0_malloc,_0_bytes (0.00s) 81 | --- PASS: TestStoringInterfaceValues/1_malloc,_4_bytes (0.00s) 82 | PASS 83 | ``` 84 | 85 | --- 86 | 87 | _Thanks for reading!_ 88 | -------------------------------------------------------------------------------- /docs/03-escape-analysis/05-tests.md: -------------------------------------------------------------------------------- 1 | # Tests 2 | 3 | This section on escape analysis has been rather assertive in its claims about criteria and behaviors. Truthfully this entire repository is the embodiment of "one physical test is worth one thousand expert opinions." I am no expert on, well, much of anything, but I do like digging into problems, even those I do not fully understand. Storing interface values, heap allocation, and escape analysis are such problems. The sources are there in Go, and I can follow _just_ enough to be annoying on Gopher Slack :smiley: 4 | 5 | To that end this repository has several tests to verify the assertions regarding escape analysis. The tests are located in [`./tests/lem`](../../tests/lem) and may be executed with the following command: 6 | 7 | ```bash 8 | docker run -it --rm go-interface-values go test -v ./tests/lem 9 | ``` 10 | 11 | The tests use a bespoke framework called _leak, escape, move_, or "lem" for short. Lem enables the use of code comments decorating and within functions to assert the expected number of allocations, bytes allocated, and where the compiler's optimiation output should indicate a value has leaked, escaped, or moved to the heap. For example: 12 | 13 | ```go 14 | // lem.escape2.name=heap cannot point to stack 15 | // lem.escape2.alloc=1 16 | // lem.escape2.bytes=4 17 | func escape2(b *testing.B) { 18 | var sink *int32 19 | f := func(p *int32) *int32 { // lem.escape1.m=leaking param: p to result ~r[0-1] level=0 20 | return p 21 | } 22 | b.ResetTimer() 23 | for i := 0; i < b.N; i++ { 24 | sink = f(new(int32)) // lem.escape1.m=new\(int32\) escapes to heap 25 | } 26 | _ = sink 27 | } 28 | ``` 29 | 30 | It is even possible to write tests that assert when optimizations should _not_ occur: 31 | 32 | ```go 33 | // lem.leak7.name=did not escape bc return value did not outlive stack frame 34 | // lem.leak7.alloc=0 35 | // lem.leak7.bytes=0 36 | func leak7(b *testing.B) { 37 | f := func(p *int32) *int32 { // lem.leak7.m=leaking param: p to result ~r[0-1] level=0 38 | return p 39 | } 40 | b.ResetTimer() 41 | for i := 0; i < b.N; i++ { 42 | var x int32 = 4096 // lem.leak7.m!=(escapes|leaking|moved) 43 | var p *int32 = &x // lem.leak7.m!=(escapes|leaking|moved) 44 | var sink *int32 // lem.leak7.m!=(escapes|leaking|moved) 45 | sink = f(p) // lem.leak7.m!=(escapes|leaking|moved) 46 | _ = sink 47 | } 48 | } 49 | ``` 50 | 51 | For more information on the lem test framework please refer [github.com/akutz/lem](https://github.com/akutz/lem). Go ahead, I'll wait. No, seriously, I'll be right here. 52 | 53 | ... 54 | 55 | You're back? Great! So now that we all understand the basics of escape analysis it is time to revisit why storing values in interfaces does not always result in memory allocated on the heap. 56 | 57 | --- 58 | 59 | Next: [Missing mallocs](../04-missing-mallocs/) 60 | -------------------------------------------------------------------------------- /docs/04-missing-mallocs/images/02-why-fig10.ascii: -------------------------------------------------------------------------------- 1 | registers data 2 | /-----------------\ /-----------------\ /---------------------------------------+ 3 | | AX | | CX | | staticuint64s c666 | 4 | | | | | | +-----------------------------------+ | 5 | | &staticuint64s+ | | &type.int64 | | | 01 02 03 04 05 06 07 08 09 10 11..| | 6 | | 16 | | | | +-----------------------------------+ | 7 | | | | | | sink | 8 | | | | | | +-----------------------------------+ | 9 | | | | | | | type꞉ &type.int64 | | 10 | | | | | | | valu꞉ &staticuint64s+16 | | 11 | | c000 | | c333 | | +-----------------------------------+ | 12 | \-----------------/ \-----------------/ +---------------------------------------/ 13 | 14 | +-> local variables 15 | | +---------------------------------------+ <-+16 0x10(SP) 16 | bottom of stack frame | | type꞉ uintptr | |15 17 | high address +---------------------------------------+ | | size꞉ 8 bytes | |14 18 | | arguments cFFF | | | valu꞉ &staticuint64s+16 | |13 19 | + +---------------------------------------+ | | | |12 20 | | | return address (PC) c9F9 | | | | |11 21 | | +---------------------------------------+ | | | |10 22 | | | frame pointer c6C6 | | | c669 | |09 23 | | +---------------------------------------+ | +---------------------------------------+ <-+08 0x8(SP) 24 | | | return values c696 | | | name꞉ x | |07 25 | v +---------------------------------------+ | | type꞉ int64 | |06 26 | | local variables c363 | -+ | size꞉ 8 bytes | |05 27 | low address +---------------------------------------+ | | valu꞉ 0 | |04 28 | stack pointer (SP) | | | |03 29 | top of stack frame | | | |02 30 | | | c336 | |01 31 | | +---------------------------------------+ <-+00 0(SP) 32 | +-> -------------------------------------------------------------------------------- /docs/04-missing-mallocs/images/02-why-fig7.ascii: -------------------------------------------------------------------------------- 1 | registers data 2 | /-----------------\ /-----------------\ /---------------------------------------+ 3 | | AX | | CX | | staticuint64s c666 | 4 | | | | | | +-----------------------------------+ | 5 | | &staticuint64s+ | | &staticuint64s+ | | | 01 02 03 04 05 06 07 08 09 10 11..| | 6 | | 16 | | 16 | | +-----------------------------------+ | 7 | | | | | | sink | 8 | | | | | | +-----------------------------------+ | 9 | | | | | | | type꞉ | | 10 | | | | | | | valu꞉ | | 11 | | c000 | | c333 | | +-----------------------------------+ | 12 | \-----------------/ \-----------------/ +---------------------------------------/ 13 | 14 | +-> local variables 15 | | +---------------------------------------+ <-+16 0x10(SP) 16 | bottom of stack frame | | type꞉ uintptr | |15 17 | high address +---------------------------------------+ | | size꞉ 8 bytes | |14 18 | | arguments cFFF | | | valu꞉ &staticuint64s+16 | |13 19 | + +---------------------------------------+ | | | |12 20 | | | return address (PC) c9F9 | | | | |11 21 | | +---------------------------------------+ | | | |10 22 | | | frame pointer c6C6 | | | c669 | |09 23 | | +---------------------------------------+ | +---------------------------------------+ <-+08 0x8(SP) 24 | | | return values c696 | | | name꞉ x | |07 25 | v +---------------------------------------+ | | type꞉ int64 | |06 26 | | local variables c363 | -+ | size꞉ 8 bytes | |05 27 | low address +---------------------------------------+ | | valu꞉ 0 | |04 28 | stack pointer (SP) | | | |03 29 | top of stack frame | | | |02 30 | | | c336 | |01 31 | | +---------------------------------------+ <-+00 0(SP) 32 | +-> -------------------------------------------------------------------------------- /docs/04-missing-mallocs/images/02-why-fig8.ascii: -------------------------------------------------------------------------------- 1 | registers data 2 | /-----------------\ /-----------------\ /---------------------------------------+ 3 | | AX | | CX | | staticuint64s c666 | 4 | | | | | | +-----------------------------------+ | 5 | | &staticuint64s+ | | &type.int64 | | | 01 02 03 04 05 06 07 08 09 10 11..| | 6 | | 16 | | | | +-----------------------------------+ | 7 | | | | | | sink | 8 | | | | | | +-----------------------------------+ | 9 | | | | | | | type꞉ | | 10 | | | | | | | valu꞉ | | 11 | | c000 | | c333 | | +-----------------------------------+ | 12 | \-----------------/ \-----------------/ +---------------------------------------/ 13 | 14 | +-> local variables 15 | | +---------------------------------------+ <-+16 0x10(SP) 16 | bottom of stack frame | | type꞉ uintptr | |15 17 | high address +---------------------------------------+ | | size꞉ 8 bytes | |14 18 | | arguments cFFF | | | valu꞉ &staticuint64s+16 | |13 19 | + +---------------------------------------+ | | | |12 20 | | | return address (PC) c9F9 | | | | |11 21 | | +---------------------------------------+ | | | |10 22 | | | frame pointer c6C6 | | | c669 | |09 23 | | +---------------------------------------+ | +---------------------------------------+ <-+08 0x8(SP) 24 | | | return values c696 | | | name꞉ x | |07 25 | v +---------------------------------------+ | | type꞉ int64 | |06 26 | | local variables c363 | -+ | size꞉ 8 bytes | |05 27 | low address +---------------------------------------+ | | valu꞉ 0 | |04 28 | stack pointer (SP) | | | |03 29 | top of stack frame | | | |02 30 | | | c336 | |01 31 | | +---------------------------------------+ <-+00 0(SP) 32 | +-> -------------------------------------------------------------------------------- /docs/04-missing-mallocs/images/02-why-fig9.ascii: -------------------------------------------------------------------------------- 1 | registers data 2 | /-----------------\ /-----------------\ /---------------------------------------+ 3 | | AX | | CX | | staticuint64s c666 | 4 | | | | | | +-----------------------------------+ | 5 | | &staticuint64s+ | | &type.int64 | | | 01 02 03 04 05 06 07 08 09 10 11..| | 6 | | 16 | | | | +-----------------------------------+ | 7 | | | | | | sink | 8 | | | | | | +-----------------------------------+ | 9 | | | | | | | type꞉ &type.int64 | | 10 | | | | | | | valu꞉ | | 11 | | c000 | | c333 | | +-----------------------------------+ | 12 | \-----------------/ \-----------------/ +---------------------------------------/ 13 | 14 | +-> local variables 15 | | +---------------------------------------+ <-+16 0x10(SP) 16 | bottom of stack frame | | type꞉ uintptr | |15 17 | high address +---------------------------------------+ | | size꞉ 8 bytes | |14 18 | | arguments cFFF | | | valu꞉ &staticuint64s+16 | |13 19 | + +---------------------------------------+ | | | |12 20 | | | return address (PC) c9F9 | | | | |11 21 | | +---------------------------------------+ | | | |10 22 | | | frame pointer c6C6 | | | c669 | |09 23 | | +---------------------------------------+ | +---------------------------------------+ <-+08 0x8(SP) 24 | | | return values c696 | | | name꞉ x | |07 25 | v +---------------------------------------+ | | type꞉ int64 | |06 26 | | local variables c363 | -+ | size꞉ 8 bytes | |05 27 | low address +---------------------------------------+ | | valu꞉ 0 | |04 28 | stack pointer (SP) | | | |03 29 | top of stack frame | | | |02 30 | | | c336 | |01 31 | | +---------------------------------------+ <-+00 0(SP) 32 | +-> -------------------------------------------------------------------------------- /docs/02-interface-values/07-special-values.md: -------------------------------------------------------------------------------- 1 | # Special values 2 | 3 | Based on everything we have learned so far... 4 | 5 | * Type addresses are shared 6 | * Value addresses are shared for constants and distinct for variables 7 | 8 | ...we _should_ be able to assume the following about the example below ([Golang playground](https://go.dev/play/p/pAegxrruNvR)): 9 | 10 | * The type address for `a` and `b` should be the same 11 | * The value addresses for `a`, `b`, `x`, and `y` should all be distinct 12 | 13 | ```go 14 | package main 15 | 16 | func main() { 17 | a, b := byte(3), byte(5) 18 | x, y := uint8(3), uint8(5) 19 | println(interface{}(a)) 20 | println(interface{}(b)) 21 | println(interface{}(x)) 22 | println(interface{}(y)) 23 | } 24 | ``` 25 | 26 | Except you have probably realized by now that I would not be posing the question if the answer were so simple :smiley: Here's the output: 27 | 28 | ```bash 29 | (0x45a240,0x4b89b8) 30 | (0x45a240,0x4b89c8) 31 | (0x45a240,0x4b89b8) 32 | (0x45a240,0x4b89c8) 33 | ``` 34 | 35 | Whoah! Not only is the address to the type the same for `byte` and `uint8`, but the addresses for the values are the same across the types as well! Before I answer the question, I have a magic trick I would like to show you ([Golang playground](https://go.dev/play/p/I-ijkvTlrds)): 36 | 37 | ```go 38 | package main 39 | 40 | import ( 41 | "fmt" 42 | "unsafe" 43 | ) 44 | 45 | func main() { 46 | x := byte(3) 47 | 48 | // Store x as an interface value. 49 | iface := interface{}(x) 50 | 51 | // Get an unsafe pointer for "iface". 52 | ptrIface := unsafe.Pointer(&iface) 53 | 54 | // Cast the pointer to a *[2]uintptr. 55 | ptrList := (*[2]uintptr)(ptrIface) 56 | 57 | // Get an unsafe.Pointer for the value address offset 58 | // by the value 197*8. 59 | ptrY := unsafe.Pointer(ptrList[1] + (197 * 8)) 60 | 61 | // Cast ptrY to a uint8. 62 | y := (*uint8)(ptrY) 63 | 64 | // What is going to be printed? 65 | fmt.Println(*y) 66 | } 67 | ``` 68 | 69 | So what do you think will be printed? If you guessed `200`, then congratulations, you successfully peeked behind the curtain! It turns out that Go defines a special, internal array with exactly 256 elements called [`staticuint64s`](https://github.com/golang/go/blob/2580d0e08d5e9f979b943758d3c49877fb2324cb/src/runtime/iface.go#L492-L526). This array holds every possible value (0-255) for an integer that is only one byte wide, such as: 70 | 71 | * `bool` 72 | * `byte` 73 | * `int8` 74 | * `uint8` 75 | 76 | Thus when it is necessary to create a pointer to one of those values, the Go compiler just addresses an element from the aforementioned array. This is also why the above trick was possible: 77 | 78 | * By adding `197 * 8` to the address that pointed to the element in the array for `3` 79 | * we addressed the value of `200` from the array 80 | * because each element is eight bytes wide, thus `197 * 8` is the offset of `200` from `3` 81 | 82 | Just about now you are probably wondering: 83 | 84 | * _If the array is typed `uint64`, does the above work for all integer values?_ 85 | * _Why was it necessary to reference the array for the value at all when `x` already has the value?_ 86 | 87 | The answer to the first question is _no_, to which you will probably ask _Then why is the array `uint64`?_ Great question! We will come back to that later. For now let's keep reading to understand why the value address did not just point to `x`... 88 | 89 | --- 90 | 91 | Next: [Copy on store](./08-copy-on-store.md) 92 | -------------------------------------------------------------------------------- /docs/03-escape-analysis/04-move.md: -------------------------------------------------------------------------------- 1 | # Move 2 | 3 | We have discussed when parameters are leaked and when variables escape to the heap, but what about when the heap is so lovely a variable decides to move there? This page discusses when and why escape analysis moves some variables to the heap. 4 | 5 | * [**Criteria**](#criteria): what are the criteria for moving to the heap? 6 | * [**Moving to the heap**](#moving-to-the-heap): when a value on the stack is moved to the heap 7 | * [**Storing value types in interfaces**](#storing-value-types-in-interfaces): when a value type escapes to the heap after all 8 | 9 | ## Criteria 10 | 11 | There are two requirements to be eligible for moving to the heap: 12 | 13 | * The variable must be a value type 14 | * A reference to the variable is assigned to a location outside of the local stack frame 15 | 16 | What does this look like? It is actually semantically the same as when a variable escapes to the heap -- it all just depends on the initial declaration of that varaible's type... 17 | 18 | ## Moving to the heap 19 | 20 | A value type variable is moved to the heap if a reference to it is assigned to a location outside of the variable's local stack frame. For example: 21 | 22 | ```go 23 | package main 24 | 25 | var sink *int32 26 | 27 | //go:noinline 28 | func main() { 29 | var id int32 = 4096 30 | sink = &id 31 | } 32 | ``` 33 | 34 | Use the following command to print the optimizations for the above program: 35 | 36 | ```bash 37 | docker run -it --rm go-interface-values \ 38 | go tool compile -m ./docs/03-escape-analysis/examples/ex5/main.go 39 | ``` 40 | 41 | The output should resemble: 42 | 43 | ```bash 44 | ./docs/03-escape-analysis/examples/ex5/main.go:23:6: moved to heap: id 45 | ``` 46 | 47 | Unlike when `id := new(int32)` escapes to the heap, the `id` in the above example is moved to the heap because it was initially a value type. Thus, if all other conditions are met for escaping and moving to the heap: 48 | 49 | * a value _**escapes**_ if it is initially a reference type 50 | * a value _**moves**_ if it is initially a value type 51 | 52 | Actually, there is _one_ exception to this rule... 53 | 54 | 55 | ## Storing value types in interfaces 56 | 57 | It would be too simple if reference types escaped to the heap and value types moved, wouldn't it? One way a value type is marked as _escaping_ to the heap is when the value is stored in an interface. For example: 58 | 59 | ```go 60 | package main 61 | 62 | var sink interface{} 63 | 64 | //go:noinline 65 | func main() { 66 | var id int32 = 4096 67 | sink = id 68 | } 69 | ``` 70 | 71 | Use the following command to print the optimizations for the above program: 72 | 73 | ```bash 74 | docker run -it --rm go-interface-values \ 75 | go tool compile -m ./docs/03-escape-analysis/examples/ex6/main.go 76 | ``` 77 | 78 | The output should resemble: 79 | 80 | ```bash 81 | ./docs/03-escape-analysis/examples/ex6/main.go:24:7: id escapes to heap 82 | ``` 83 | 84 | So let's revise our above rules -- if all other conditions are met for escaping and moving to the heap: 85 | 86 | * a value _**escapes**_ if it is initially a reference type or the operation **is** storing a value type in an interface 87 | * a value _**moves**_ if it is initially a value type and the operation is **not** storing a value type in an interface 88 | 89 | Okay, so this section has made a _lot_ of assertion using poor analogies about leaky sinks, but do all of these claims...hold water? :smiley: 90 | 91 | --- 92 | 93 | Next: [Tests](./05-tests.md) 94 | -------------------------------------------------------------------------------- /docs/01-prereqs/02-docker.md: -------------------------------------------------------------------------------- 1 | # Docker 2 | 3 | Docker is the preferred approach to running the examples from this repository. 4 | 5 | * [**Build the image**](#build-the-image): build the Docker image locally 6 | * [**Use the image**](#use-the-image): use the Docker image to run the examples in this repository 7 | 8 | ## Build the image 9 | 10 | Build the Docker image by executing the following commands: 11 | 12 | 1. Clone this repository locally: 13 | 14 | ```bash 15 | git clone https://github.com/akutz/go-interface-values 16 | ``` 17 | 18 | 1. Change directories to the root of the cloned repository: 19 | 20 | ``` 21 | cd go-interface-values 22 | ``` 23 | 24 | 1. Build the image: 25 | 26 | ```bash 27 | docker build -t go-interface-values . 28 | ``` 29 | 30 | And voilá, you have the image! 31 | 32 | --- 33 | 34 | :star: **Please note** the image is also available remotely and can obtained by executing the following commands: 35 | 36 | ```bash 37 | docker pull akutz/go-interface-values && \ 38 | docker tag akutz/go-interface-values go-interface-values 39 | ``` 40 | 41 | --- 42 | 43 | ## Use the image 44 | 45 | The image may be used one of three ways: 46 | 47 | * [**Self-contained**](#self-contained): run the examples from the image's filesystem 48 | * [**Mount repository**](#mount-repository): run the examples from your local filesystem using the image 49 | * [**VS Code dev container**](#dev-container): run the examples in VS Code using the `Remote - Containers` extension 50 | 51 | ### Self-contained 52 | 53 | This method does not require cloning this repository locally since the Docker image includes the contents of this repository. For example, the following command will run the boxing benchmark using the examples sources from inside the image: 54 | 55 | ```bash 56 | docker run -it --rm go-interface-values go test -v ./examples/ 57 | ``` 58 | 59 | ### Mount repository 60 | 61 | Alternatively, it is also possible to run the examples using the Docker image and this repository cloned to your local filesystem. This has two benefits: 62 | 63 | * The examples will be up-to-date 64 | * It is easier to capture file output from the examples such as profiles 65 | 66 | For example, the following command will: 67 | 68 | * Run the benchmarks 69 | * Produce a memory profile 70 | * Produce an SVG image from the memory profile 71 | 72 | ```bash 73 | docker run -it --rm -v "$(pwd):/go-interface-values" \ 74 | go-interface-values bash -c ' \ 75 | go test -bench . -run Bench -benchmem -benchtime 100x -memprofile mem.profile && \ 76 | go tool pprof -svg mem.profile' 77 | ``` 78 | 79 | At the end there will be a file in the current, local directory named `profile001.svg` that visualizes the memory profile produced from the benchmark. 80 | 81 | ### Dev container 82 | 83 | Alternatively, it is also possible to run the examples through opening the cloned reposity in a VS Code devcontainer: 84 | 85 | 1. To get started, please install the extension `Remote - Containers` by Microsoft (ms-vscode-remote.remote-containers). 86 | 1. Open up your Command Palette and type or select the command `Remote-Containers: Reopen in Container`. 87 | 88 | The extension should be able to pick up the settings described in the `.devcontainer/devcontainer.json` and build the Dockerfile 89 | 1. Open up your terminal and you should be inside a container and at the project root. Verify this with: 90 | 91 | ```bash 92 | go version 93 | ``` 94 | 95 | If you see `go version go1.18beta2 linux/arm64` then you should be good to go. 96 | 97 | --- 98 | 99 | Next: [Interface values](../02-interface-values/) 100 | -------------------------------------------------------------------------------- /docs/02-interface-values/images/09-on-the-stack-fig2.ascii: -------------------------------------------------------------------------------- 1 | +-> local variables 2 | | +---------------------------------------+ <-+ 3 | | | name꞉ y | |31 4 | | | type꞉ interface{} | |30 5 | | | size꞉ 16 bytes | |29 6 | | | | |28 7 | | | | |27 8 | bottom of stack frame | | | |26 9 | high address +---------------------------------------+ | | | |25 10 | | arguments cFFF | | | | <-+24 0x18(SP) 11 | | +---------------------------------------+ | | | |23 12 | | | return address (PC) c9F9 | | | | |22 13 | | +---------------------------------------+ | | | |21 14 | | | frame pointer c6C6 | | | | |20 15 | | +---------------------------------------+ | | | |19 16 | | | return values c696 | | | | |18 17 | v +---------------------------------------+ | | c669 | |17 18 | | local variables c363 | -+ +---------------------------------------+ <-+16 0x10(SP) 19 | low address +---------------------------------------+ | : : |15 20 | stack pointer (SP) | : : |14 21 | top of stack frame | : : |13 22 | | : : |12 23 | | : : |11 24 | | : : |10 25 | | : : |09 26 | | +---------------------------------------+ <-+08 0x8(SP) 27 | | | name꞉ x | |07 28 | | | type꞉ int64 | |06 29 | | | size꞉ 8 bytes | |05 30 | | | valu꞉ 0 | |04 31 | | | | |03 32 | | | | |02 33 | | | c336 | |01 34 | | +---------------------------------------+ <-+00 0(SP) 35 | +-> -------------------------------------------------------------------------------- /docs/02-interface-values/images/09-on-the-stack-fig3.ascii: -------------------------------------------------------------------------------- 1 | +-> local variables 2 | | +---------------------------------------+ <-+ 3 | | | name꞉ y | |31 4 | | | type꞉ interface{} | |30 5 | | | size꞉ 16 bytes | |29 6 | | | | |28 7 | | | | |27 8 | bottom of stack frame | | | |26 9 | high address +---------------------------------------+ | | | |25 10 | | arguments cFFF | | | | <-+24 0x18(SP) 11 | | +---------------------------------------+ | | | |23 12 | | | return address (PC) c9F9 | | | | |22 13 | | +---------------------------------------+ | | | |21 14 | | | frame pointer c6C6 | | | | |20 15 | | +---------------------------------------+ | | | |19 16 | | | return values c696 | | | | |18 17 | v +---------------------------------------+ | | c669 | |17 18 | | local variables c363 | -+ +---------------------------------------+ <-+16 0x10(SP) 19 | low address +---------------------------------------+ | : : |15 20 | stack pointer (SP) | : : |14 21 | top of stack frame | : : |13 22 | | : : |12 23 | | : : |11 24 | | : : |10 25 | | : : |09 26 | | +---------------------------------------+ <-+08 0x8(SP) 27 | | | name꞉ x | |07 28 | | | type꞉ int64 | |06 29 | | | size꞉ 8 bytes | |05 30 | | | valu꞉ 2 | |04 31 | | | | |03 32 | | | | |02 33 | | | c336 | |01 34 | | +---------------------------------------+ <-+00 0(SP) 35 | +-> -------------------------------------------------------------------------------- /docs/02-interface-values/images/09-on-the-stack-fig4.ascii: -------------------------------------------------------------------------------- 1 | +-> local variables 2 | | +---------------------------------------+ <-+ 3 | | | name꞉ y | |31 4 | | | type꞉ interface{} | |30 5 | | | size꞉ 16 bytes | |29 6 | | | | |28 7 | | | | |27 8 | bottom of stack frame | | | |26 9 | high address +---------------------------------------+ | | | |25 10 | | arguments cFFF | | | | <-+24 0x18(SP) 11 | | +---------------------------------------+ | | | |23 12 | | | return address (PC) c9F9 | | | | |22 13 | | +---------------------------------------+ | | | |21 14 | | | frame pointer c6C6 | | | | |20 15 | | +---------------------------------------+ | | | |19 16 | | | return values c696 | | | | |18 17 | v +---------------------------------------+ | | c99C | |17 18 | | local variables c363 | -+ +---------------------------------------+ <-+16 0x10(SP) 19 | low address +---------------------------------------+ | | name꞉ | |15 20 | stack pointer (SP) | | type꞉ int64 | |14 21 | top of stack frame | | size꞉ 8 bytes | |13 22 | | | valu꞉ 2 | |10 23 | | | | |11 24 | | | | |10 25 | | | c669 | |09 26 | | +---------------------------------------+ <-+08 0x8(SP) 27 | | | name꞉ x | |07 28 | | | type꞉ int64 | |06 29 | | | size꞉ 8 bytes | |05 30 | | | valu꞉ 2 | |04 31 | | | | |03 32 | | | | |02 33 | | | c336 | |01 34 | | +---------------------------------------+ <-+00 0(SP) 35 | +-> -------------------------------------------------------------------------------- /docs/02-interface-values/images/09-on-the-stack-fig5.ascii: -------------------------------------------------------------------------------- 1 | +-> local variables 2 | | +---------------------------------------+ <-+ 3 | | | name꞉ y | |31 4 | | | type꞉ interface{} | |30 5 | | | size꞉ 16 bytes | |29 6 | | | | |28 7 | | | | |27 8 | bottom of stack frame | | | |26 9 | high address +---------------------------------------+ | | | |25 10 | | arguments cFFF | | | +-------------------+ <-+24 0x18(SP) 11 | | +---------------------------------------+ | | | type꞉ uintptr | |23 12 | | | return address (PC) c9F9 | | | | size꞉ 8 bytes | |22 13 | | +---------------------------------------+ | | | valu꞉ &type.int64 | |21 14 | | | frame pointer c6C6 | | | | | |20 15 | | +---------------------------------------+ | | | | |19 16 | | | return values c696 | | | | | |18 17 | v +---------------------------------------+ | | c99C | c99C | |17 18 | | local variables c363 | -+ +-------------------+-------------------+ <-+16 0x10(SP) 19 | low address +---------------------------------------+ | | name꞉ | |15 20 | stack pointer (SP) | | type꞉ int64 | |14 21 | top of stack frame | | size꞉ 8 bytes | |13 22 | | | valu꞉ 2 | |10 23 | | | | |11 24 | | | | |10 25 | | | c669 | |09 26 | | +---------------------------------------+ <-+08 0x8(SP) 27 | | | name꞉ x | |07 28 | | | type꞉ int64 | |06 29 | | | size꞉ 8 bytes | |05 30 | | | valu꞉ 2 | |04 31 | | | | |03 32 | | | | |02 33 | | | c336 | |01 34 | | +---------------------------------------+ <-+00 0(SP) 35 | +-> -------------------------------------------------------------------------------- /docs/02-interface-values/images/09-on-the-stack-fig6.ascii: -------------------------------------------------------------------------------- 1 | +-> local variables 2 | | +-------------------+-------------------+ <-+ 3 | | | name꞉ y | type꞉ uintptr | |31 4 | | | type꞉ interface{} | size꞉ 8 bytes | |30 5 | | | size꞉ 16 bytes | valu꞉ 0x8(SP) | |29 6 | | | | | |28 7 | | | | | |27 8 | bottom of stack frame | | | | |26 9 | high address +---------------------------------------+ | | | c99C | |25 10 | | arguments cFFF | | | +-------------------+ <-+24 0x18(SP) 11 | | +---------------------------------------+ | | | type꞉ uintptr | |23 12 | | | return address (PC) c9F9 | | | | size꞉ 8 bytes | |22 13 | | +---------------------------------------+ | | | valu꞉ &type.int64 | |21 14 | | | frame pointer c6C6 | | | | | |20 15 | | +---------------------------------------+ | | | | |19 16 | | | return values c696 | | | | | |18 17 | v +---------------------------------------+ | | c99C | c99C | |17 18 | | local variables c363 | -+ +-------------------+-------------------+ <-+16 0x10(SP) 19 | low address +---------------------------------------+ | | name꞉ | |15 20 | stack pointer (SP) | | type꞉ int64 | |14 21 | top of stack frame | | size꞉ 8 bytes | |13 22 | | | valu꞉ 2 | |10 23 | | | | |11 24 | | | | |10 25 | | | c669 | |09 26 | | +---------------------------------------+ <-+08 0x8(SP) 27 | | | name꞉ x | |07 28 | | | type꞉ int64 | |06 29 | | | size꞉ 8 bytes | |05 30 | | | valu꞉ 2 | |04 31 | | | | |03 32 | | | | |02 33 | | | c336 | |01 34 | | +---------------------------------------+ <-+00 0(SP) 35 | +-> -------------------------------------------------------------------------------- /docs/03-escape-analysis/03-escape.md: -------------------------------------------------------------------------------- 1 | # Escape 2 | 3 | Someone turned on the tap and that leaky sink just let water escape. But just like in real life, there does not need to be a pre-existing leak for the floor to get wet. This page discusses when and how variables escape to the heap, either by way of a leaked parameter or all on their own. 4 | 5 | * [**Criteria**](#criteria): what are the criteria for escaping to the heap? 6 | * [**Escaping via a leaked parameter**](#escaping-via-a-leaked-parameter): bring the duct tape! 7 | * [**Escaping via a sink**](#escaping-via-a-sink): a little easier to detect 8 | 9 | 10 | ## Criteria 11 | 12 | There is one requirement to be eligible for escaping to the heap: 13 | 14 | * The variable must be a reference type, ex. channels, interfaces, maps, pointers, slices 15 | * A value type stored in an interface value can also escape to the heap 16 | 17 | If the above criteria is met, then a parameter will escape if it outlives its current stack frame. That usually happens when either: 18 | 19 | * The variable is sent to a function that assigns the variable to a sink outside the stack frame 20 | * Or the function where the variable is declared assigns it to a sink outside the stack frame 21 | 22 | 23 | ## Escaping via a leaked parameter 24 | 25 | A variable can escape via a function with a parameter that leaks to a location outside of the stakc frame. For example: 26 | 27 | ```go 28 | var sink *int32 29 | 30 | //go:noinline 31 | func recordID(id *int32) { // leaking param: id 32 | sink = id 33 | } 34 | 35 | //go:noinline 36 | func main() { 37 | id1 := new(int32) // new(int32) escapes to the heap 38 | *id1 = 4096 39 | recordID(id1) 40 | } 41 | ``` 42 | 43 | Use the following command to print the optimizations for the above program: 44 | 45 | ```bash 46 | docker run -it --rm go-interface-values \ 47 | go tool compile -m ./docs/03-escape-analysis/examples/ex3/main.go 48 | ``` 49 | 50 | The output should resemble: 51 | 52 | ```bash 53 | ./docs/03-escape-analysis/examples/ex3/main.go:22:15: leaking param: id 54 | ./docs/03-escape-analysis/examples/ex3/main.go:28:12: new(int32) escapes to heap 55 | ``` 56 | 57 | Unlike the similar example from the previous page, this time `new(int32)` _does_ escape to the heap. The escape occurs because: 58 | 59 | * `main` declared `id1` as an `*int32` 60 | * `id1` is used as an argument for the `recordID` function's `id` parameter 61 | * the `recordID` paramter `id` leaks to the package-level `sink` variable 62 | * package scoped variables are not rooted on the same stack frame as function locals 63 | 64 | However, a variable that escapes to the heap does not need to do so via a leaked function parameter. 65 | 66 | ## Escaping via a sink 67 | 68 | You have probably already guessed the other way a variable escapes to the heap -- _What if `main` assigned `id1` to `sink` directly?_ And you know what, you would be right! A variable escapes to the heap if it is a reference type and outlives its stack frame. The purpose of the two, distinct examples on this page is to illustrate that _leak_, _escape_, _move_ is not a sequence -- a variable can escape to the heap without being used with a leaky parameter. Consider the following example: 69 | 70 | ```go 71 | package main 72 | 73 | var sink *int32 74 | 75 | //go:noinline 76 | func main() { 77 | id1 := new(int32) // new(int32) escapes to the heap 78 | *id1 = 4096 79 | sink = id1 80 | } 81 | ``` 82 | 83 | Use the following command to print the optimizations for the above program: 84 | 85 | ```bash 86 | docker run -it --rm go-interface-values \ 87 | go tool compile -m ./docs/03-escape-analysis/examples/ex4/main.go 88 | ``` 89 | 90 | The output should resemble: 91 | 92 | ```bash 93 | ./docs/03-escape-analysis/examples/ex4/main.go:23:12: new(int32) escapes to heap 94 | ``` 95 | 96 | With no leak in sight the value of `id1` still escapes to the heap. I am sure some of you have noticed the strange way the last few examples have assigned the value to the id variables. What happens if we simplify it like so? 97 | 98 | ```go 99 | id1 := int32(4096) 100 | sink = &id1 101 | ``` 102 | 103 | Well, I suppose you will just have to _move_ to the next page to find out! 104 | 105 | --- 106 | 107 | Next: [Move](./04-move.md) 108 | -------------------------------------------------------------------------------- /docs/02-interface-values/05-underlying-type.md: -------------------------------------------------------------------------------- 1 | # Underlying type 2 | 3 | Now that we know interfaces are just a pair of pointers with addresses to the underlying type and value, it should be pretty simple to access the values at those addresses, right? Well, it's simple to access the _memory_, but to access the value we need to know the type of the value stored _in_ that memory. Remember, an interface is a tuple with: 4 | 5 | * an address to the type stored in the interface 6 | * an address to the value stored in the interface 7 | 8 | Therefore if we can figure out how to make sense of the first element, the underlying type, we are a step closer to making sense of the second, the underlying value. Luckily it's fairly straight-forward to do so. An interface stores type information in an internal type, [`type _type struct`](https://github.com/golang/go/blob/d588f487703e773ba4a2f0a04f2d4141610bff6b/src/runtime/type.go#L30-L51) from the [`runtime`](https://pkg.go.dev/runtime) package: 9 | 10 | ```go 11 | type _type struct { 12 | size uintptr 13 | ptrdata uintptr 14 | hash uint32 15 | tflag tflag // uint8 16 | align uint8 17 | fieldAlign uint8 18 | kind uint8 19 | equal func(unsafe.Pointer, unsafe.Pointer) bool // uintptr 20 | gcdata *byte 21 | str nameOff // int32 22 | ptrToThis typeOff // int32 23 | } 24 | ``` 25 | 26 | The value of the `kind` field is the one we need, and in fact this value maps directly to the public [`reflect.Kind`](https://pkg.go.dev/reflect#Kind) type. There are two ways to obtain the information we need: 27 | 28 | * A modified version of the private `_type` struct 29 | * A memory offset 30 | 31 | Here is an example that uses both methods ([Golang playground](https://go.dev/play/p/ddUPcDsDaJQ)): 32 | 33 | ```go 34 | package main 35 | 36 | import ( 37 | "fmt" 38 | "reflect" 39 | "unsafe" 40 | ) 41 | 42 | // _type is a copy of the private runtime._type struct 43 | // (https://bit.ly/34z6d0R) with the following changes: 44 | // 45 | // * the field "tflag" is changed to its underlying type, uint8 46 | // * fields after "kind" are omitted 47 | type _type struct { 48 | size uintptr // 8 bytes on a 64-bit platform 49 | ptrdata uintptr // 8 bytes on a 64-bit platform 50 | hash uint32 // 4 bytes 51 | tflag uint8 // 1 byte 52 | align uint8 // 1 byte 53 | fieldAlign uint8 // 1 byte 54 | kind uint8 // offset by 23 bytes on 64-bit platforms 55 | } 56 | 57 | func main() { 58 | // Store an int64 as an interface value. 59 | iface := interface{}(int64(2)) 60 | 61 | // Get an unsafe pointer for "iface". 62 | ptrIface := unsafe.Pointer(&iface) 63 | 64 | // Cast the unsafe pointer to a *[2]uintptr. 65 | ptrList := (*[2]uintptr)(ptrIface) 66 | 67 | // Read the type information using: 68 | // * the _type struct 69 | // * the memory offset for the kind field -- 23 bytes on 64-bit platforms 70 | kindFromStruct := reflect.Kind(((*_type)(unsafe.Pointer(ptrList[0]))).kind) 71 | kindFromOffset := reflect.Kind(*(*uint8)(unsafe.Pointer(ptrList[0] + 23))) 72 | 73 | fmt.Println(kindFromStruct) 74 | fmt.Println(kindFromOffset) 75 | } 76 | ``` 77 | 78 | The output from the above program will be: 79 | 80 | ```bash 81 | int64 82 | int64 83 | ``` 84 | 85 | In fact, there is something else interesting about the underlying types -- they are shared across all interfaces. Check out this example ([Golang playground](https://go.dev/play/p/ewZtZafue19)): 86 | 87 | ```go 88 | package main 89 | 90 | func main() { 91 | println(interface{}(int32(3))) 92 | println(interface{}(int32(5))) 93 | println(interface{}(int64(3))) 94 | println(interface{}(int64(5))) 95 | } 96 | ``` 97 | 98 | The output this time will resemble: 99 | 100 | ```bash 101 | (0x459d80,0x476598) 102 | (0x459d80,0x4765a0) 103 | (0x459dc0,0x476598) 104 | (0x459dc0,0x4765a0) 105 | ``` 106 | 107 | Huh, it looks like there is some duplicate information. Notice how the addresses for the types are the same for the two `int32` values and the two `int64` values? 108 | 109 | * **`0x459d80`**: type address for `int32(3)` and `int32(5)` 110 | * **`0x459dc0`**: type address for `int64(3)` and `int64(5)` 111 | 112 | This is because type information is global and is shared for all values stored in interfaces. However, are the addresses for the values _also_ shared -- across _type_!? 113 | 114 | * **`0x476598`**: value address for `int32(3)` and `int64(3)` 115 | * **`0x4765a0`**: value address for `int32(5)` and `int64(5)` 116 | 117 | How can this be? Keep reading to find out! 118 | 119 | --- 120 | 121 | Next: [Underlying value](./06-underlying-value.md) 122 | -------------------------------------------------------------------------------- /hack/b2md.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | Copyright 2022 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | """ 18 | 19 | """b2md 20 | 21 | Transforms Go benchmark output into a markdown table. 22 | 23 | b2md.py FILE 24 | cat FILE | b2md.py 25 | """ 26 | 27 | import argparse 28 | import re 29 | import sys 30 | 31 | 32 | def intOrFloat(s): 33 | if "." in s: 34 | return float(s) 35 | return int(s) 36 | 37 | 38 | def twoDecimalFloat(f): 39 | return float("{:.2f}".format(f)) 40 | 41 | 42 | def noZedZed(f): 43 | c = str(f) 44 | if "." in c: 45 | if c.endswith(".0"): 46 | return int(c.replace(".0", "")) 47 | return twoDecimalFloat(float(c)) 48 | return int(c) 49 | 50 | 51 | parser = argparse.ArgumentParser( 52 | description="b2md", formatter_class=argparse.ArgumentDefaultsHelpFormatter 53 | ) 54 | 55 | parser.add_argument( 56 | "--no-echo", 57 | dest="no_echo", 58 | action="store_true", 59 | help="do not echo stdin to stdout", 60 | ) 61 | 62 | parser.add_argument( 63 | "file", 64 | metavar="FILE", 65 | nargs="?", 66 | help="path to an input file", 67 | ) 68 | 69 | args = parser.parse_args() 70 | if not args: 71 | parser.print_help() 72 | sys.exit(1) 73 | 74 | 75 | if args.file: 76 | echo = False 77 | f = open(args.file, "r") 78 | else: 79 | echo = not args.no_echo 80 | f = sys.stdin 81 | 82 | rx_name = re.compile(r"^.+real\(T\)=(.+)$") 83 | rx_data = re.compile( 84 | r"^BenchmarkMem/([^/]+)/([^/]+)/([^-]+)-\d+\s+([\d.]+)\s+([\d.]+)\s+ns/op\s+([\d.]+)\s+B/op\s+([\d.]+) allocs/op$" 85 | ) 86 | 87 | """ 88 | data = [ 89 | { 90 | "type": "int64", 91 | "0": { 92 | "h": { 93 | "bytes": 0, 94 | "alloc": 0, 95 | }, 96 | 97 | # same as h 98 | "s": {} 99 | }, 100 | 101 | # same as 0 102 | "n": {} 103 | }, 104 | 105 | # repeat for other types 106 | """ 107 | data = [] 108 | 109 | for line in f: 110 | if echo: 111 | print(line, end="") 112 | 113 | m = rx_name.match(line) 114 | if m: 115 | data.append( 116 | { 117 | "T": m.group(1), 118 | "0": { 119 | "h": { 120 | "bytes": "", 121 | "alloc": "", 122 | }, 123 | "s": { 124 | "bytes": "", 125 | "alloc": "", 126 | }, 127 | }, 128 | "n": { 129 | "h": { 130 | "bytes": "", 131 | "alloc": "", 132 | }, 133 | "s": { 134 | "bytes": "", 135 | "alloc": "", 136 | }, 137 | }, 138 | } 139 | ) 140 | else: 141 | m = rx_data.match(line) 142 | if not m: 143 | continue 144 | 145 | type = m.group(1) 146 | zorn = m.group(2) 147 | hors = m.group(3) 148 | bytesOp = noZedZed(m.group(6)) 149 | allocOp = noZedZed(m.group(7)) 150 | 151 | data[len(data) - 1]["type"] = type 152 | data[len(data) - 1][zorn][hors]["bytes"] = bytesOp 153 | data[len(data) - 1][zorn][hors]["alloc"] = allocOp 154 | 155 | # Go ahead and close the file. 156 | f.close() 157 | 158 | print( 159 | "| Type | `%T` | Bytes to store zero value | ....in an interface | Bytes to store non-zero, random value | ...in an interface |" 160 | ) 161 | print( 162 | "|:----:|:----:|:-------------------------:|:-------------------:|:-------------------------------------:|:------------------:|" 163 | ) 164 | 165 | s = "| {} | `{}` | {} | {} | {} | {} |" 166 | for o in data: 167 | print( 168 | s.format( 169 | o["type"], 170 | o["T"], 171 | o["0"]["s"]["bytes"], 172 | o["0"]["h"]["bytes"], 173 | o["n"]["s"]["bytes"], 174 | o["n"]["h"]["bytes"], 175 | ) 176 | ) 177 | -------------------------------------------------------------------------------- /tests/lem/lem_leak_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package lem_test 18 | 19 | import "testing" 20 | 21 | // lem.leak1.name=to sink 22 | // lem.leak1.alloc=0 23 | // lem.leak1.bytes=0 24 | func leak1(b *testing.B) { 25 | var sink *int32 26 | f := func(p *int32) { // lem.leak1.m=leaking param: p 27 | sink = p 28 | } 29 | b.ResetTimer() 30 | for i := 0; i < b.N; i++ { 31 | f(nil) 32 | } 33 | _ = sink 34 | } 35 | func init() { 36 | lemFuncs["leak1"] = leak1 37 | } 38 | 39 | // lem.leak2.name=pointer to result 40 | // lem.leak2.alloc=0 41 | // lem.leak2.bytes=0 42 | func leak2(b *testing.B) { 43 | f := func(p *int32) *int32 { // lem.leak2.m=leaking param: p to result ~r[0-1] level=0 44 | return p 45 | } 46 | b.ResetTimer() 47 | for i := 0; i < b.N; i++ { 48 | f(nil) 49 | } 50 | } 51 | func init() { 52 | lemFuncs["leak2"] = leak2 53 | } 54 | 55 | // lem.leak3.name=map to result 56 | // lem.leak3.alloc=0 57 | // lem.leak3.bytes=0 58 | func leak3(b *testing.B) { 59 | var sink map[string]struct{} 60 | f := func(m map[string]struct{}) { // lem.leak3.m=leaking param: m 61 | sink = m 62 | } 63 | b.ResetTimer() 64 | for i := 0; i < b.N; i++ { 65 | f(nil) 66 | } 67 | _ = sink 68 | } 69 | func init() { 70 | lemFuncs["leak3"] = leak3 71 | } 72 | 73 | // lem.leak4.name=slice to result 74 | // lem.leak4.alloc=0 75 | // lem.leak4.bytes=0 76 | func leak4(b *testing.B) { 77 | var sink []int32 78 | f := func(s []int32) { // lem.leak4.m=leaking param: s 79 | sink = s 80 | } 81 | b.ResetTimer() 82 | for i := 0; i < b.N; i++ { 83 | f(nil) 84 | } 85 | _ = sink 86 | } 87 | func init() { 88 | lemFuncs["leak4"] = leak4 89 | } 90 | 91 | // lem.leak5.name=chan to result 92 | // lem.leak5.alloc=0 93 | // lem.leak5.bytes=0 94 | func leak5(b *testing.B) { 95 | var sink chan int32 96 | f := func(c chan int32) { // lem.leak5.m=leaking param: c 97 | sink = c 98 | } 99 | b.ResetTimer() 100 | for i := 0; i < b.N; i++ { 101 | f(nil) 102 | } 103 | _ = sink 104 | } 105 | func init() { 106 | lemFuncs["leak5"] = leak5 107 | } 108 | 109 | // lem.leak6.name=five ref params to result w zero mallocs 110 | // lem.leak6.alloc=0 111 | // lem.leak6.bytes=0 112 | func leak6(b *testing.B) { 113 | type s struct{ a, b int32 } 114 | noop := func( 115 | a *int32, // lem.leak6.m=leaking param: a to result ~r[06] level=0 116 | b *int64, // lem.leak6.m=leaking param: b to result ~r[17] level=0 117 | c []int32, // lem.leak6.m=leaking param: c to result ~r[28] level=0 118 | d []*int64, // lem.leak6.m=leaking param: d to result ~r[39] level=0 119 | e s, // lem.leak6.m!=(escapes|leaking|moved) 120 | f *s, // lem.leak6.m=leaking param: f to result ~r(5|11) level=0 121 | ) (*int32, *int64, []int32, []*int64, s, *s) { 122 | return a, b, c, d, e, f 123 | } 124 | b.ResetTimer() 125 | for i := 0; i < b.N; i++ { 126 | var ( 127 | a = new(int32) // lem.leak6.m=new\(int32\) does not escape 128 | b = new(int64) // lem.leak6.m=new\(int64\) does not escape 129 | c = make([]int32, 5, 5) // lem.leak6.m=make\(\[\]int32, 5, 5\) does not escape 130 | d = make([]*int64, 10, 10) // lem.leak6.m=make\(\[\]\*int64, 10, 10\) does not escape 131 | e = s{a: 4096, b: 4096} // lem.leak6.m!=(escapes|leaking|moved) 132 | f = new(s) // lem.leak6.m=new\(s\) does not escape 133 | ) 134 | noop(a, b, c, d, e, f) 135 | } 136 | } 137 | func init() { 138 | lemFuncs["leak6"] = leak6 139 | } 140 | 141 | // lem.leak7.name=did not escape bc return value did not outlive stack frame 142 | // lem.leak7.alloc=0 143 | // lem.leak7.bytes=0 144 | func leak7(b *testing.B) { 145 | f := func(p *int32) *int32 { // lem.leak7.m=leaking param: p to result ~r[0-1] level=0 146 | return p 147 | } 148 | b.ResetTimer() 149 | for i := 0; i < b.N; i++ { 150 | var x int32 = 4096 // lem.leak7.m!=(escapes|leaking|moved) 151 | var p *int32 = &x // lem.leak7.m!=(escapes|leaking|moved) 152 | var sink *int32 // lem.leak7.m!=(escapes|leaking|moved) 153 | sink = f(p) // lem.leak7.m!=(escapes|leaking|moved) 154 | _ = sink 155 | } 156 | } 157 | func init() { 158 | lemFuncs["leak7"] = leak7 159 | } 160 | -------------------------------------------------------------------------------- /docs/03-escape-analysis/02-leak.md: -------------------------------------------------------------------------------- 1 | # Leak 2 | 3 | Imagine for a moment there is a kitchen sink with a crack in it. The sink has the _potential_ to leak, but nothing will _escape_ the basin until the sink is used. Much like our imaginary sink, escape analysis inspects variables with the potential to escape when they are used. If that potential exists, the variable is marked as "leaking." 4 | 5 | * [**Criteria**](#criteria): what are the criteria for leaking? 6 | * [**Leak destination**](#leak-destination): where does the water go when it goes down the drain? 7 | * [**Leaking (to a sink)**](#leaking-to-a-sink): bye-bye 8 | * [**Leaking to result**](#leaking-to-result): you'll be back! 9 | * [**Leak without escape**](#leak-without-escape): you never left! 10 | 11 | 12 | ## Criteria 13 | 14 | There are two requirements to be eligible for leaking: 15 | 16 | * The variable must be a function parameter 17 | * The variable must be a reference type, ex. channels, interfaces, maps, pointers, slices 18 | 19 | Value types such as built-in numeric types, structs, and arrays are not elgible to be leaked. That does not mean they are never placed on the heap, it just means a parameter of `int32` is not going to send you running for a mop anytime soon. 20 | 21 | If the above criteria is met, then a parameter will leak if: 22 | 23 | * The variable is returned from the same function and/or 24 | * is assigned to a sink outside of the stack frame to which the variable belongs. 25 | 26 | 27 | ## Leak destination 28 | 29 | There are two primary types of leaks: 30 | 31 | * leaking (to a sink) 32 | * leaking to result 33 | 34 | 35 | ### Leaking (to a sink) 36 | 37 | If a function's parameter is a reference type and the function assigns the parameter to a variable outside of the function, the variable is leaking. While the compiler flag `-m` that prints optimizations does not indicate to _where_ the parameter is leaking, it is helpful to think of this as _leaking to a [sink](https://en.wikipedia.org/wiki/Sink_(computing)). For example: 38 | 39 | ```golang 40 | var sink *int32 41 | 42 | func recordID(id *int32) { // leaking param: id 43 | sink = id 44 | } 45 | ``` 46 | 47 | The function `recordID` leaks the parameter `id` to the package-level field `sink`. 48 | 49 | ### Leaking to result 50 | 51 | Another type of leak is when a reference parameter is returned from a function: 52 | 53 | ```golang 54 | func validateID(id *int32) *int32 { // leaking param: id to result ~r1 level=0 55 | return id 56 | } 57 | ``` 58 | 59 | Other than the fact that `validateID` has very poor validation logic (indeed some might call it non-existent :smiley:), the function returns the `id` parameter. Because the value of `id` is returned, it means it outlives the function's stack frame and has the potential to escape to the heap. Therefore the parameter is marked as _leaking to result_. 60 | 61 | ## Leak without escape 62 | 63 | Please remember, a leak is about the _potential_ to escape to the heap. For example, a parameter can leak to result without ever escaping to the heap. For example: 64 | 65 | ```go 66 | func main() { 67 | var id1 int32 = 4096 68 | if validateID(&id1) == nil { 69 | os.Exit(1) 70 | } 71 | 72 | var id2 *int32 = new(int32) // new(int32) does not escape 73 | *id2 = 4096 74 | validID := validateID(id2) 75 | if validID == nil { 76 | os.Exit(1) 77 | } 78 | } 79 | ``` 80 | 81 | Use the following command to print the optimizations for the above program: 82 | 83 | ```bash 84 | docker run -it --rm go-interface-values \ 85 | go tool compile -m ./docs/03-escape-analysis/examples/ex2/main.go 86 | ``` 87 | 88 | The output should resemble: 89 | 90 | ```bash 91 | ./docs/03-escape-analysis/examples/ex2/main.go:22:17: leaking param: id to result ~r0 level=0 92 | ./docs/03-escape-analysis/examples/ex2/main.go:32:22: new(int32) does not escape 93 | ``` 94 | 95 | Please note: 96 | 97 | * The `id` parameter for the `validateID` function is leaking to result because the function returns the incoming parameter and thus there is potential for the value of `id` to outlive its stack frame. 98 | * The value `id1` is not even mentioned because it is a value type and escape analysis only applies to reference types. While a pointer to `id1` was passed into the `validateID` function, the Go compiler optimized the pointer to the stack. 99 | * The value `id2` _is_ a reference type, but it does not escape. Even though the return value of `validateID` is assigned to `validID`, its object is on the same stack frame as `id2`, thus the latter does not outlive its stack frame. Therefore `id2` does not escape to the heap. 100 | 101 | 102 | A leak is often only felt a drop at a time, but what about a full-on escape? 103 | 104 | --- 105 | 106 | Next: [Escape](./03-escape.md) 107 | -------------------------------------------------------------------------------- /docs/99-appendix/assembly.md: -------------------------------------------------------------------------------- 1 | # Assembly 2 | 3 | This is a reference for the assembly found in this repository: 4 | 5 | * [**Why are all of the operands inverted for `MOV` and other instructions?**](#why-are-all-of-the-operands-inverted-for-mov-and-other-instructions) 6 | * [**What does the `Q` suffix for instructions like `MOVQ` and `LEAQ` mean?**](#what-does-the-q-suffix-for-instructions-like-movq-and-leaq-mean) 7 | * [**What is the x86 assembly instruction `CALL` actually calling?**](#what-is-the-x86-assembly-instruction-call-actually-calling) 8 | * [**Where is the `CALL` instruction in ARM assembly?**](#where-is-the-call-instruction-in-arm-assembly) 9 | 10 | 11 | ## Why are all of the operands inverted for `MOV` and other instructions? 12 | 13 | Normally `MOVQ` operates right to left, `MOVQ DST SRC`, but as the [Go assembly documentation](https://go.dev/doc/asm) states: 14 | 15 | > One detail evident in the examples from the previous sections is that data in the instructions flows from left to right: MOVQ $0, CX clears CX. This rule applies even on architectures where the conventional notation uses the opposite direction. 16 | 17 | This inversion is true for many different instructions with the `DST SRC` operands. In Go asm the order is inverted to be `SRC DST`. 18 | 19 | 20 | ## What does the `Q` suffix for instructions like `MOVQ` and `LEAQ` mean? 21 | 22 | While a _word_ is supposed to be dependent upon the platform, because _word_ was eight bits in early x86 architecture, a _word_ still refers to eight bits on modern x86 and x86_64 systems. Therefore a _quadword_, or `Q`, refers to 64 bits. 23 | 24 | Thus instrucitons like `MOVQ` and `LEAQ` are variants of `MOV` and `LEA` that address 64-bit literals and/or memory addresses. For example, in Go asm the instruction `MOVQ 0x8(SP) 0x10(SP)` copies eight bytes of data from the address `0x8(SP)` to `0x10(SP)`. 25 | 26 | 27 | ## What is the x86 assembly instruction `CALL` actually calling? 28 | 29 | The x86 assembly instruction `CALL` is used to call a procedure. For example, consider the following line of assembly from this project: 30 | 31 | ```assembly 32 | 0x003c 00060 (mem_test.go:312) CALL runtime.convT32(SB) 33 | ``` 34 | 35 | The `CALL` instruction specifies `runtime.convT32(SB)`. The `(SB)` (_static base_) suffix lets us know that `runtime.convT32` is a global symbol and references the memory address for the procedure to call. In order to examine what that symbol is, we can: 36 | 37 | 1. Build the test binary for this project: 38 | 39 | ```bash 40 | go test -c ./tests/mem 41 | ``` 42 | 43 | 2. Use `go tool objdump` to search for the symbol: 44 | 45 | ```bash 46 | $ go tool objdump -s runtime.convT32 mem.test 47 | TEXT runtime.convT32(SB) /Users/akutz/.go/1.18beta2/src/runtime/iface.go 48 | iface.go:365 0x100008b20 f9400b90 MOVD 16(R28), R16 49 | ... 50 | ``` 51 | 52 | Procedures in assembly are easy to spot as they are defined with the `TEXT` directive illustrated up above. 53 | 54 | 3. Or build the `runtime` package without linking it into this project's test binary. This assembly provides even more insight into the translation from Go to machine code: 55 | 56 | ```bash 57 | $ go build -gcflags "-S" runtime 2>&1 | grep -FA1 '"".convT32 STEXT' 58 | "".convT32 STEXT size=144 args=0x8 locals=0x28 funcid=0x0 align=0x0 59 | 0x0000 00000 (/Users/akutz/.go/1.18beta2/src/runtime/iface.go:365) TEXT "".convT32(SB), ABIInternal, $48-8 60 | ... 61 | ``` 62 | 63 | To learn more about the `CALL` instruction and procedures, please refer to: 64 | 65 | * [**A Quick Guide to Go's Assembler**](https://go.dev/doc/asm) ([symbols](https://go.dev/doc/asm#symbols), [directives](https://go.dev/doc/asm#directives)) 66 | * [**x86 asm `CALL`**](https://www.felixcloutier.com/x86/call) 67 | 68 | 69 | ## Where is the `CALL` instruction in ARM assembly? 70 | 71 | The assembly for x86 and amd64 both define the [`CALL`](https://www.felixcloutier.com/x86/call) instruction. However, [ARM assembly](https://developer.arm.com/documentation/ddi0602/2021-12/?lang=en) does _not_ define a `CALL` instruction, despite it appearing in the assembly for Go sources in this project when they are compiled on an M1 Macbook. What gives? 72 | 73 | What was likely an attempt to maintain naming convention consistency in Go assembly across different processor architectures, Go renames the ARM assembly instruction [`BLR`](https://developer.arm.com/documentation/ddi0602/2021-12/Base-Instructions/BLR--Branch-with-Link-to-Register-?lang=en) (_branch with link to register_) to `CALL`. Both instructions call a procedure (x86) or subroutine (ARM) at a given address, so for all intents and purposes they behave semantically similar. 74 | 75 | To learn more about the `CALL` instruction and Go on ARM, please refer to: 76 | 77 | * [**Instructions mnemonics mapping rules for the Go ARM64 assembler**](https://pkg.go.dev/cmd/internal/obj/arm64#hdr-Instructions_mnemonics_mapping_rules) 78 | * [**ARM64 developer documentation**](https://developer.arm.com/documentation/ddi0602/2021-12/?lang=en) 79 | -------------------------------------------------------------------------------- /docs/04-missing-mallocs/04-what.md: -------------------------------------------------------------------------------- 1 | # Overall impact 2 | 3 | The previous page reviews which types are subject to optimizations Go uses when storing values in interfaces, but did not actually detail the impact. Well, there are a few criteria reviewed on the previous page, but here it is again for convenience: 4 | 5 | * Zero values, including `0`, `nil`, and an empty string `""` all qualify. 6 | * Any value that is type which is a single byte wide, such as `bool`, `int8`, and `uint8`. 7 | * Any integer type with a value that is in the inclusive range of 0-255. 8 | * In some cases if `T` is subject to an optimization, so too will `struct{a T}`. 9 | 10 | In order to produce a more comprehensive dataset for when this behavior _could_ occur, we can run the following command: 11 | 12 | ```bash 13 | docker run -it --rm go-interface-values:latest \ 14 | bash -c 'go test -v -count 1 -benchtime 1000x \ 15 | -run Mem -benchmem -bench BenchmarkMem ./tests/mem | \ 16 | python3 hack/b2md.py --no-echo' 17 | ``` 18 | 19 | The output will be a markdown table that prints the: 20 | 21 | * friendly name of the type 22 | * actual type as formatted with `%T` 23 | * number of bytes allocated on the heap to: 24 | * copy a zero value to another variable of the same type 25 | * store a zero value in an interface 26 | * copy a non-zero value to another variable of the same type 27 | * store a non-zero value in an interface 28 | 29 | | Type | `%T` | Bytes to store zero value | ....in an interface | Bytes to store non-zero, random value | ...in an interface | 30 | |:----:|:----:|:-------------------------:|:-------------------:|:-------------------------------------:|:------------------:| 31 | | int | `int` | 0 | 0 | 0 | 8 | 32 | | int8 | `int8` | 0 | 0 | 0 | 0 | 33 | | int16 | `int16` | 0 | 0 | 0 | 2 | 34 | | int32 | `int32` | 0 | 0 | 0 | 4 | 35 | | int64 | `int64` | 0 | 0 | 0 | 8 | 36 | | uint | `uint` | 0 | 0 | 0 | 8 | 37 | | uint8 | `uint8` | 0 | 0 | 0 | 0 | 38 | | uint16 | `uint16` | 0 | 0 | 0 | 2 | 39 | | uint32 | `uint32` | 0 | 0 | 0 | 4 | 40 | | uint64 | `uint64` | 0 | 0 | 0 | 8 | 41 | | float32 | `float32` | 0 | 0 | 0 | 4 | 42 | | float64 | `float64` | 0 | 0 | 0 | 8 | 43 | | complex64 | `complex64` | 0 | 8 | 0 | 8 | 44 | | complex128 | `complex128` | 0 | 16 | 0 | 16 | 45 | | byte | `uint8` | 0 | 0 | 0 | 0 | 46 | | bool | `bool` | 0 | 0 | 0 | 0 | 47 | | rune | `int32` | 0 | 0 | 0 | 4 | 48 | | string | `string` | 0 | 0 | 0 | 16 | 49 | | struct_int | `struct { a int }` | 0 | 0 | 0 | 8 | 50 | | struct_int8 | `struct { a int8 }` | 0 | 1 | 0 | 1 | 51 | | struct_int16 | `struct { a int16 }` | 0 | 0 | 0 | 2 | 52 | | struct_int32 | `struct { a int32 }` | 0 | 0 | 0 | 4 | 53 | | struct_int64 | `struct { a int64 }` | 0 | 0 | 0 | 8 | 54 | | struct_uint | `struct { a uint }` | 0 | 0 | 0 | 8 | 55 | | struct_uint8 | `struct { a uint8 }` | 0 | 1 | 0 | 1 | 56 | | struct_uint16 | `struct { a uint16 }` | 0 | 0 | 0 | 2 | 57 | | struct_uint32 | `struct { a uint32 }` | 0 | 0 | 0 | 4 | 58 | | struct_uint64 | `struct { a uint64 }` | 0 | 0 | 0 | 8 | 59 | | struct_float32 | `struct { a float32 }` | 0 | 0 | 0 | 4 | 60 | | struct_float64 | `struct { a float64 }` | 0 | 0 | 0 | 8 | 61 | | struct_complex64 | `struct { a complex64 }` | 0 | 8 | 0 | 8 | 62 | | struct_complex128 | `struct { a complex128 }` | 0 | 16 | 0 | 16 | 63 | | struct_byte | `struct { a uint8 }` | 0 | 1 | 0 | 1 | 64 | | struct_bool | `struct { a bool }` | 0 | 1 | 0 | 1 | 65 | | struct_rune | `struct { a int32 }` | 0 | 0 | 0 | 4 | 66 | | struct_string | `struct { a string }` | 0 | 0 | 0 | 16 | 67 | | struct_int32_int32 | `struct { a int32; b int32 }` | 0 | 8 | 0 | 8 | 68 | | struct_int32_int64 | `struct { a int32; b int64 }` | 0 | 16 | 0 | 16 | 69 | | struct_array_bytes_7 | `struct { a [7]uint8 }` | 0 | 8 | 0 | 8 | 70 | | struct_byte_7 | `struct { a uint8; b uint8; c uint8; d uint8; e uint8; f uint8; g uint8 }` | 0 | 8 | 0 | 8 | 71 | 72 | The above table clearly aligns with the findings in this repository, that the following values stored in interfaces do not result in memory allocated on the heap: 73 | 74 | * zero values, ex. `0`, `nil`, and `""` 75 | * single byte-wide types, ex. `byte`, `bool`, `int8`, and `uint8` 76 | * non-zero, numeric values between and including 0-255 for types: 77 | * `int`, `int8`, `int16`, `int32`, `int64` 78 | * `uint`, `uint8`, `uint16`, `uint32`, `iint64` 79 | * ~~`float32`, `float64`~~ 80 | 81 | --- 82 | 83 | It is unclear to me why the types `float32` and `float64` are not subject to this optimization, but tests show they are not. The functions `convT32` and `convT64` are used for these types when storing values in interfaces, and those functions have the optimization logic. 84 | 85 | I intend to raise this in Gopher Slack and with a GitHub issue, and I will link those here. 86 | 87 | --- 88 | 89 | * zero values for `struct{a T}` where `T` is: 90 | * `int`, `int16`, `int32`, `int64` 91 | * `uint`, `uint16`, `uint32`, `uint64` 92 | * `float32`, `float64` 93 | * `rune` (which has an underlying type of `int32`) 94 | * `string` 95 | 96 | --- 97 | 98 | Next: [Lessons learned](../05-lessons-learned/) 99 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Go interface values 2 | 3 | This repository deep dives Go interface values, what they are, how they work, and when storing a value in a Go interface allocates memory on the heap. 4 | 5 | * [**Labs**](#labs): a step-by-step walkthrough of the topic 6 | * [**FAQ**](#FAQ): answers to frequently asked questions 7 | * [**Links**](#links): links to related reference material 8 | * [**Thanks**](#thanks): it takes a community 9 | * [**Appendix**](#appendix): in-repo reference material 10 | 11 | 12 | ## Labs 13 | 14 | 1. [**Prerequisites**](./docs/01-prereqs/): how to get from here to there 15 | 1. [**Interface values**](./docs/02-interface-values/): whatever you do, do not call it "boxing" 16 | 1. [**Escape analysis**](./docs/03-escape-analysis/): to malloc or not to malloc 17 | 1. [**Missing mallocs**](./docs/04-missing-mallocs/): there's a heap of missing memory 18 | 1. [**Lessons learned**](./docs/05-lessons-learned/): key takeaways 19 | 20 | 21 | ## FAQ 22 | 23 | * [**What does the `Q` suffix for instructions like `MOVQ` and `LEAQ` mean?**](#what-does-the-q-suffix-for-instructions-like-movq-and-leaq-mean) 24 | * [**What is the x86 assembly instruction `CALL` actually calling?**](#what-is-the-x86-assembly-instruction-call-actually-calling) 25 | * [**Where is the `CALL` instruction in ARM assembly?**](#where-is-the-call-instruction-in-arm-assembly) 26 | * [**What is the `hack` directory and the files inside of it?**](#what-is-the-hack-directory-and-the-files-inside-of-it) 27 | 28 | 29 | ### What does the `Q` suffix for instructions like `MOVQ` and `LEAQ` mean? 30 | 31 | Please refer to [this answer](./docs/99-appendix/assembly.md#what-does-the-q-suffix-for-instructions-like-movq-and-leaq-mean) from the assembly section in the appendix. 32 | 33 | 34 | ### What is the x86 assembly instruction `CALL` actually calling? 35 | 36 | Please refer to [this answer](./docs/99-appendix/assembly.md#what-is-the-x86-assembly-instruction-actually-calling) from the assembly section in the appendix. 37 | 38 | 39 | ### Where is the `CALL` instruction in ARM assembly? 40 | 41 | Please refer to [this answer](./docs/99-appendix/assembly.md#where-is-the-call-instruction-in-arm-assembly) from the assembly section in the appendix. 42 | 43 | 44 | ### What is the [`hack`](./hack) directory and the files inside of it? 45 | 46 | The `hack` directory is a convention I picked up from working on Kubernetes and projects related to Kuberentes. The directory contains scripts useful to the project, but not a core piece of the project itself. For example: 47 | 48 | * [**`hack/`**](./hack) 49 | * [**`asm2md.py`**](./hack/asm2md.py): parses the output of `go tool compile -S -wb=false ./tests/mem/*.go` and produces a markdown table 50 | * [**`b2md.py`**](./hack/b2md.py): parses the output of `go test -v -count 1 -benchtime 1000x -bench BenchmarkMem -run Mem -benchmem ./tests/mem` and produces a markdown table 51 | * [**`gen.py`**](./hack/gen.py): generates [`./tests/mem/bench_test.go`](./tests/mem/mem_test.go) and [`./tests/mem/types_test.go`](./tests/mem/types_test.go) 52 | 53 | 54 | ## Links 55 | 56 | * [**ARM developer documentation**](https://developer.arm.com/documentation/ddi0602/2021-12/?lang=en) 57 | * [**x86 and amd64 instruction set**](https://www.felixcloutier.com/x86/index.html) 58 | * [**A quick guide to Go assembly**](https://go.dev/doc/asm) 59 | * [**Go internal application binary interface (ABI) specification**](https://github.com/golang/go/blob/master/src/cmd/compile/abi-internal.md) 60 | * [**Logging, interfaces, and allocation**](https://commaok.xyz/post/interface-allocs/) 61 | * [**Go introduction to escape analysise**](https://medium.com/a-journey-with-go/go-introduction-to-the-escape-analysis-f7610174e890) 62 | * [**Type definitions for leaks**](https://github.com/golang/go/blob/master/src/cmd/compile/internal/escape/graph.go) 63 | * [**Enums for escape**](https://github.com/golang/go/blob/master/src/cmd/compile/internal/ir/node.go) 64 | * [**Type definition for a Go stack frame**](https://github.com/golang/go/blob/master/src/runtime/stack.go) 65 | 66 | 67 | ## Thanks 68 | 69 | * Many thanks to reddit user _nikandfor_ for [their response](https://www.reddit.com/r/golang/comments/sdsfl9/trying_to_understand_when_boxing_results_in_a/huf2upt/) to my post on this topic. Without that initial work, I am not sure this repository would exist today. 70 | * My gratitude to Crypto Jones from Gopher Slack for keeping me honest about "boxing." :smiley: 71 | * My colleague Michael Gasch who spent a lot of time proofreading this repository. Hear that y'all? Any mistakes you find? Totally Michael's fault! :smiley: 72 | * Andrew Williams, another co-worker, who did not judge me when he helpfully explained [cache lines](https://teivah.medium.com/go-and-cpu-caches-af5d32cc5592). 73 | * The first person who offered to help me dig into the assembly, Kevin Grittner! 74 | * Several of my colleagues who directed me to a [Trie](https://en.wikipedia.org/wiki/Trie) structure for the repository's bespoke test framework: 75 | * Michal Jankowski 76 | * Zhanghe Liu 77 | * Yiyi Zhou 78 | * Mayank Bhatt 79 | * Arunesh Pandey 80 | 81 | 82 | ## Appendix 83 | 84 | * [**Assembly**](./docs/99-appendix/assembly.md): reference section for go asm 85 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # If you update this file, please follow 16 | # https://suva.sh/posts/well-documented-makefiles 17 | 18 | ## -------------------------------------- 19 | ## General 20 | ## -------------------------------------- 21 | 22 | SHELL:=/usr/bin/env bash 23 | .DEFAULT_GOAL:=help 24 | 25 | # Use GOPROXY environment variable if set 26 | GOPROXY := $(shell go env GOPROXY) 27 | ifeq ($(GOPROXY),) 28 | GOPROXY := https://proxy.golang.org 29 | endif 30 | export GOPROXY 31 | 32 | # Active module mode, as we use go modules to manage dependencies 33 | export GO111MODULE=on 34 | 35 | # The help will print out all targets with their descriptions organized below 36 | # their categories. The categories are represented by `##@` and the target 37 | # descriptions by `##`. 38 | # 39 | # The awk commands is responsible to read the entire set of makefiles included 40 | # in this invocation, looking for lines of the file as xyz: ## something, and 41 | # then pretty-format the target and help. Then, if there's a line with ##@ 42 | # something, that gets pretty-printed as a category. 43 | # 44 | # More info over the usage of ANSI control characters for terminal 45 | # formatting: 46 | # https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters 47 | # 48 | # More info over awk command: http://linuxcommand.org/lc3_adv_awk.php 49 | .PHONY: help 50 | help: ## Display this help 51 | @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) 52 | 53 | 54 | IMAGE_NAME ?= go-interface-values 55 | IMAGE_TAG ?= latest 56 | IMAGE ?= $(IMAGE_NAME):$(IMAGE_TAG) 57 | PLATFORMS ?= linux/amd64,linux/arm64 58 | PUSH_ALL ?= 59 | IMAGE_RUN_FLAGS ?= -it --rm 60 | 61 | # DOCKER_TARGETS is a list of targets that will have the -docker 62 | # option to run in the container with the working directory mounted 63 | # into the container. 64 | DOCKER_TARGETS := generate-svgs 65 | 66 | # SANDBOX_TARGETS is a list of targets that will have the -sandbox 67 | # option to run in the container. 68 | SANDBOX_TARGETS := asm bench sizes test 69 | 70 | RUN_IN_PREFIX := docker run $(IMAGE_RUN_FLAGS) 71 | RUN_IN_DOCKER := $(RUN_IN_PREFIX) -v "$$(pwd):/go-interface-values" $(IMAGE) 72 | RUN_IN_SANDBOX := $(RUN_IN_PREFIX) $(IMAGE) 73 | 74 | 75 | ## -------------------------------------- 76 | ## Images 77 | ## -------------------------------------- 78 | .PHONY: image-build 79 | image-build: ## Build the docker image 80 | docker build -t $(IMAGE) . 81 | 82 | .PHONY: image-build-all 83 | image-build-all: ## Build the docker image for multiple platforms 84 | docker buildx build -t $(IMAGE) --platform $(PLATFORMS) $(PUSH_ALL) . 85 | 86 | .PHONY: image-push 87 | image-push: ## Push the docker image 88 | docker push $(IMAGE) 89 | 90 | .PHONY: image-push-all 91 | image-push-all: PUSH_ALL=--push 92 | image-push-all: image-build-all 93 | image-push-all: ## Push the docker image for multiple platforms 94 | 95 | .PHONY: image-run 96 | image-run: ## Launch the docker image 97 | $(RUN_IN_SANDBOX) 98 | 99 | 100 | ## -------------------------------------- 101 | ## Generate 102 | ## -------------------------------------- 103 | 104 | # The command line to invoke ditaa. In the Docker image this 105 | # will be set to "java /ditaa.jar". 106 | DITAA ?= ditaa 107 | 108 | .PHONY: generate-tests 109 | generate-tests: ## Generate the mem tests 110 | cd tests/mem && python3 ../../hack/gen.py 111 | 112 | .PHONY: generate-svgs 113 | generate-svgs: ## Generate the svgs 114 | @find . -name '*.ascii' -type f -print0 | \ 115 | xargs -0n1 $(DITAA) -E -o --background FFFFFF --svg --font-family courier 116 | 117 | 118 | ## -------------------------------------- 119 | ## Lint 120 | ## -------------------------------------- 121 | .PHONY: lint-markdown 122 | lint-markdown: ## Lint the project's markdown 123 | @find . -name "*.md" -type f -print0 | \ 124 | xargs -0 markdownlint -c .markdownlint.yaml 125 | 126 | 127 | ## -------------------------------------- 128 | ## Testing 129 | ## -------------------------------------- 130 | GCFLAGS := -gcflags "-l -N" 131 | 132 | .PHONY: test 133 | test: ## Run tests 134 | go version && go test -count 1 -v -run "^Test" ./... 135 | 136 | .PHONY: test-m 137 | test-lem-m: ## Print optimizations 138 | go version && go test -count 1 -v -c -gcflags -m ./tests/lem 139 | 140 | .PHONY: bench 141 | bench: ## Run benchmarks 142 | go version && \ 143 | go test \ 144 | -v \ 145 | -count 1 \ 146 | -benchtime 1000x \ 147 | -run Mem -benchmem \ 148 | -bench BenchmarkMem \ 149 | ./tests/mem | \ 150 | python3 hack/b2md.py 151 | 152 | .PHONY: asm 153 | asm: ## Print asm table 154 | go version && \ 155 | cd ./tests/mem && \ 156 | go tool compile -S -wb=false *.go | \ 157 | python3 ../../hack/asm2md.py 158 | 159 | 160 | ## -------------------------------------- 161 | ## Clean 162 | ## -------------------------------------- 163 | FILE_EXT_TO_CLEAN := .a .o .out .profile .test 164 | FIND_EXT_TO_CLEAN := $(foreach e,$(FILE_EXT_TO_CLEAN),-name '*$e'$(if $(filter-out $e,$(lastword $(FILE_EXT_TO_CLEAN))), -or,)) 165 | 166 | .PHONY: clean 167 | clean: ## Clean up artifacts 168 | @find . -type f \( $(FIND_EXT_TO_CLEAN) \) -delete 169 | @go clean -i -testcache ./... 170 | 171 | 172 | ## -------------------------------------- 173 | ## Meta 174 | ## -------------------------------------- 175 | # Set up the sandbox targets. 176 | SANDBOX_TARGETS := $(addsuffix -sandbox,$(SANDBOX_TARGETS)) 177 | .PHONY: $(SANDBOX_TARGETS) 178 | $(SANDBOX_TARGETS): 179 | $(RUN_IN_SANDBOX) make $(subst -sandbox,,$@) 180 | 181 | # Set up the docker targets. 182 | DOCKER_TARGETS := $(addsuffix -docker,$(DOCKER_TARGETS)) 183 | .PHONY: $(DOCKER_TARGETS) 184 | $(DOCKER_TARGETS): 185 | $(RUN_IN_DOCKER) make $(subst -docker,,$@) 186 | 187 | -------------------------------------------------------------------------------- /docs/02-interface-values/images/09-on-the-stack-fig1.svg: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | high address 40 | low address 41 | local variables 42 | stack pointer (SP) 43 | top of stack frame 44 | frame pointer 45 | return values 46 | bottom of stack frame 47 | return address (PC) 48 | arguments 49 | name꞉ x 50 | type꞉ int64 51 | size꞉ 8 bytes 52 | local variables 53 | 07 54 | 06 55 | 05 56 | 04 57 | 03 58 | 02 59 | 01 60 | 00 0(SP) 61 | 62 | -------------------------------------------------------------------------------- /hack/asm2md.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | Copyright 2022 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | """ 18 | 19 | """asm2md 20 | 21 | Transforms Go compiler assembly into a markdown table. 22 | 23 | asm2md.py FILE 24 | cat FILE | asm2md.py 25 | """ 26 | 27 | import argparse 28 | import platform 29 | import re 30 | import sys 31 | 32 | parser = argparse.ArgumentParser( 33 | description="asm2md", formatter_class=argparse.ArgumentDefaultsHelpFormatter 34 | ) 35 | 36 | parser.add_argument( 37 | "--no-echo", 38 | dest="no_echo", 39 | action="store_true", 40 | help="do not echo stdin to stdout", 41 | ) 42 | 43 | parser.add_argument( 44 | "file", 45 | metavar="FILE", 46 | nargs="?", 47 | help="path to an input file", 48 | ) 49 | 50 | args = parser.parse_args() 51 | if not args: 52 | parser.print_help() 53 | sys.exit(1) 54 | 55 | 56 | if args.file: 57 | echo = False 58 | f = open(args.file, "r") 59 | else: 60 | echo = not args.no_echo 61 | f = sys.stdin 62 | 63 | rx = re.compile( 64 | r"\((bench_test\.go:(\d+)\))\s(\w?MOV\w*|LEAQ?)\s+\S+\.(_[\w\d_]+)\(SB\).*\1\s(CALL|(?:LEAQ?)|(?:\w?MOV\w*))\s(runtime\.[\w\d]+)\(SB\)", 65 | re.S, # singleline 66 | ) 67 | 68 | 69 | x86_asm_op_links = { 70 | "CALL": "https://www.felixcloutier.com/x86/call", 71 | "LEA": "https://www.felixcloutier.com/x86/lea", 72 | "MOV": "https://www.felixcloutier.com/x86/mov", 73 | } 74 | 75 | arm_asm_op_links = { 76 | "CALL": "https://developer.arm.com/documentation/ddi0602/2021-12/Base-Instructions/BLR--Branch-with-Link-to-Register-?lang=en", 77 | } 78 | 79 | go_func_links = { 80 | "convT16": "https://github.com/golang/go/blob/41f485b9a7d8fd647c415be1d11b612063dff21c/src/runtime/iface.go#L352-L363", 81 | "convT32": "https://github.com/golang/go/blob/41f485b9a7d8fd647c415be1d11b612063dff21c/src/runtime/iface.go#L365-L376", 82 | "convT64": "https://github.com/golang/go/blob/41f485b9a7d8fd647c415be1d11b612063dff21c/src/runtime/iface.go#L378-L386", 83 | "convTstring": "https://github.com/golang/go/blob/41f485b9a7d8fd647c415be1d11b612063dff21c/src/runtime/iface.go#L388-L396", 84 | "convTnoptr": "https://github.com/golang/go/blob/41f485b9a7d8fd647c415be1d11b612063dff21c/src/runtime/iface.go#L335-L350", 85 | "staticuint64s": "https://github.com/golang/go/blob/41f485b9a7d8fd647c415be1d11b612063dff21c/src/runtime/iface.go#L492-L526", 86 | } 87 | 88 | 89 | def get_arm_asm_op_link(s): 90 | key = None 91 | if "CALL" in s: 92 | key = "CALL" 93 | if not key or key not in arm_asm_op_links: 94 | return s 95 | return "[{}]({})".format(s, arm_asm_op_links[key]) 96 | 97 | 98 | def get_x86_asm_op_link(s): 99 | key = None 100 | if "CALL" in s: 101 | key = "CALL" 102 | elif "LEA" in s: 103 | key = "LEA" 104 | if not key or key not in x86_asm_op_links: 105 | return s 106 | return "[{}]({})".format(s, x86_asm_op_links[key]) 107 | 108 | 109 | def get_asm_op_link(s): 110 | if "arm" in platform.processor(): 111 | return get_arm_asm_op_link(s) 112 | return get_x86_asm_op_link(s) 113 | 114 | 115 | def get_go_func_link(s): 116 | key = None 117 | if "convT16" in s: 118 | key = "convT16" 119 | elif "convT32" in s: 120 | key = "convT32" 121 | elif "convT64" in s: 122 | key = "convT64" 123 | elif "convTstring" in s: 124 | key = "convTstring" 125 | elif "convTnoptr" in s: 126 | key = "convTnoptr" 127 | elif "staticuint64s" in s: 128 | key = "staticuint64s" 129 | if not key: 130 | return s 131 | return "[{}]({})".format(s, go_func_links[key]) 132 | 133 | 134 | """ 135 | data = [ 136 | { 137 | "type": "_int64", 138 | 139 | "0": { 140 | "lino": "_test.go:248", 141 | "load_asm": "MOVQ", 142 | "stor_asm": "CALL", 143 | "stor_go": "runtime.convT64", 144 | }, 145 | 146 | # same as "0" 147 | "n": {}, 148 | }, 149 | 150 | # repeat for other types 151 | ] 152 | """ 153 | data = [] 154 | 155 | # Read the entire file at once. I really wanted to avoid this and read it 156 | # a line at a time, but parsing ASM where Go source is not contiguous just 157 | # proved too difficult. This approach enables the use of a single-line regex 158 | # with back references to find what we need. 159 | lines = f.read() 160 | for m in rx.finditer(lines): 161 | 162 | lino = m.group(2) 163 | 164 | # This incredibly inefficient, but I was having trouble getting the correct 165 | # regex to eliminate the blocks of ASM that print the type. 166 | if re.search(r":" + lino + r"\).*go.string.\"real\(T\)=%T\"\(SB\)", lines): 167 | continue 168 | 169 | load_asm = m.group(3) 170 | type = m.group(4) 171 | stor_asm = m.group(5) 172 | stor_go = m.group(6) 173 | 174 | if type.endswith("_n"): 175 | data[len(data) - 1]["n"] = { 176 | "lino": lino, 177 | "load_asm": get_asm_op_link(load_asm), 178 | "stor_asm": get_asm_op_link(stor_asm), 179 | "stor_go": get_go_func_link(stor_go), 180 | } 181 | else: 182 | data.append( 183 | { 184 | "type": type.removeprefix("_"), 185 | "0": { 186 | "lino": lino, 187 | "load_asm": get_asm_op_link(load_asm), 188 | "stor_asm": get_asm_op_link(stor_asm), 189 | "stor_go": get_go_func_link(stor_go), 190 | }, 191 | "n": { 192 | "stor_asm": "NA", 193 | "stor_go": "NA", 194 | }, 195 | } 196 | ) 197 | 198 | if echo: 199 | print(lines) 200 | 201 | # Go ahead and close the file. 202 | f.close() 203 | 204 | print( 205 | "| Type | Line no | Op to store zero value in asm | ...in go | Op to store non-zero, random value in asm | ...in go |" 206 | ) 207 | print( 208 | "|:----:|:-------:|:-----------------------------:|:--------:|:-----------------------------------------:|:--------:|" 209 | ) 210 | 211 | s = "| `{}` | {} | {} | {} | {} | {} |" 212 | for o in data: 213 | print( 214 | s.format( 215 | o["type"], 216 | o["0"]["lino"], 217 | o["0"]["stor_asm"], 218 | o["0"]["stor_go"], 219 | o["n"]["stor_asm"], 220 | o["n"]["stor_go"], 221 | ) 222 | ) 223 | -------------------------------------------------------------------------------- /tests/mem/types_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // 18 | // !! Generated code -- do not modify !! 19 | // 20 | 21 | package mem_test 22 | 23 | var ( 24 | _int int 25 | _int8 int8 26 | _int16 int16 27 | _int32 int32 28 | _int64 int64 29 | 30 | _uint uint 31 | _uint8 uint8 32 | _uint16 uint16 33 | _uint32 uint32 34 | _uint64 uint64 35 | 36 | _float32 float32 37 | _float64 float64 38 | 39 | _complex64 complex64 40 | _complex128 complex128 41 | 42 | _byte byte 43 | _bool bool 44 | _rune rune 45 | _string string 46 | 47 | _struct_int struct{ a int } 48 | _struct_int8 struct{ a int8 } 49 | _struct_int16 struct{ a int16 } 50 | _struct_int32 struct{ a int32 } 51 | _struct_int64 struct{ a int64 } 52 | 53 | _struct_uint struct{ a uint } 54 | _struct_uint8 struct{ a uint8 } 55 | _struct_uint16 struct{ a uint16 } 56 | _struct_uint32 struct{ a uint32 } 57 | _struct_uint64 struct{ a uint64 } 58 | 59 | _struct_float32 struct{ a float32 } 60 | _struct_float64 struct{ a float64 } 61 | 62 | _struct_complex64 struct{ a complex64 } 63 | _struct_complex128 struct{ a complex128 } 64 | 65 | _struct_byte struct{ a byte } 66 | _struct_bool struct{ a bool } 67 | _struct_rune struct{ a rune } 68 | _struct_string struct{ a string } 69 | 70 | _struct_int32_int32 struct{ a, b int32 } 71 | _struct_int32_int64 struct { 72 | a int32 73 | b int64 74 | } 75 | _struct_array_bytes_7 struct{ a [7]byte } 76 | _struct_byte_7 struct{ a, b, c, d, e, f, g byte } 77 | 78 | _int_n int = nonZeroRandInt(_int_size) 79 | _int8_n int8 = int8(nonZeroRandInt(8)) 80 | _int16_n int16 = int16(nonZeroRandInt(16)) 81 | _int32_n int32 = int32(nonZeroRandInt(32)) 82 | _int64_n int64 = int64(nonZeroRandInt(64)) 83 | 84 | _uint_n uint = uint(nonZeroRandInt(_int_size)) 85 | _uint8_n uint8 = uint8(nonZeroRandInt(8)) 86 | _uint16_n uint16 = uint16(nonZeroRandInt(16)) 87 | _uint32_n uint32 = uint32(nonZeroRandInt(32)) 88 | _uint64_n uint64 = uint64(nonZeroRandInt(64)) 89 | 90 | _float32_n float32 = float32(nonZeroRandInt(32)) 91 | _float64_n float64 = float64(nonZeroRandInt(64)) 92 | 93 | _complex64_n complex64 = complex(float32(nonZeroRandInt(32)), float32(nonZeroRandInt(32))) 94 | _complex128_n complex128 = complex(float64(nonZeroRandInt(64)), float64(nonZeroRandInt(64))) 95 | 96 | _byte_n byte = byte(nonZeroRandInt(8)) 97 | _bool_n bool = nonConstBoolTrue() 98 | _rune_n rune = rune(nonZeroRandInt(32)) 99 | _string_n string = nonZeroString(50) 100 | 101 | _struct_int_n = struct{ a int }{a: nonZeroRandInt(_int_size)} 102 | _struct_int8_n = struct{ a int8 }{a: int8(nonZeroRandInt(8))} 103 | _struct_int16_n = struct{ a int16 }{a: int16(nonZeroRandInt(16))} 104 | _struct_int32_n = struct{ a int32 }{a: int32(nonZeroRandInt(32))} 105 | _struct_int64_n = struct{ a int64 }{a: int64(nonZeroRandInt(64))} 106 | 107 | _struct_uint_n = struct{ a uint }{a: uint(nonZeroRandInt(_int_size))} 108 | _struct_uint8_n = struct{ a uint8 }{a: uint8(nonZeroRandInt(8))} 109 | _struct_uint16_n = struct{ a uint16 }{a: uint16(nonZeroRandInt(16))} 110 | _struct_uint32_n = struct{ a uint32 }{a: uint32(nonZeroRandInt(32))} 111 | _struct_uint64_n = struct{ a uint64 }{a: uint64(nonZeroRandInt(64))} 112 | 113 | _struct_float32_n = struct{ a float32 }{a: float32(nonZeroRandInt(32))} 114 | _struct_float64_n = struct{ a float64 }{a: float64(nonZeroRandInt(64))} 115 | 116 | _struct_complex64_n = struct{ a complex64 }{a: complex(float32(nonZeroRandInt(32)), float32(nonZeroRandInt(32)))} 117 | _struct_complex128_n = struct{ a complex128 }{a: complex(float64(nonZeroRandInt(64)), float64(nonZeroRandInt(64)))} 118 | 119 | _struct_byte_n = struct{ a byte }{a: byte(nonZeroRandInt(8))} 120 | _struct_bool_n = struct{ a bool }{a: nonConstBoolTrue()} 121 | _struct_rune_n = struct{ a rune }{a: rune(nonZeroRandInt(32))} 122 | _struct_string_n = struct{ a string }{a: nonZeroString(50)} 123 | 124 | _struct_int32_int32_n = struct{ a, b int32 }{a: int32(nonZeroRandInt(32)), b: int32(nonZeroRandInt(32))} 125 | _struct_int32_int64_n = struct { 126 | a int32 127 | b int64 128 | }{a: int32(nonZeroRandInt(32)), b: int64(nonZeroRandInt(64))} 129 | _struct_array_bytes_7_n = struct{ a [7]byte }{a: [7]byte{byte(nonZeroRandInt(8)), byte(nonZeroRandInt(8)), byte(nonZeroRandInt(8)), byte(nonZeroRandInt(8)), byte(nonZeroRandInt(8)), byte(nonZeroRandInt(8)), byte(nonZeroRandInt(8))}} 130 | _struct_byte_7_n = struct{ a, b, c, d, e, f, g byte }{a: byte(nonZeroRandInt(8)), b: byte(nonZeroRandInt(8)), c: byte(nonZeroRandInt(8)), d: byte(nonZeroRandInt(8)), e: byte(nonZeroRandInt(8)), f: byte(nonZeroRandInt(8)), g: byte(nonZeroRandInt(8))} 131 | 132 | _int_benchmark int 133 | _int8_benchmark int8 134 | _int16_benchmark int16 135 | _int32_benchmark int32 136 | _int64_benchmark int64 137 | 138 | _uint_benchmark uint 139 | _uint8_benchmark uint8 140 | _uint16_benchmark uint16 141 | _uint32_benchmark uint32 142 | _uint64_benchmark uint64 143 | 144 | _float32_benchmark float32 145 | _float64_benchmark float64 146 | 147 | _complex64_benchmark complex64 148 | _complex128_benchmark complex128 149 | 150 | _byte_benchmark byte 151 | _bool_benchmark bool 152 | _rune_benchmark rune 153 | _string_benchmark string 154 | 155 | _struct_int_benchmark struct{ a int } 156 | _struct_int8_benchmark struct{ a int8 } 157 | _struct_int16_benchmark struct{ a int16 } 158 | _struct_int32_benchmark struct{ a int32 } 159 | _struct_int64_benchmark struct{ a int64 } 160 | 161 | _struct_uint_benchmark struct{ a uint } 162 | _struct_uint8_benchmark struct{ a uint8 } 163 | _struct_uint16_benchmark struct{ a uint16 } 164 | _struct_uint32_benchmark struct{ a uint32 } 165 | _struct_uint64_benchmark struct{ a uint64 } 166 | 167 | _struct_float32_benchmark struct{ a float32 } 168 | _struct_float64_benchmark struct{ a float64 } 169 | 170 | _struct_complex64_benchmark struct{ a complex64 } 171 | _struct_complex128_benchmark struct{ a complex128 } 172 | 173 | _struct_byte_benchmark struct{ a byte } 174 | _struct_bool_benchmark struct{ a bool } 175 | _struct_rune_benchmark struct{ a rune } 176 | _struct_string_benchmark struct{ a string } 177 | 178 | _struct_int32_int32_benchmark struct{ a, b int32 } 179 | _struct_int32_int64_benchmark struct { 180 | a int32 181 | b int64 182 | } 183 | _struct_array_bytes_7_benchmark struct{ a [7]byte } 184 | _struct_byte_7_benchmark struct{ a, b, c, d, e, f, g byte } 185 | ) 186 | -------------------------------------------------------------------------------- /docs/99-appendix/two-integers.md: -------------------------------------------------------------------------------- 1 | # Two integers 2 | 3 | This page describes how two integer values are allocated on the stack by walking assembly language. The example on this page is based on the source code in [xint2yint.go](../../examples/xint2yint.go): 4 | 5 | ```go 6 | /* line 17 */ package examples 7 | /* line 18 */ 8 | /* line 19 */ func xint2yint() { 9 | /* line 20 */ var x int64 10 | /* line 21 */ var y int64 11 | /* line 22 */ x = 2 12 | /* line 23 */ y = x 13 | /* line 24 */ _ = y 14 | /* line 25 */ } 15 | ``` 16 | 17 | With that in mind, let's get started: 18 | 19 | 1. Build the `examples` package with compiler flags to prevent write barriers (`-wb=false`), inlining (`-l`), and optimization (`-N`). You would never do this in producton, but it makes walking the assembly easier: 20 | 21 | ```bash 22 | go build -gcflags "-wb=false -l -N" -o examples.a ./examples 23 | ``` 24 | 25 | 1. Dump the symbol `xint2yint$` from the newly built archive: 26 | 27 | ```bash 28 | go tool objdump -s xint2yint$ examples.a 29 | ``` 30 | 31 | --- 32 | 33 | :wave: **Alternative assembly** 34 | 35 | Please note it is also possible to dump the assembly for a single Go source file: 36 | 37 | ```bash 38 | go tool compile -wb=false -l -N -S ./examples/xint2yint.go 39 | ``` 40 | 41 | However I [have found](https://gophers.slack.com/archives/C029RQSEE/p1644033676178239) the Go compiler will produce different assembly based on `go tool compile` and actually packing the archive with `go build`. In order to be more aligned with package archive assembly, this page uses `go build`. 42 | 43 | --- 44 | 45 | 3. The resulting output depends on the platform. The following was produced on darwin/amd64: 46 | 47 | ```assembly 48 | TEXT go-interface-values/examples.xint2yint(SB) gofile../Users/akutz/Projects/go-interface-values/examples/xint2yint.go 49 | xint2yint.go:19 0x2c2a 4883ec18 SUBQ $0x18, SP 50 | xint2yint.go:19 0x2c2e 48896c2410 MOVQ BP, 0x10(SP) 51 | xint2yint.go:19 0x2c33 488d6c2410 LEAQ 0x10(SP), BP 52 | xint2yint.go:20 0x2c38 48c744240800000000 MOVQ $0x0, 0x8(SP) 53 | xint2yint.go:21 0x2c41 48c7042400000000 MOVQ $0x0, 0(SP) 54 | xint2yint.go:22 0x2c49 48c744240802000000 MOVQ $0x2, 0x8(SP) 55 | xint2yint.go:23 0x2c52 48c7042402000000 MOVQ $0x2, 0(SP) 56 | xint2yint.go:25 0x2c5a 488b6c2410 MOVQ 0x10(SP), BP 57 | xint2yint.go:25 0x2c5f 4883c418 ADDQ $0x18, SP 58 | xint2yint.go:25 0x2c63 c3 RET 59 | ``` 60 | 61 | We want to focus on four lines in particular: 62 | 63 | ```assembly 64 | xint2yint.go:20 0x2c38 48c744240800000000 MOVQ $0x0, 0x8(SP) 65 | xint2yint.go:21 0x2c41 48c7042400000000 MOVQ $0x0, 0(SP) 66 | xint2yint.go:22 0x2c49 48c744240802000000 MOVQ $0x2, 0x8(SP) 67 | xint2yint.go:23 0x2c52 48c7042402000000 MOVQ $0x2, 0(SP) 68 | ``` 69 | 70 | Let's take it line by line. 71 | 72 | 1. `xint2yint.go:20 0x2c38 48c744240800000000 MOVQ $0x0, 0x8(SP)` 73 | * `xint2yint.go:20` 74 | * This is the file and line number of the source code that corresponds to this line of assembly. 75 | * In this case it is line 20 from the file `xint2yint.go` -- `var x int64`. 76 | * `0x2c38` 77 | * The program counter formatted as hexadecimal. 78 | * GNU's `objdump` tool formats this value as hexadecimal as well, but without the leading prefix `0x`. 79 | * `48c744240800000000` 80 | * The executable instruction formatted as hexadecimal. 81 | * GNU's `objdump` tool formats this value as hexadecimal as well, but with spaces, ex. `48 c7 44 24 08 00 00 00 00`. 82 | * `MOVQ $0x0, 0x8(SP)` 83 | * The instruction `MOVQ` copies the value from one address to another, ex. `MOVQ SRC, DST`. 84 | * `MOVQ` 85 | * Normally `MOVQ` operates right to left, `MOVQ DST SRC`, but as the [Go assembly documentation](https://go.dev/doc/asm) states: 86 | 87 | > One detail evident in the examples from the previous sections is that data in the instructions flows from left to right: MOVQ $0, CX clears CX. This rule applies even on architectures where the conventional notation uses the opposite direction. 88 | * The `Q` in `MOVQ` stands for _quadword_: 89 | * On x86 and x86_64 platforms a _word_ is 16 bits. 90 | * Because this example is from a 64-bit system, a _quadword_ is 16x4, or...64 bits. 91 | * `$0x0` 92 | * The `SRC` of the copy operation. 93 | * The leading `$` indicates `SRC` is not a memory address, but a literal value. 94 | * The value to copy is therefore `0x0`, or the integer value `0`. 95 | * `0x8(SP)` 96 | * The `DST` of the copy operation. 97 | * The `0x8` indicates an offset of eight bytes from some address. 98 | * The address is indicated by `(SP)`, _stack pointer_, the highest memory address of the current stack frame. 99 | * Therefore `0x8(SP)` can be translated as _eight bytes from the highest memory address of the current strack frame_. 100 | 101 |
102 | 103 | ``` 104 | SP +----------------+ SP + 0 bytes 105 | | | 106 | | | 107 | | | 108 | | | 109 | +----------------+ SP + 8 bytes 110 | | name: x | 111 | | type: int64 | 112 | | size: 8 bytes | 113 | | value: 0 | 114 | +----------------+ 115 | ``` 116 | 117 | 118 | 1. `xint2yint.go:21 0x2c41 48c7042400000000 MOVQ $0x0, 0(SP)` 119 | * The assembly for `var y int64`. 120 | * Copies the literal value `0` to the address `0(SP)`. 121 | * Please note that `0(SP)` could also be written as `0x0(SP)`. 122 | * Therefore `0(SP)` can be translated as `_zero bytes from the highest memory address of the current stack frame_. 123 | 124 |
125 | 126 | ``` 127 | SP +----------------+ SP + 0 bytes 128 | | name: y | 129 | | type: int64 | 130 | | size: 8 bytes | 131 | | value: 0 | 132 | +----------------+ SP + 8 bytes 133 | | name: x | 134 | | type: int64 | 135 | | size: 8 bytes | 136 | | value: 0 | 137 | +----------------+ 138 | ``` 139 | 140 | 4. `xint2yint.go:22 0x2c49 48c744240802000000 MOVQ $0x2, 0x8(SP)` 141 | * The assembly for `x = 2` 142 | * `MOVQ $0x2, 0x8(SP)` copies the literal value `2` to the memory address for the variable `x`. 143 | 144 |
145 | 146 | ``` 147 | SP +----------------+ SP + 0 bytes 148 | | name: y | 149 | | type: int64 | 150 | | size: 8 bytes | 151 | | value: 0 | 152 | +----------------+ SP + 8 bytes 153 | | name: x | 154 | | type: int64 | 155 | | size: 8 bytes | 156 | | value: 2 | 157 | +----------------+ 158 | ``` 159 | 160 | 4. `xint2yint.go:23 0x2c52 48c7042402000000 MOVQ $0x2, 0(SP)` 161 | * The assembly for `y = x` 162 | * `MOVQ $0x2, 0(SP)` copies the literal value `2` to the memory address for the variable `y`. 163 | * Note the compiler, even with optimizations disabled, elected to copy the literal `$0x2` to the address of `y` and not use `MOVQ 0x8(SP) SP` to copy `x` to `y`. 164 | 165 |
166 | 167 | ``` 168 | SP +----------------+ SP + 0 bytes 169 | | name: y | 170 | | type: int64 | 171 | | size: 8 bytes | 172 | | value: 2 | 173 | +----------------+ SP + 8 bytes 174 | | name: x | 175 | | type: int64 | 176 | | size: 8 bytes | 177 | | value: 2 | 178 | +----------------+ 179 | ``` 180 | 181 | In other words, the stack is large enough for two, 64-bit integers. When values are assigned to them, those values are just copied to that variable's address on the stack. 182 | -------------------------------------------------------------------------------- /tests/lem/lem_escape_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package lem_test 18 | 19 | import ( 20 | "testing" 21 | ) 22 | 23 | // lem.escape1.name=pointer outlived its call stack 24 | // lem.escape1.alloc=1 25 | // lem.escape1.bytes=4 26 | func escape1(b *testing.B) { 27 | var sink *int32 28 | f1 := func() *int32 { 29 | return new(int32) // lem.escape1.m=new\(int32\) escapes to heap 30 | } 31 | f2 := func(p *int32) *int32 { // lem.escape1.m=leaking param: p to result ~r[0-1] level=0 32 | return p 33 | } 34 | b.ResetTimer() 35 | for i := 0; i < b.N; i++ { 36 | sink = (f2(f1())) // lem.escape1.m=new\(int32\) escapes to heap 37 | } 38 | _ = sink 39 | } 40 | func init() { 41 | lemFuncs["escape1"] = escape1 42 | } 43 | 44 | // lem.escape2.name=heap cannot point to stack 45 | // lem.escape2.alloc=1 46 | // lem.escape2.bytes=4 47 | func escape2(b *testing.B) { 48 | var sink *int32 49 | f := func(p *int32) *int32 { // lem.escape1.m=leaking param: p to result ~r[0-1] level=0 50 | return p 51 | } 52 | b.ResetTimer() 53 | for i := 0; i < b.N; i++ { 54 | sink = f(new(int32)) // lem.escape1.m=new\(int32\) escapes to heap 55 | } 56 | _ = sink 57 | } 58 | func init() { 59 | lemFuncs["escape2"] = escape2 60 | } 61 | 62 | // lem.escape3.name=no malloc bc iface & zero value 63 | // lem.escape3.alloc=0 64 | // lem.escape3.bytes=0 65 | func escape3(b *testing.B) { 66 | var sink interface{} 67 | b.ResetTimer() 68 | for i := 0; i < b.N; i++ { 69 | var x int32 70 | sink = x // lem.escape3.m=x escapes to heap 71 | } 72 | _ = sink 73 | } 74 | func init() { 75 | lemFuncs["escape3"] = escape3 76 | } 77 | 78 | // lem.escape4.name=no malloc bc storing byte value in iface 79 | // lem.escape4.alloc=0 80 | // lem.escape4.bytes=0 81 | func escape4(b *testing.B) { 82 | var sink interface{} 83 | b.ResetTimer() 84 | for i := 0; i < b.N; i++ { 85 | var x byte 86 | x = 253 87 | sink = x // lem.escape4.m=x escapes to heap 88 | } 89 | _ = sink 90 | } 91 | func init() { 92 | lemFuncs["escape4"] = escape4 93 | } 94 | 95 | // lem.escape5.name=no malloc bc storing single byte-wide value in iface 96 | // lem.escape5.alloc=0 97 | // lem.escape5.bytes=0 98 | func escape5(b *testing.B) { 99 | var sink interface{} 100 | b.ResetTimer() 101 | for i := 0; i < b.N; i++ { 102 | var x int64 103 | x = 253 104 | sink = x // lem.escape5.m=x escapes to heap 105 | } 106 | _ = sink 107 | } 108 | func init() { 109 | lemFuncs["escape5"] = escape5 110 | } 111 | 112 | // lem.escape6.name=no malloc bc storing struct w single byte-wide value in iface 113 | // lem.escape6.alloc=0 114 | // lem.escape6.bytes=0 115 | func escape6(b *testing.B) { 116 | var sink interface{} 117 | b.ResetTimer() 118 | for i := 0; i < b.N; i++ { 119 | var s struct{ a int32 } 120 | s.a = 253 121 | sink = s // lem.escape6.m=s escapes to heap 122 | } 123 | _ = sink 124 | } 125 | func init() { 126 | lemFuncs["escape6"] = escape6 127 | } 128 | 129 | // lem.escape7.name=malloc bc storing struct w field value >255 in iface 130 | // lem.escape7.alloc=1 131 | // lem.escape7.bytes=4 132 | func escape7(b *testing.B) { 133 | var sink interface{} 134 | b.ResetTimer() 135 | for i := 0; i < b.N; i++ { 136 | var s struct{ a int32 } 137 | s.a = 256 138 | sink = s // lem.escape7.m=s escapes to heap 139 | } 140 | _ = sink 141 | } 142 | func init() { 143 | lemFuncs["escape7"] = escape7 144 | } 145 | 146 | // lem.escape8.name=malloc bc storing values >255 in iface 147 | // lem.escape8.alloc=2 148 | // lem.escape8.bytes=16 149 | func escape8(b *testing.B) { 150 | var sink interface{} 151 | b.ResetTimer() 152 | for i := 0; i < b.N; i++ { 153 | var x int16 154 | var y int64 155 | x = 256 156 | y = 256 157 | sink = x // lem.escape8.m=x escapes to heap 158 | sink = y // lem.escape8.m=y escapes to heap 159 | } 160 | _ = sink 161 | } 162 | func init() { 163 | lemFuncs["escape8"] = escape8 164 | } 165 | 166 | // lem.escape9.name=malloc bc storing values >255 in iface; 16 bytes bc alignment 167 | // lem.escape9.alloc=2 168 | // lem.escape9.bytes=16 169 | func escape9(b *testing.B) { 170 | var sink interface{} 171 | b.ResetTimer() 172 | for i := 0; i < b.N; i++ { 173 | var x int32 174 | var y int64 175 | x = 256 176 | y = 256 177 | sink = x // lem.escape9.m=x escapes to heap 178 | sink = y // lem.escape9.m=y escapes to heap 179 | } 180 | _ = sink 181 | } 182 | func init() { 183 | lemFuncs["escape9"] = escape9 184 | } 185 | 186 | // lem.escape10.name=malloc bc storing value >255 in iface 187 | // lem.escape10.alloc=1 188 | // lem.escape10.bytes=8 189 | func escape10(b *testing.B) { 190 | var sink interface{} 191 | b.ResetTimer() 192 | for i := 0; i < b.N; i++ { 193 | var x int32 194 | var y int64 195 | x = 255 196 | y = 256 197 | sink = x // lem.escape10.m=x escapes to heap 198 | sink = y // lem.escape10.m=y escapes to heap 199 | } 200 | _ = sink 201 | } 202 | func init() { 203 | lemFuncs["escape10"] = escape10 204 | } 205 | 206 | // lem.escape11.name=malloc bc zero value trick only works when storing value in iface 207 | // lem.escape11.alloc=1 208 | // lem.escape11.bytes=8 209 | func escape11(b *testing.B) { 210 | var sink *int64 211 | b.ResetTimer() 212 | for i := 0; i < b.N; i++ { 213 | x := new(int64) // lem.escape11.m=new\(int64\) escapes to heap 214 | *x = 0 215 | sink = x 216 | } 217 | _ = sink 218 | } 219 | func init() { 220 | lemFuncs["escape11"] = escape11 221 | } 222 | 223 | // lem.escape12.name=five ref params to result w one malloc for stored return value 224 | // lem.escape12.alloc=1 225 | // lem.escape12.bytes=4 226 | func escape12(b *testing.B) { 227 | type s struct{ a, b int32 } 228 | noop := func( 229 | a *int32, // lem.escape12.m=leaking param: a to result ~r[06] level=0 230 | b *int64, // lem.escape12.m=leaking param: b to result ~r[17] level=0 231 | c []int32, // lem.escape12.m=leaking param: c to result ~r[28] level=0 232 | d []*int64, // lem.escape12.m=leaking param: d to result ~r[39] level=0 233 | e s, // lem.escape12.m!=(escapes|leaking|moved) 234 | f *s, // lem.escape12.m=leaking param: f to result ~r(5|11) level=0 235 | ) (*int32, *int64, []int32, []*int64, s, *s) { 236 | return a, b, c, d, e, f 237 | } 238 | var sink *int32 239 | b.ResetTimer() 240 | for i := 0; i < b.N; i++ { 241 | var ( 242 | a = new(int32) // lem.escape12.m=new\(int32\) escapes to heap 243 | b = new(int64) // lem.escape12.m=new\(int64\) does not escape 244 | c = make([]int32, 5, 5) // lem.escape12.m=make\(\[\]int32, 5, 5\) does not escape 245 | d = make([]*int64, 10, 10) // lem.escape12.m=make\(\[\]\*int64, 10, 10\) does not escape 246 | e = s{a: 4096, b: 4096} // lem.escape12.m!=(escapes|leaking|moved) 247 | f = new(s) // lem.escape12.m=new\(s\) does not escape 248 | ) 249 | sink, _, _, _, _, _ = noop(a, b, c, d, e, f) 250 | } 251 | _ = sink 252 | } 253 | func init() { 254 | lemFuncs["escape12"] = escape12 255 | } 256 | 257 | // lem.escape13.name=return value was captured & outlived stack frame 258 | // lem.escape13.alloc=1 259 | // lem.escape13.bytes=4 260 | func escape13(b *testing.B) { 261 | f := func(p *int32) *int32 { // lem.escape13.m=leaking param: p to result ~r[0-1] level=0 262 | return p 263 | } 264 | var sink *int32 265 | b.ResetTimer() 266 | for i := 0; i < b.N; i++ { 267 | x := new(int32) // lem.escape13.m=new\(int32\) escapes to heap 268 | *x = 4096 269 | if sink = f(x); sink == nil { 270 | // Do nothing 271 | } 272 | _ = x 273 | } 274 | _ = sink 275 | } 276 | func init() { 277 | lemFuncs["escape13"] = escape13 278 | } 279 | 280 | // lem.escape14.name=malloc w storing <256 in interface bc float32 281 | // lem.escape14.alloc=1 282 | // lem.escape14.bytes=4 283 | func escape14(b *testing.B) { 284 | var sink interface{} 285 | b.ResetTimer() 286 | for i := 0; i < b.N; i++ { 287 | var x float32 288 | x = 253 289 | sink = x // lem.escape14.m=x escapes to heap 290 | } 291 | _ = sink 292 | } 293 | func init() { 294 | lemFuncs["escape14"] = escape14 295 | } 296 | 297 | // lem.escape15.name=malloc w storing <256 in interface bc float64 298 | // lem.escape15.alloc=1 299 | // lem.escape15.bytes=8 300 | func escape15(b *testing.B) { 301 | var sink interface{} 302 | b.ResetTimer() 303 | for i := 0; i < b.N; i++ { 304 | var x float64 305 | x = 253 306 | sink = x // lem.escape15.m=x escapes to heap 307 | } 308 | _ = sink 309 | } 310 | func init() { 311 | lemFuncs["escape15"] = escape15 312 | } 313 | -------------------------------------------------------------------------------- /docs/04-missing-mallocs/images/02-why-fig1.svg: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | stack pointer (SP) 50 | top of stack frame 51 | local variables 52 | return address (PC) 53 | frame pointer 54 | return values 55 | arguments 56 | bottom of stack frame 57 | low address 58 | high address 59 | AX 60 | registers 61 | CX 62 | name꞉ x 63 | type꞉ int64 64 | size꞉ 8 bytes 65 | sink 66 | staticuint64s 67 | 01 02 03 04 05 06 07 08 09 10 11.. 68 | local variables 69 | type꞉ 70 | valu꞉ 71 | data 72 | 07 73 | 06 74 | 05 75 | 04 76 | 03 77 | 02 78 | 01 79 | 00 0(SP) 80 | 81 | -------------------------------------------------------------------------------- /docs/04-missing-mallocs/images/02-why-fig2.svg: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | stack pointer (SP) 50 | top of stack frame 51 | local variables 52 | return address (PC) 53 | frame pointer 54 | return values 55 | arguments 56 | bottom of stack frame 57 | low address 58 | high address 59 | AX 60 | registers 61 | CX 62 | name꞉ x 63 | type꞉ int64 64 | size꞉ 8 bytes 65 | valu꞉ 2 66 | sink 67 | staticuint64s 68 | 01 02 03 04 05 06 07 08 09 10 11.. 69 | local variables 70 | type꞉ 71 | valu꞉ 72 | data 73 | 07 74 | 06 75 | 05 76 | 04 77 | 03 78 | 02 79 | 01 80 | 00 0(SP) 81 | 82 | -------------------------------------------------------------------------------- /docs/04-missing-mallocs/images/02-why-fig3.svg: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | stack pointer (SP) 50 | top of stack frame 51 | local variables 52 | return address (PC) 53 | frame pointer 54 | return values 55 | arguments 56 | bottom of stack frame 57 | low address 58 | high address 59 | 2 60 | AX 61 | registers 62 | CX 63 | name꞉ x 64 | type꞉ int64 65 | size꞉ 8 bytes 66 | valu꞉ 2 67 | sink 68 | staticuint64s 69 | 01 02 03 04 05 06 07 08 09 10 11.. 70 | local variables 71 | type꞉ 72 | valu꞉ 73 | data 74 | 07 75 | 06 76 | 05 77 | 04 78 | 03 79 | 02 80 | 01 81 | 00 0(SP) 82 | 83 | --------------------------------------------------------------------------------