├── LICENSE ├── Makefile ├── README.md ├── chapter1_assembly_primer ├── Makefile ├── README.md └── direct_topfunc_call.go ├── chapter2_interfaces ├── Makefile ├── README.md ├── compound_interface.go ├── direct_calls.go ├── dump_sym.sh ├── eface_scalar_test.go ├── eface_to_type.go ├── eface_to_type_test.go ├── eface_type_hash.go ├── escape.go ├── escape_test.go ├── iface.go ├── iface_bench_test.go ├── iface_type_hash.go ├── zerobase.go └── zeroval.go └── chapter3_garbage_collector └── README.md /LICENSE: -------------------------------------------------------------------------------- 1 | Attribution-NonCommercial-ShareAlike 4.0 International 2 | 3 | ======================================================================= 4 | 5 | Creative Commons Corporation ("Creative Commons") is not a law firm and 6 | does not provide legal services or legal advice. Distribution of 7 | Creative Commons public licenses does not create a lawyer-client or 8 | other relationship. Creative Commons makes its licenses and related 9 | information available on an "as-is" basis. Creative Commons gives no 10 | warranties regarding its licenses, any material licensed under their 11 | terms and conditions, or any related information. Creative Commons 12 | disclaims all liability for damages resulting from their use to the 13 | fullest extent possible. 14 | 15 | Using Creative Commons Public Licenses 16 | 17 | Creative Commons public licenses provide a standard set of terms and 18 | conditions that creators and other rights holders may use to share 19 | original works of authorship and other material subject to copyright 20 | and certain other rights specified in the public license below. The 21 | following considerations are for informational purposes only, are not 22 | exhaustive, and do not form part of our licenses. 23 | 24 | Considerations for licensors: Our public licenses are 25 | intended for use by those authorized to give the public 26 | permission to use material in ways otherwise restricted by 27 | copyright and certain other rights. Our licenses are 28 | irrevocable. Licensors should read and understand the terms 29 | and conditions of the license they choose before applying it. 30 | Licensors should also secure all rights necessary before 31 | applying our licenses so that the public can reuse the 32 | material as expected. Licensors should clearly mark any 33 | material not subject to the license. This includes other CC- 34 | licensed material, or material used under an exception or 35 | limitation to copyright. More considerations for licensors: 36 | wiki.creativecommons.org/Considerations_for_licensors 37 | 38 | Considerations for the public: By using one of our public 39 | licenses, a licensor grants the public permission to use the 40 | licensed material under specified terms and conditions. If 41 | the licensor's permission is not necessary for any reason--for 42 | example, because of any applicable exception or limitation to 43 | copyright--then that use is not regulated by the license. Our 44 | licenses grant only permissions under copyright and certain 45 | other rights that a licensor has authority to grant. Use of 46 | the licensed material may still be restricted for other 47 | reasons, including because others have copyright or other 48 | rights in the material. A licensor may make special requests, 49 | such as asking that all changes be marked or described. 50 | Although not required by our licenses, you are encouraged to 51 | respect those requests where reasonable. More_considerations 52 | for the public: 53 | wiki.creativecommons.org/Considerations_for_licensees 54 | 55 | ======================================================================= 56 | 57 | Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International 58 | Public License 59 | 60 | By exercising the Licensed Rights (defined below), You accept and agree 61 | to be bound by the terms and conditions of this Creative Commons 62 | Attribution-NonCommercial-ShareAlike 4.0 International Public License 63 | ("Public License"). To the extent this Public License may be 64 | interpreted as a contract, You are granted the Licensed Rights in 65 | consideration of Your acceptance of these terms and conditions, and the 66 | Licensor grants You such rights in consideration of benefits the 67 | Licensor receives from making the Licensed Material available under 68 | these terms and conditions. 69 | 70 | 71 | Section 1 -- Definitions. 72 | 73 | a. Adapted Material means material subject to Copyright and Similar 74 | Rights that is derived from or based upon the Licensed Material 75 | and in which the Licensed Material is translated, altered, 76 | arranged, transformed, or otherwise modified in a manner requiring 77 | permission under the Copyright and Similar Rights held by the 78 | Licensor. For purposes of this Public License, where the Licensed 79 | Material is a musical work, performance, or sound recording, 80 | Adapted Material is always produced where the Licensed Material is 81 | synched in timed relation with a moving image. 82 | 83 | b. Adapter's License means the license You apply to Your Copyright 84 | and Similar Rights in Your contributions to Adapted Material in 85 | accordance with the terms and conditions of this Public License. 86 | 87 | c. BY-NC-SA Compatible License means a license listed at 88 | creativecommons.org/compatiblelicenses, approved by Creative 89 | Commons as essentially the equivalent of this Public License. 90 | 91 | d. Copyright and Similar Rights means copyright and/or similar rights 92 | closely related to copyright including, without limitation, 93 | performance, broadcast, sound recording, and Sui Generis Database 94 | Rights, without regard to how the rights are labeled or 95 | categorized. For purposes of this Public License, the rights 96 | specified in Section 2(b)(1)-(2) are not Copyright and Similar 97 | Rights. 98 | 99 | e. Effective Technological Measures means those measures that, in the 100 | absence of proper authority, may not be circumvented under laws 101 | fulfilling obligations under Article 11 of the WIPO Copyright 102 | Treaty adopted on December 20, 1996, and/or similar international 103 | agreements. 104 | 105 | f. Exceptions and Limitations means fair use, fair dealing, and/or 106 | any other exception or limitation to Copyright and Similar Rights 107 | that applies to Your use of the Licensed Material. 108 | 109 | g. License Elements means the license attributes listed in the name 110 | of a Creative Commons Public License. The License Elements of this 111 | Public License are Attribution, NonCommercial, and ShareAlike. 112 | 113 | h. Licensed Material means the artistic or literary work, database, 114 | or other material to which the Licensor applied this Public 115 | License. 116 | 117 | i. Licensed Rights means the rights granted to You subject to the 118 | terms and conditions of this Public License, which are limited to 119 | all Copyright and Similar Rights that apply to Your use of the 120 | Licensed Material and that the Licensor has authority to license. 121 | 122 | j. Licensor means the individual(s) or entity(ies) granting rights 123 | under this Public License. 124 | 125 | k. NonCommercial means not primarily intended for or directed towards 126 | commercial advantage or monetary compensation. For purposes of 127 | this Public License, the exchange of the Licensed Material for 128 | other material subject to Copyright and Similar Rights by digital 129 | file-sharing or similar means is NonCommercial provided there is 130 | no payment of monetary compensation in connection with the 131 | exchange. 132 | 133 | l. Share means to provide material to the public by any means or 134 | process that requires permission under the Licensed Rights, such 135 | as reproduction, public display, public performance, distribution, 136 | dissemination, communication, or importation, and to make material 137 | available to the public including in ways that members of the 138 | public may access the material from a place and at a time 139 | individually chosen by them. 140 | 141 | m. Sui Generis Database Rights means rights other than copyright 142 | resulting from Directive 96/9/EC of the European Parliament and of 143 | the Council of 11 March 1996 on the legal protection of databases, 144 | as amended and/or succeeded, as well as other essentially 145 | equivalent rights anywhere in the world. 146 | 147 | n. You means the individual or entity exercising the Licensed Rights 148 | under this Public License. Your has a corresponding meaning. 149 | 150 | 151 | Section 2 -- Scope. 152 | 153 | a. License grant. 154 | 155 | 1. Subject to the terms and conditions of this Public License, 156 | the Licensor hereby grants You a worldwide, royalty-free, 157 | non-sublicensable, non-exclusive, irrevocable license to 158 | exercise the Licensed Rights in the Licensed Material to: 159 | 160 | a. reproduce and Share the Licensed Material, in whole or 161 | in part, for NonCommercial purposes only; and 162 | 163 | b. produce, reproduce, and Share Adapted Material for 164 | NonCommercial purposes only. 165 | 166 | 2. Exceptions and Limitations. For the avoidance of doubt, where 167 | Exceptions and Limitations apply to Your use, this Public 168 | License does not apply, and You do not need to comply with 169 | its terms and conditions. 170 | 171 | 3. Term. The term of this Public License is specified in Section 172 | 6(a). 173 | 174 | 4. Media and formats; technical modifications allowed. The 175 | Licensor authorizes You to exercise the Licensed Rights in 176 | all media and formats whether now known or hereafter created, 177 | and to make technical modifications necessary to do so. The 178 | Licensor waives and/or agrees not to assert any right or 179 | authority to forbid You from making technical modifications 180 | necessary to exercise the Licensed Rights, including 181 | technical modifications necessary to circumvent Effective 182 | Technological Measures. For purposes of this Public License, 183 | simply making modifications authorized by this Section 2(a) 184 | (4) never produces Adapted Material. 185 | 186 | 5. Downstream recipients. 187 | 188 | a. Offer from the Licensor -- Licensed Material. Every 189 | recipient of the Licensed Material automatically 190 | receives an offer from the Licensor to exercise the 191 | Licensed Rights under the terms and conditions of this 192 | Public License. 193 | 194 | b. Additional offer from the Licensor -- Adapted Material. 195 | Every recipient of Adapted Material from You 196 | automatically receives an offer from the Licensor to 197 | exercise the Licensed Rights in the Adapted Material 198 | under the conditions of the Adapter's License You apply. 199 | 200 | c. No downstream restrictions. You may not offer or impose 201 | any additional or different terms or conditions on, or 202 | apply any Effective Technological Measures to, the 203 | Licensed Material if doing so restricts exercise of the 204 | Licensed Rights by any recipient of the Licensed 205 | Material. 206 | 207 | 6. No endorsement. Nothing in this Public License constitutes or 208 | may be construed as permission to assert or imply that You 209 | are, or that Your use of the Licensed Material is, connected 210 | with, or sponsored, endorsed, or granted official status by, 211 | the Licensor or others designated to receive attribution as 212 | provided in Section 3(a)(1)(A)(i). 213 | 214 | b. Other rights. 215 | 216 | 1. Moral rights, such as the right of integrity, are not 217 | licensed under this Public License, nor are publicity, 218 | privacy, and/or other similar personality rights; however, to 219 | the extent possible, the Licensor waives and/or agrees not to 220 | assert any such rights held by the Licensor to the limited 221 | extent necessary to allow You to exercise the Licensed 222 | Rights, but not otherwise. 223 | 224 | 2. Patent and trademark rights are not licensed under this 225 | Public License. 226 | 227 | 3. To the extent possible, the Licensor waives any right to 228 | collect royalties from You for the exercise of the Licensed 229 | Rights, whether directly or through a collecting society 230 | under any voluntary or waivable statutory or compulsory 231 | licensing scheme. In all other cases the Licensor expressly 232 | reserves any right to collect such royalties, including when 233 | the Licensed Material is used other than for NonCommercial 234 | purposes. 235 | 236 | 237 | Section 3 -- License Conditions. 238 | 239 | Your exercise of the Licensed Rights is expressly made subject to the 240 | following conditions. 241 | 242 | a. Attribution. 243 | 244 | 1. If You Share the Licensed Material (including in modified 245 | form), You must: 246 | 247 | a. retain the following if it is supplied by the Licensor 248 | with the Licensed Material: 249 | 250 | i. identification of the creator(s) of the Licensed 251 | Material and any others designated to receive 252 | attribution, in any reasonable manner requested by 253 | the Licensor (including by pseudonym if 254 | designated); 255 | 256 | ii. a copyright notice; 257 | 258 | iii. a notice that refers to this Public License; 259 | 260 | iv. a notice that refers to the disclaimer of 261 | warranties; 262 | 263 | v. a URI or hyperlink to the Licensed Material to the 264 | extent reasonably practicable; 265 | 266 | b. indicate if You modified the Licensed Material and 267 | retain an indication of any previous modifications; and 268 | 269 | c. indicate the Licensed Material is licensed under this 270 | Public License, and include the text of, or the URI or 271 | hyperlink to, this Public License. 272 | 273 | 2. You may satisfy the conditions in Section 3(a)(1) in any 274 | reasonable manner based on the medium, means, and context in 275 | which You Share the Licensed Material. For example, it may be 276 | reasonable to satisfy the conditions by providing a URI or 277 | hyperlink to a resource that includes the required 278 | information. 279 | 3. If requested by the Licensor, You must remove any of the 280 | information required by Section 3(a)(1)(A) to the extent 281 | reasonably practicable. 282 | 283 | b. ShareAlike. 284 | 285 | In addition to the conditions in Section 3(a), if You Share 286 | Adapted Material You produce, the following conditions also apply. 287 | 288 | 1. The Adapter's License You apply must be a Creative Commons 289 | license with the same License Elements, this version or 290 | later, or a BY-NC-SA Compatible License. 291 | 292 | 2. You must include the text of, or the URI or hyperlink to, the 293 | Adapter's License You apply. You may satisfy this condition 294 | in any reasonable manner based on the medium, means, and 295 | context in which You Share Adapted Material. 296 | 297 | 3. You may not offer or impose any additional or different terms 298 | or conditions on, or apply any Effective Technological 299 | Measures to, Adapted Material that restrict exercise of the 300 | rights granted under the Adapter's License You apply. 301 | 302 | 303 | Section 4 -- Sui Generis Database Rights. 304 | 305 | Where the Licensed Rights include Sui Generis Database Rights that 306 | apply to Your use of the Licensed Material: 307 | 308 | a. for the avoidance of doubt, Section 2(a)(1) grants You the right 309 | to extract, reuse, reproduce, and Share all or a substantial 310 | portion of the contents of the database for NonCommercial purposes 311 | only; 312 | 313 | b. if You include all or a substantial portion of the database 314 | contents in a database in which You have Sui Generis Database 315 | Rights, then the database in which You have Sui Generis Database 316 | Rights (but not its individual contents) is Adapted Material, 317 | including for purposes of Section 3(b); and 318 | 319 | c. You must comply with the conditions in Section 3(a) if You Share 320 | all or a substantial portion of the contents of the database. 321 | 322 | For the avoidance of doubt, this Section 4 supplements and does not 323 | replace Your obligations under this Public License where the Licensed 324 | Rights include other Copyright and Similar Rights. 325 | 326 | 327 | Section 5 -- Disclaimer of Warranties and Limitation of Liability. 328 | 329 | a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE 330 | EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS 331 | AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF 332 | ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, 333 | IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, 334 | WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR 335 | PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, 336 | ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT 337 | KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT 338 | ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. 339 | 340 | b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE 341 | TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, 342 | NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, 343 | INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, 344 | COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR 345 | USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN 346 | ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR 347 | DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR 348 | IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. 349 | 350 | c. The disclaimer of warranties and limitation of liability provided 351 | above shall be interpreted in a manner that, to the extent 352 | possible, most closely approximates an absolute disclaimer and 353 | waiver of all liability. 354 | 355 | 356 | Section 6 -- Term and Termination. 357 | 358 | a. This Public License applies for the term of the Copyright and 359 | Similar Rights licensed here. However, if You fail to comply with 360 | this Public License, then Your rights under this Public License 361 | terminate automatically. 362 | 363 | b. Where Your right to use the Licensed Material has terminated under 364 | Section 6(a), it reinstates: 365 | 366 | 1. automatically as of the date the violation is cured, provided 367 | it is cured within 30 days of Your discovery of the 368 | violation; or 369 | 370 | 2. upon express reinstatement by the Licensor. 371 | 372 | For the avoidance of doubt, this Section 6(b) does not affect any 373 | right the Licensor may have to seek remedies for Your violations 374 | of this Public License. 375 | 376 | c. For the avoidance of doubt, the Licensor may also offer the 377 | Licensed Material under separate terms or conditions or stop 378 | distributing the Licensed Material at any time; however, doing so 379 | will not terminate this Public License. 380 | 381 | d. Sections 1, 5, 6, 7, and 8 survive termination of this Public 382 | License. 383 | 384 | 385 | Section 7 -- Other Terms and Conditions. 386 | 387 | a. The Licensor shall not be bound by any additional or different 388 | terms or conditions communicated by You unless expressly agreed. 389 | 390 | b. Any arrangements, understandings, or agreements regarding the 391 | Licensed Material not stated herein are separate from and 392 | independent of the terms and conditions of this Public License. 393 | 394 | 395 | Section 8 -- Interpretation. 396 | 397 | a. For the avoidance of doubt, this Public License does not, and 398 | shall not be interpreted to, reduce, limit, restrict, or impose 399 | conditions on any use of the Licensed Material that could lawfully 400 | be made without permission under this Public License. 401 | 402 | b. To the extent possible, if any provision of this Public License is 403 | deemed unenforceable, it shall be automatically reformed to the 404 | minimum extent necessary to make it enforceable. If the provision 405 | cannot be reformed, it shall be severed from this Public License 406 | without affecting the enforceability of the remaining terms and 407 | conditions. 408 | 409 | c. No term or condition of this Public License will be waived and no 410 | failure to comply consented to unless expressly agreed to by the 411 | Licensor. 412 | 413 | d. Nothing in this Public License constitutes or may be interpreted 414 | as a limitation upon, or waiver of, any privileges and immunities 415 | that apply to the Licensor or You, including from the legal 416 | processes of any jurisdiction or authority. 417 | 418 | ======================================================================= 419 | 420 | Creative Commons is not a party to its public 421 | licenses. Notwithstanding, Creative Commons may elect to apply one of 422 | its public licenses to material it publishes and in those instances 423 | will be considered the “Licensor.” The text of the Creative Commons 424 | public licenses is dedicated to the public domain under the CC0 Public 425 | Domain Dedication. Except for the limited purpose of indicating that 426 | material is shared under a Creative Commons public license or as 427 | otherwise permitted by the Creative Commons policies published at 428 | creativecommons.org/policies, Creative Commons does not authorize the 429 | use of the trademark "Creative Commons" or any other trademark or logo 430 | of Creative Commons without its prior written consent including, 431 | without limitation, in connection with any unauthorized modifications 432 | to any of its public licenses or any other arrangements, 433 | understandings, or agreements concerning use of licensed material. For 434 | the avoidance of doubt, this paragraph does not form part of the 435 | public licenses. 436 | 437 | Creative Commons may be contacted at creativecommons.org. 438 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: toc 2 | 3 | toc: 4 | docker run --rm -it -v ${PWD}:/usr/src jorgeandrada/doctoc --github 5 | $(shell tail -n +`grep -n '# \`go-internals\`' README.md | tr ':' ' ' | awk '{print $$1}'` README.md > /tmp/README2.md) 6 | cp /tmp/README2.md README.md 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `go-internals` 2 | 3 | `go-internals` 本书是关于 Go 程序设计语言内部实现原理的阐释,当前正在进行中。 4 | 5 | --- 6 | 7 | ## 目录 8 | 9 | - [第一章: Go语言汇编介绍](./chapter1_assembly_primer/README.md) 10 | - [第二章: 接口](./chapter2_interfaces/README.md) 11 | - [第三章 (即将呈现!): 垃圾回收](./chapter3_garbage_collector/README.md) 12 | 13 | --- 14 | 15 | *我们提供 GitBook 版本的链接 点击[这里](https://go-internals-cn.gitbook.io/go-internals/).* 16 | 17 | ## 目标 18 | 19 | - **精确**:本书为尽可能做到精确,书中会优先使用代码和图例阐述问题,而非冗长的叙述。 20 | - **依托社区**:我自己也是边学边写这本书,我会有错误的地方,希望社区一起帮助提高完善这本书。 21 | - **理论实践结合**:本书不是仅仅对理论的阐述,我们做到对具体实现的深入分析,所有假设都会用实验验证和测量。 22 | - **实时更新**:本书内容尽力保持同最新发布版本的 Go 语言保持同步。 23 | - **高级读者**:虽然 Go 社区有非常棒的针对新手的入门材料,但我们仍然缺乏优质的对高级内容阐述的资源,本书就是为了弥补这个空白。 24 | 25 | ## 参与贡献 26 | 27 | 我们欢迎所有形式的贡献和参与。 28 | 29 | 请直接给我们提 Issue 包括但不限于以下情况: 30 | - 指出技术或者语言描述错误 31 | - 建议对已有章节的改进和新增内容 32 | - 提供其他对本书理解有帮助的外部链接 33 | - 其他任何你能想到的建议,我们真诚期待! 34 | 35 | ## 作者 36 | 37 | Clement Rey <> ([@teh_cmc](https://twitter.com/teh_cmc)) 38 | 39 | ## 授权许可 40 | 41 | 授权协议请参考 [BY-NC-SA Creative Commons 4.0 International Public License](http://creativecommons.org/licenses/by-nc-sa/4.0/) 42 | -------------------------------------------------------------------------------- /chapter1_assembly_primer/Makefile: -------------------------------------------------------------------------------- 1 | GOOS=linux 2 | GOARCH=amd64 3 | 4 | SOURCES := $(wildcard *.go) 5 | OBJECTS = $(SOURCES:.go=.o) 6 | EXECUTABLES = $(OBJECTS:.o=.bin) 7 | 8 | .SECONDARY: ${OBJECTS} 9 | 10 | all: ${EXECUTABLES} 11 | 12 | %.o: %.go 13 | GOOS=${GOOS} GOARCH=${GOARCH} go tool compile $< 14 | 15 | %.bin: %.o 16 | GOOS=${GOOS} GOARCH=${GOARCH} go tool link -o $@ $< 17 | 18 | clean: 19 | rm -f ${OBJECTS} 20 | rm -f ${EXECUTABLES} 21 | -------------------------------------------------------------------------------- /chapter1_assembly_primer/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # 第一章: Go 汇编入门 5 | 6 | 在深入学习 runtime 和标准库的实现之前,我们需要先对 Go 的汇编有一定的熟练度。这份快速指南希望能够加速你的学习进程。 7 | 8 | - *本章假设你已经对某一种汇编器的基础知识有所了解* 9 | - *涉及到架构相关的情况时,请假设我们是运行在 `linux/amd64` 平台上* 10 | - *学习过程中编译器优化会**打开**。* 11 | 12 | --- 13 | 14 | **目录** 15 | 16 | 17 | 18 | 19 | - ["伪汇编"](#%E4%BC%AA%E6%B1%87%E7%BC%96) 20 | - [拆解一个简单程序](#%E6%8B%86%E8%A7%A3%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E7%A8%8B%E5%BA%8F) 21 | - [解剖 `add`](#%E8%A7%A3%E5%89%96-add) 22 | - [解剖 `main`](#%E8%A7%A3%E5%89%96-main) 23 | - [关于协程, 栈及栈分裂](#%E5%85%B3%E4%BA%8E%E5%8D%8F%E7%A8%8B-%E6%A0%88%E5%8F%8A%E6%A0%88%E5%88%86%E8%A3%82) 24 | - [栈](#%E6%A0%88) 25 | - [栈分裂](#%E6%A0%88%E5%88%86%E8%A3%82) 26 | - [缺失的细节](#%E7%BC%BA%E5%A4%B1%E7%9A%84%E7%BB%86%E8%8A%82) 27 | - [总结](#%E6%80%BB%E7%BB%93) 28 | - [链接](#%E9%93%BE%E6%8E%A5) 29 | 30 | 31 | 32 | --- 33 | 34 | *本章中的引用段落/注释都引用自官方文档或者 Go 的代码库,除非另外注明* 35 | 36 | ## "伪汇编" 37 | 38 | Go 编译器会输出一种抽象可移植的汇编代码,这种汇编并不对应某种真实的硬件架构。Go 的汇编器会使用这种伪汇编,再为目标硬件生成具体的机器指令。 39 | 40 | 伪汇编这一个额外层可以带来很多好处,最主要的一点是方便将 Go 移植到新的架构上。相 41 | 关的信息可以参考文后列出的 Rob Pike 的 *The Design of the Go Assembler*。 42 | 43 | > 要了解Go的汇编器最重要的是要知道Go的汇编器不是对底层机器的直接表示,即Go的汇 44 | > 编器没有直接使用目标机器的汇编指令。Go汇编器所用的指令,一部分与目标机器的指令 45 | > 一一对应,而另外一部分则不是。这是因为编译器套件不需要汇编器直接参与常规的编译 46 | > 过程。相反,编译器使用了一种半抽象的指令集,并且部分指令是在代码生成后才被选择 47 | > 的。汇编器基于这种半抽象的形式工作,所以虽然你看到的是一条MOV指令,但是工具链 48 | > 针对对这条指令实际生成可能完全不是一个移动指令,也许会是清除或者加载。也有可能 49 | > 精确的对应目标平台上同名的指令。概括来说,特定于机器的指令会以他们的本尊出现, 50 | > 然而对于一些通用的操作,如内存的移动以及子程序的调用以及返回通常都做了抽象。细 51 | > 节因架构不同而不一样,我们对这样的不精确性表示歉意,情况并不明确。 52 | 53 | > 汇编器程序的工作是对这样半抽象指令集进行解析并将其转变为可以输入到链接器的指令。 54 | 55 | > The most important thing to know about Go's assembler is that it is not a direct representation of the underlying machine. Some of the details map precisely to the machine, but some do not. This is because the compiler suite needs no assembler pass in the usual pipeline. Instead, the compiler operates on a kind of semi-abstract instruction set, and instruction selection occurs partly after code generation. The assembler works on the semi-abstract form, so when you see an instruction like MOV what the toolchain actually generates for that operation might not be a move instruction at all, perhaps a clear or load. Or it might correspond exactly to the machine instruction with that name. In general, machine-specific operations tend to appear as themselves, while more general concepts like memory move and subroutine call and return are more abstract. The details vary with architecture, and we apologize for the imprecision; the situation is not well-defined. 56 | 57 | > The assembler program is a way to parse a description of that semi-abstract instruction set and turn it into instructions to be input to the linker. 58 | 59 | ## 拆解一个简单程序 60 | 61 | 思考一下下面这段 Go 代码 ([direct_topfunc_call.go](./direct_topfunc_call.go)): 62 | 63 | ```Go 64 | //go:noinline 65 | func add(a, b int32) (int32, bool) { return a + b, true } 66 | 67 | func main() { add(10, 32) } 68 | ``` 69 | *(注意这里的 `//go:noinline` 编译器指令。。不要省略掉这部分)* 70 | 71 | 将这段代码编译到汇编: 72 | ``` 73 | $ GOOS=linux GOARCH=amd64 go tool compile -S direct_topfunc_call.go 74 | ``` 75 | ```Assembly 76 | 0x0000 TEXT "".add(SB), NOSPLIT, $0-16 77 | 0x0000 FUNCDATA $0, gclocals·f207267fbf96a0178e8758c6e3e0ce28(SB) 78 | 0x0000 FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) 79 | 0x0000 MOVL "".b+12(SP), AX 80 | 0x0004 MOVL "".a+8(SP), CX 81 | 0x0008 ADDL CX, AX 82 | 0x000a MOVL AX, "".~r2+16(SP) 83 | 0x000e MOVB $1, "".~r3+20(SP) 84 | 0x0013 RET 85 | 86 | 0x0000 TEXT "".main(SB), $24-0 87 | ;; ...omitted stack-split prologue... 88 | 0x000f SUBQ $24, SP 89 | 0x0013 MOVQ BP, 16(SP) 90 | 0x0018 LEAQ 16(SP), BP 91 | 0x001d FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) 92 | 0x001d FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) 93 | 0x001d MOVQ $137438953482, AX 94 | 0x0027 MOVQ AX, (SP) 95 | 0x002b PCDATA $0, $0 96 | 0x002b CALL "".add(SB) 97 | 0x0030 MOVQ 16(SP), BP 98 | 0x0035 ADDQ $24, SP 99 | 0x0039 RET 100 | ;; ...omitted stack-split epilogue... 101 | ``` 102 | 103 | 接下来一行一行地对这两个函数进行解析来帮助我们理解编译器在编译期间都做了什么事情。 104 | 105 | ### 解剖 `add` 106 | 107 | ```Assembly 108 | 0x0000 TEXT "".add(SB), NOSPLIT, $0-16 109 | ``` 110 | 111 | - `0x0000`: 当前指令相对于当前函数的偏移量。 112 | 113 | - `TEXT "".add`: `TEXT` 指令声明了 `"".add` 是 `.text` 段(程序代码在运行期会放在内存的 .text 段中)的一部分,并表明跟在这个声明后的是函数的函数体。 114 | 在链接期,`""` 这个空字符会被替换为当前的包名: 也就是说,`"".add` 在链接到二进制文件后会变成 `main.add`。 115 | 116 | - `(SB)`: `SB` 是一个虚拟寄存器,保存了静态基地址(static-base) 指针,即我们程序地址空间的开始地址。 117 | `"".add(SB)` 表明我们的符号位于某个固定的相对地址空间起始处的偏移位置 (最终是由链接器计算得到的)。换句话来讲,它有一个直接的绝对地址: 是一个全局的函数符号。 118 | `objdump` 这个工具能帮我们确认上面这些结论: 119 | 120 | ``` 121 | $ objdump -j .text -t direct_topfunc_call | grep 'main.add' 122 | 000000000044d980 g F .text 000000000000000f main.add 123 | ``` 124 | 125 | > 所有用户定义的符号都被写为相对于伪寄存器FP(参数以及局部值)和SB(全局值)的偏移量。 126 | > SB伪寄存器可以被认为是内存的起始位置,所以对于符号foo(SB)就是名称foo在内存的地址。 127 | 128 | > All user-defined symbols are written as offsets to the pseudo-registers FP (arguments and locals) and SB (globals). 129 | > The SB pseudo-register can be thought of as the origin of memory, so the symbol foo(SB) is the name foo as an address in memory. 130 | 131 | - `NOSPLIT`: 向编译器表明*不应该*插入 *stack-split* 的用来检查栈需要扩张的前导指令。 132 | 在我们 `add` 函数的这种情况下,编译器自己帮我们插入了这个标记: 它足够聪明地意识到,由于 `add` 没有任何局部变量且没有它自己的栈帧,所以一定不会超出当前的栈;因此每次调用函数时在这里执行栈检查就是完全浪费 CPU 循环了。 133 | 134 | > "NOSPLIT": 不会插入前导码来检查栈是否必须被分裂。协程上的栈帧,以及他所有的调 135 | > 用,都必须存放在栈顶的空闲空间。用来保护协程诸如栈分裂代码本身。 136 | 137 | > "NOSPLIT": Don't insert the preamble to check if the stack must be split. The frame for the routine, plus anything it calls, must fit in the spare space at the top of the stack segment. Used to protect routines such as the stack splitting code itself. 138 | 139 | 本章结束时会对 goroutines 和 stack-splits 进行简单介绍。 140 | 141 | - `$0-16`: `$0` 代表即将分配的栈帧大小;而 `$16` 指定了调用方传入的参数大小。 142 | 143 | > 通常来讲,帧大小后一般都跟随着一个参数大小,用减号分隔。(这不是一个减法操作,只是 144 | > 一种特殊的语法)帧大小 $24-8 意味着这个函数有24个字节的帧以及8个字节的参数,位 145 | > 于调用者的帧上。如果NOSPLIT没有在TEXT中指定,则必须提供参数大小。对于Go原型的 146 | > 汇编函数,go vet会检查参数大小是否正确。 147 | 148 | > In the general case, the frame size is followed by an argument size, separated by a minus sign. (It's not a subtraction, just idiosyncratic syntax.) The frame size $24-8 states that the function has a 24-byte frame and is called with 8 bytes of argument, which live on the caller's frame. If NOSPLIT is not specified for the TEXT, the argument size must be provided. For assembly functions with Go prototypes, go vet will check that the argument size is correct. 149 | 150 | ```Assembly 151 | 0x0000 FUNCDATA $0, gclocals·f207267fbf96a0178e8758c6e3e0ce28(SB) 152 | 0x0000 FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB) 153 | ``` 154 | 155 | > FUNCDATA以及PCDATA指令包含有被垃圾回收所使用的信息;这些指令是被编译器加入的。 156 | 157 | > The FUNCDATA and PCDATA directives contain information for use by the garbage collector; they are introduced by the compiler. 158 | 159 | 现在还不要对这个太上心;在本书深入探讨垃圾收集时,会再回来了解这些知识。 160 | 161 | ```Assembly 162 | 0x0000 MOVL "".b+12(SP), AX 163 | 0x0004 MOVL "".a+8(SP), CX 164 | ``` 165 | 166 | Go 的调用规约要求每一个参数都通过栈来传递,这部分空间由 caller 在其栈帧(stack frame)上提供。 167 | 168 | 调用其它过程之前,caller 就需要按照参数和返回变量的大小来对应地增长(返回后收缩)栈。 169 | 170 | Go 编译器不会生成任何 PUSH/POP 族的指令: 栈的增长和收缩是通过在栈指针寄存器 `SP` 上分别执行减法和加法指令来实现的。 171 | 172 | > SP伪寄存器是虚拟的栈指针,用于引用帧局部变量以及为函数调用准备的参数。 173 | > 它指向局部栈帧的顶部,所以应用应该使用负的偏移且范围在[-framesize, 0): 174 | > x-8(SP), y-4(SP), 等等。 175 | 176 | > The SP pseudo-register is a virtual stack pointer used to refer to frame-local variables and the arguments being prepared for function calls. It points to the top of the local stack frame, so references should use negative offsets in the range [−framesize, 0): x-8(SP), y-4(SP), and so on. 177 | 178 | 尽管官方文档说 "*All user-defined symbols are written as offsets to the pseudo-register FP(arguments and locals)*",实际这个原则只是在手写的代码场景下才是有效的。 179 | 与大多数最近的编译器做法一样,Go 工具链总是在其生成的代码中,使用相对栈指针(stack-pointer)的偏移量来引用参数和局部变量。这样使得我们可以在那些寄存器数量较少的平台上(例如 x86),也可以将帧指针(frame-pointer)作为一个额外的通用寄存器。 180 | 如果你喜欢了解这些细节问题,可以参考本章后提供的 *Stack frame layout on x86-64* 一文。 181 | 182 | `"".b+12(SP)` 和 `"".a+8(SP)` 分别指向栈的低 12 字节和低 8 字节位置(记住: 栈是向低位地址方向增长的!)。 183 | `.a` 和 `.b` 是分配给引用地址的任意别名;尽管 *它们没有任何语义上的含义* ,但在使用虚拟寄存器和相对地址时,这种别名是需要强制使用的。 184 | 虚拟寄存器帧指针(frame-pointer)的文档对此有所提及: 185 | 186 | > FP伪寄存器是虚拟的帧指针,用来对函数的参数做参考。编译器维护虚拟帧指针并将栈中 187 | > 的参数作为该伪寄存器的偏移量。因此0(FP)是函数的第一个参数,8(FP)是第二个(在64 188 | > 位机器上),等等。然而,当使用这种方式应用函数参数时,必须在开始的位置放置一个 189 | > 名称,比如first_arg+0(FP) 以及 second_arg+8(FP). (偏移————相对于帧指针的偏 190 | > 移————的意义是与SB中的偏移不一样的,它是相对于符号的偏移。)汇编器强制执行这种 191 | > 约定,拒绝纯0(FP)以及8(FP)。实际名称与语义不想关,但应该用来记录参数的名字。 192 | 193 | > The FP pseudo-register is a virtual frame pointer used to refer to function arguments. The compilers maintain a virtual frame pointer and refer to the arguments on the stack as offsets from that pseudo-register. Thus 0(FP) is the first argument to the function, 8(FP) is the second (on a 64-bit machine), and so on. However, when referring to a function argument this way, it is necessary to place a name at the beginning, as in first_arg+0(FP) and second_arg+8(FP). (The meaning of the offset —offset from the frame pointer— distinct from its use with SB, where it is an offset from the symbol.) The assembler enforces this convention, rejecting plain 0(FP) and 8(FP). The actual name is semantically irrelevant but should be used to document the argument's name. 194 | 195 | 最后,有两个重点需要指出: 196 | 1. 第一个变量 `a` 的地址并不是 `0(SP)`,而是在 `8(SP)`;这是因为调用方通过使用 `CALL` 伪指令,把其返回地址保存在了 `0(SP)` 位置。 197 | 2. 参数是反序传入的;也就是说,第一个参数和栈顶距离最近。 198 | 199 | ```Assembly 200 | 0x0008 ADDL CX, AX 201 | 0x000a MOVL AX, "".~r2+16(SP) 202 | 0x000e MOVB $1, "".~r3+20(SP) 203 | ``` 204 | 205 | `ADDL` 进行实际的加法操作,L 这里代表 **L**ong,4 字节的值,其将保存在 `AX` 和 `CX` 寄存器中的值进行相加,然后再保存进 `AX` 寄存器中。 206 | 这个结果之后被移动到 `"".~r2+16(SP)` 地址处,这是之前调用方专门为返回值预留的栈空间。这一次 `"".~r2` 同样没什么语义上的含义。 207 | 208 | 为了演示 Go 如何处理多返回值,我们同时返回了一个 bool 常量 `true`。 209 | 返回这个 bool 值的方法和之前返回数值的方法是一样的;只是相对于 `SP` 寄存器的偏移量发生了变化。 210 | 211 | ```Assembly 212 | 0x0013 RET 213 | ``` 214 | 215 | 最后的 `RET` 伪指令告诉 Go 汇编器插入一些指令,这些指令是对应的目标平台中的调用规约所要求的,从子过程中返回时所需要的指令。 216 | 一般情况下这样的指令会使在 `0(SP)` 寄存器中保存的函数返回地址被 pop 出栈,并跳回到该地址。 217 | 218 | > TEXT块的最后一条指令必须为某种形式的跳转,通常为RET(伪)指令。 219 | > (如果不是的话,链接器会添加一条跳转到自己的指令;TEXT块没有失败处理) 220 | 221 | > The last instruction in a TEXT block must be some sort of jump, usually a RET (pseudo-)instruction. 222 | > (If it's not, the linker will append a jump-to-itself instruction; there is no fallthrough in TEXTs.) 223 | 224 | 我们一次性需要消化的语法和语义细节有点多。下面将我们已经覆盖到的知识点作为注释加进了汇编代码中: 225 | ```Assembly 226 | ;; Declare global function symbol "".add (actually main.add once linked) 227 | ;; Do not insert stack-split preamble 228 | ;; 0 bytes of stack-frame, 16 bytes of arguments passed in 229 | ;; func add(a, b int32) (int32, bool) 230 | 0x0000 TEXT "".add(SB), NOSPLIT, $0-16 231 | ;; ...omitted FUNCDATA stuff... 232 | 0x0000 MOVL "".b+12(SP), AX ;; move second Long-word (4B) argument from caller's stack-frame into AX 233 | 0x0004 MOVL "".a+8(SP), CX ;; move first Long-word (4B) argument from caller's stack-frame into CX 234 | 0x0008 ADDL CX, AX ;; compute AX=CX+AX 235 | 0x000a MOVL AX, "".~r2+16(SP) ;; move addition result (AX) into caller's stack-frame 236 | 0x000e MOVB $1, "".~r3+20(SP) ;; move `true` boolean (constant) into caller's stack-frame 237 | 0x0013 RET ;; jump to return address stored at 0(SP) 238 | ``` 239 | 240 | 总之,下面是 `main.add` 即将执行 `RET` 指令时的栈的情况。 241 | 242 | ``` 243 | | +-------------------------+ <-- 32(SP) 244 | | | | 245 | G | | | 246 | R | | | 247 | O | | main.main's saved | 248 | W | | frame-pointer (BP) | 249 | S | |-------------------------| <-- 24(SP) 250 | | | [alignment] | 251 | D | | "".~r3 (bool) = 1/true | <-- 21(SP) 252 | O | |-------------------------| <-- 20(SP) 253 | W | | | 254 | N | | "".~r2 (int32) = 42 | 255 | W | |-------------------------| <-- 16(SP) 256 | A | | | 257 | R | | "".b (int32) = 32 | 258 | D | |-------------------------| <-- 12(SP) 259 | S | | | 260 | | | "".a (int32) = 10 | 261 | | |-------------------------| <-- 8(SP) 262 | | | | 263 | | | | 264 | | | | 265 | \ | / | return address to | 266 | \|/ | main.main + 0x30 | 267 | - +-------------------------+ <-- 0(SP) (TOP OF STACK) 268 | 269 | (diagram made with https://textik.com) 270 | ``` 271 | 272 | 273 | ### 解剖 `main` 274 | 275 | 这里略去了一些代码帮你节省滚鼠标的时间,我们再次回忆一下 `main` 函数的逆向结果: 276 | ```Assembly 277 | 0x0000 TEXT "".main(SB), $24-0 278 | ;; ...omitted stack-split prologue... 279 | 0x000f SUBQ $24, SP 280 | 0x0013 MOVQ BP, 16(SP) 281 | 0x0018 LEAQ 16(SP), BP 282 | ;; ...omitted FUNCDATA stuff... 283 | 0x001d MOVQ $137438953482, AX 284 | 0x0027 MOVQ AX, (SP) 285 | ;; ...omitted PCDATA stuff... 286 | 0x002b CALL "".add(SB) 287 | 0x0030 MOVQ 16(SP), BP 288 | 0x0035 ADDQ $24, SP 289 | 0x0039 RET 290 | ;; ...omitted stack-split epilogue... 291 | ``` 292 | 293 | ```Assembly 294 | 0x0000 TEXT "".main(SB), $24-0 295 | ``` 296 | 297 | 没什么新东西: 298 | 299 | - `"".main` (被链接之后名字会变成 `main.main`) 是一个全局的函数符号,存储在 `.text` 段中,该函数的地址是相对于整个地址空间起始位置的一个固定的偏移量。 300 | - 它分配了 24 字节的栈帧,且不接收参数,不返回值。 301 | 302 | ```Assembly 303 | 0x000f SUBQ $24, SP 304 | 0x0013 MOVQ BP, 16(SP) 305 | 0x0018 LEAQ 16(SP), BP 306 | ``` 307 | 308 | 上面我们已经提到过,Go 的调用规约强制我们将所有参数都通过栈来进行传递。 309 | 310 | `main` 作为调用者,通过对虚拟栈指针(stack-pointer)寄存器做减法,将其栈帧大小增加了 24 个字节(*回忆一下栈是向低地址方向增长,所以这里的 `SUBQ` 指令是将栈帧的大小调整得更大了*)。 311 | 这 24 个字节中: 312 | 313 | - 8 个字节(`16(SP)`-`24(SP)`) 用来存储当前帧指针 `BP` (这是一个实际存在的寄存器)的值,以支持栈的展开和方便调试 314 | - 1+3 个字节(`12(SP)`-`16(SP)`) 是预留出的给第二个返回值 (`bool`) 的空间,除了类型本身的 1 个字节,在 `amd64` 平台上还额外需要 3 个字节来做对齐 315 | - 4 个字节(`8(SP)`-`12(SP)`) 预留给第一个返回值 (`int32`) 316 | - 4 个字节(`4(SP)`-`8(SP)`) 是预留给传给被调用函数的参数 `b (int32)` 317 | - 4 个字节(`0(SP)`-`4(SP)`) 预留给传入参数 `a (int32)` 318 | 319 | 最后,跟着栈的增长,`LEAQ` 指令计算出帧指针的新地址,并将其存储到 `BP` 寄存器中。 320 | 321 | ```Assembly 322 | 0x001d MOVQ $137438953482, AX 323 | 0x0027 MOVQ AX, (SP) 324 | ``` 325 | 326 | 调用方将被调用方需要的参数作为一个 **Q**uad word(8 字节值)推到了刚刚增长的栈的栈顶。 327 | 328 | 尽管指令里出现的 `137438953482` 这个值看起来像是随机的垃圾值,实际上这个值对应的就是 `10` 和 `32` 这两个 4 字节值,它们两被连接成了一个 8 字节值。 329 | 330 | ``` 331 | $ echo 'obase=2;137438953482' | bc 332 | 10000000000000000000000000000000001010 333 | \_____/\_____________________________/ 334 | 32 10 335 | ``` 336 | 337 | ```Assembly 338 | 0x002b CALL "".add(SB) 339 | ``` 340 | 341 | 我们使用相对于 static-base 指针的偏移量,来对 `add` 函数进行 `CALL` 调用: 这种调用实际上相当于直接跳到一个指定的地址。 342 | 343 | 注意 `CALL` 指令还会将函数的返回地址(8 字节值)也推到栈顶;所以每次我们在 `add` 函数中引用 `SP` 寄存器的时候还需要额外偏移 8 个字节! 344 | 例如,`"".a` 现在不是 `0(SP)` 了,而是在 `8(SP)` 位置。 345 | 346 | ```Assembly 347 | 0x0030 MOVQ 16(SP), BP 348 | 0x0035 ADDQ $24, SP 349 | 0x0039 RET 350 | ``` 351 | 352 | 最后,我们: 353 | 354 | 1. 将帧指针(frame-pointer)下降一个栈帧(stack-frame)的大小(就是“向下”一级) 355 | 2. 将栈收缩 24 个字节,回收之前分配的栈空间 356 | 3. 请求 Go 汇编器插入子过程返回相关的指令 357 | 358 | ## 关于协程, 栈及栈分裂 359 | 360 | 现在还不是能够深入 goroutine 内部实现的合适时间点(*这部分会在之后讲解*),不过随着我们一遍遍 dump 出程序的汇编代码,栈管理相关的指令会越来越熟悉。 361 | 这样我们就可以快速地看出代码的模式,并且可以理解这些代码一般情况下在做什么,为什么要做这些事情。 362 | 363 | ### 栈 364 | 365 | 由于 Go 程序中的 goroutine 数目是不可确定的,并且实际场景可能会有百万级别的 goroutine,runtime 必须使用保守的思路来给 goroutine 分配空间以避免吃掉所有的可用内存。 366 | 367 | 也由于此,每个新的 goroutine 会被 runtime 分配初始为 2KB 大小的栈空间(Go 的栈在底层实际上是分配在堆空间上的)。 368 | 369 | 随着一个 goroutine 进行自己的工作,可能会超出最初分配的栈空间限制(就是栈溢出的意思)。 370 | 为了防止这种情况发生,runtime 确保 goroutine 在超出栈范围时,会创建一个相当于原来两倍大小的新栈,并将原来栈的上下文拷贝到新栈上。 371 | 这个过程被称为 *栈分裂*(stack-split),这样使得 goroutine 栈能够动态调整大小。 372 | 373 | ### 栈分裂 374 | 375 | 为了使栈分裂正常工作,编译器会在每一个函数的开头和结束位置插入指令来防止 goroutine 爆栈。 376 | 像我们本章早些看到的一样,为了避免不必要的开销,一定不会爆栈的函数会被标记上 `NOSPLIT` 来提示编译器不要在这些函数的开头和结束部分插入这些检查指令。 377 | 378 | 我们来看看之前的 main 函数,这次不再省略栈分裂的前导指令: 379 | 380 | ```Assembly 381 | 0x0000 TEXT "".main(SB), $24-0 382 | ;; stack-split prologue 383 | 0x0000 MOVQ (TLS), CX 384 | 0x0009 CMPQ SP, 16(CX) 385 | 0x000d JLS 58 386 | 387 | 0x000f SUBQ $24, SP 388 | 0x0013 MOVQ BP, 16(SP) 389 | 0x0018 LEAQ 16(SP), BP 390 | ;; ...omitted FUNCDATA stuff... 391 | 0x001d MOVQ $137438953482, AX 392 | 0x0027 MOVQ AX, (SP) 393 | ;; ...omitted PCDATA stuff... 394 | 0x002b CALL "".add(SB) 395 | 0x0030 MOVQ 16(SP), BP 396 | 0x0035 ADDQ $24, SP 397 | 0x0039 RET 398 | 399 | ;; stack-split epilogue 400 | 0x003a NOP 401 | ;; ...omitted PCDATA stuff... 402 | 0x003a CALL runtime.morestack_noctxt(SB) 403 | 0x003f JMP 0 404 | ``` 405 | 406 | 可以看到,栈分裂(stack-split)前导码被分成 prologue 和 epilogue 两个部分: 407 | 408 | - prologue 会检查当前 goroutine 是否已经用完了所有的空间,然后如果确实用完了的话,会直接跳转到后部。 409 | - epilogue 会触发栈增长(stack-growth),然后再跳回到前部。 410 | 411 | 这样就形成了一个反馈循环,使我们的栈在没有达到饥饿的 goroutine 要求之前不断地进行空间扩张。 412 | 413 | **Prologue** 414 | ```Assembly 415 | 0x0000 MOVQ (TLS), CX ;; store current *g in CX 416 | 0x0009 CMPQ SP, 16(CX) ;; compare SP and g.stackguard0 417 | 0x000d JLS 58 ;; jumps to 0x3a if SP <= g.stackguard0 418 | ``` 419 | 420 | `TLS` 是一个由 runtime 维护的虚拟寄存器,保存了指向当前 `g` 的指针,这个 `g` 的数据结构会跟踪 goroutine 运行时的所有状态值。 421 | 422 | 看一看 runtime 源代码中对于 `g` 的定义: 423 | ```Go 424 | type g struct { 425 | stack stack // 16 bytes 426 | // stackguard0 is the stack pointer compared in the Go stack growth prologue. 427 | // It is stack.lo+StackGuard normally, but can be StackPreempt to trigger a preemption. 428 | stackguard0 uintptr 429 | stackguard1 uintptr 430 | 431 | // ...omitted dozens of fields... 432 | } 433 | ``` 434 | 我们可以看到 `16(CX)` 对应的是 `g.stackguard0`,是 runtime 维护的一个阈值,该值会被拿来与栈指针(stack-pointer)进行比较以判断一个 goroutine 是否马上要用完当前的栈空间。 435 | 436 | 因此 prologue 只要检查当前的 `SP` 的值是否小于或等于 `stackguard0` 的阈值就行了,如果是的话,就跳到 epilogue 部分去。 437 | 438 | **Epilogue** 439 | ```Assembly 440 | 0x003a NOP 441 | 0x003a CALL runtime.morestack_noctxt(SB) 442 | 0x003f JMP 0 443 | ``` 444 | 445 | epilogue 部分的代码就很直来直去了: 它直接调用 runtime 的函数,对应的函数会将栈进行扩张,然后再跳回到函数的第一条指令去(就是指 prologue部分)。 446 | 447 | 在 `CALL` 之前出现的 `NOP` 这个指令使 prologue 部分不会直接跳到 `CALL` 指令位置。在一些平台上,直接跳到 `CALL` 可能会有一些麻烦的问题;所以在调用位置插一个 noop 的指令并在跳转时跳到这个 `NOP` 位置是一种最佳实践。 448 | 449 | ### 缺失的细节 450 | 451 | 本章的内容只是冰山一角。 452 | 栈的调整涉及的技术还有很多精妙的细节,这里暂时还没有提到。整个流程是一个非常复杂的流程,需要单独的一个章节来进行阐释。 453 | 454 | 之后我们会再回来讨论这些细节。 455 | 456 | ## 总结 457 | 458 | 对 Go 的汇编器的介绍应该已经为你提供了开始学习的足够的材料。 459 | 460 | 随着本书剩余部分对 Go 内部原理越来越深入的探究,Go 的汇编会是我们最为依仗的工具,用来帮助我们理解现象背后的那些不总是那么明显的实质。 461 | 462 | 如果你有问题或者建议,不要犹豫,开一个蛓有 `chapter1:` 前缀的 issue 即可! 463 | 464 | ## 链接 465 | 466 | - [[Official] A Quick Guide to Go's Assembler](https://golang.org/doc/asm) 467 | - [[Official] Go Compiler Directives](https://golang.org/cmd/compile/#hdr-Compiler_Directives) 468 | - [[Official] The design of the Go Assembler](https://www.youtube.com/watch?v=KINIAgRpkDA) 469 | - [[Official] Contiguous stacks Design Document](https://docs.google.com/document/d/1wAaf1rYoM4S4gtnPh0zOlGzWtrZFQ5suE8qr2sD8uWQ/pub) 470 | - [[Official] The `_StackMin` constant](https://github.com/golang/go/blob/ea8d7a370d66550d587414cc0cab650f35400f94/src/runtime/stack.go#L70-L71) 471 | - [A Foray Into Go Assembly Programming](https://blog.sgmansfield.com/2017/04/a-foray-into-go-assembly-programming/) 472 | - [Dropping Down Go Functions in Assembly](https://www.youtube.com/watch?v=9jpnFmJr2PE) 473 | - [What is the purpose of the EBP frame pointer register?](https://stackoverflow.com/questions/579262/what-is-the-purpose-of-the-ebp-frame-pointer-register) 474 | - [Stack frame layout on x86-64](https://eli.thegreenplace.net/2011/09/06/stack-frame-layout-on-x86-64) 475 | - [How Stacks are Handled in Go](https://blog.cloudflare.com/how-stacks-are-handled-in-go/) 476 | - [Why stack grows down](https://gist.github.com/cpq/8598782) 477 | -------------------------------------------------------------------------------- /chapter1_assembly_primer/direct_topfunc_call.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | //go:noinline 4 | func add(a, b int32) (int32, bool) { return a + b, true } 5 | 6 | func main() { add(10, 32) } 7 | -------------------------------------------------------------------------------- /chapter2_interfaces/Makefile: -------------------------------------------------------------------------------- 1 | GOOS=linux 2 | GOARCH=amd64 3 | 4 | SOURCES := $(wildcard *.go) 5 | OBJECTS = $(SOURCES:.go=.o) 6 | EXECUTABLES = $(OBJECTS:.o=.bin) 7 | 8 | .SECONDARY: ${OBJECTS} 9 | 10 | all: ${EXECUTABLES} 11 | 12 | %.o: %.go 13 | GOOS=${GOOS} GOARCH=${GOARCH} go tool compile $< 14 | 15 | %.bin: %.o 16 | GOOS=${GOOS} GOARCH=${GOARCH} go tool link -o $@ $< 17 | 18 | clean: 19 | rm -f ${OBJECTS} 20 | rm -f ${EXECUTABLES} 21 | -------------------------------------------------------------------------------- /chapter2_interfaces/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ```Bash 5 | $ go version 6 | go version go1.10 linux/amd64 7 | ``` 8 | 9 | # 第二章: 接口 10 | 11 | 本章覆盖 GO 的 interface 内部实现。 12 | 13 | 我们主要关注: 14 | - 函数和方法在运行时如何被调用。 15 | - interface 如何构建,其内容如何组成。 16 | - 动态分发是如何实现的,什么时候进行,并且有什么样的调用成本。 17 | - 空接口和其它特殊情况有什么异同。 18 | - 怎么组合 interface 完成工作。 19 | - 如何进行断言,断言的成本有多高。 20 | 21 | 随着我们越来越深入的挖掘,我们将会研究各种各样的底层知识,比如现代 CPU 的实现细节,Go 编译器的各种优化手段。 22 | 23 | --- 24 | 25 | **目录** 26 | 27 | 28 | 29 | 30 | - [函数及方法的调用](#%E5%87%BD%E6%95%B0%E5%8F%8A%E6%96%B9%E6%B3%95%E7%9A%84%E8%B0%83%E7%94%A8) 31 | - [直接调用概述](#%E7%9B%B4%E6%8E%A5%E8%B0%83%E7%94%A8%E6%A6%82%E8%BF%B0) 32 | - [隐式解引用](#%E9%9A%90%E5%BC%8F%E8%A7%A3%E5%BC%95%E7%94%A8) 33 | - [解构接口](#%E8%A7%A3%E6%9E%84%E6%8E%A5%E5%8F%A3) 34 | - [数据结构概述](#%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E6%A6%82%E8%BF%B0) 35 | - [创建接口](#%E5%88%9B%E5%BB%BA%E6%8E%A5%E5%8F%A3) 36 | - [从可执行文件中重建`itab`](#%E4%BB%8E%E5%8F%AF%E6%89%A7%E8%A1%8C%E6%96%87%E4%BB%B6%E4%B8%AD%E9%87%8D%E5%BB%BAitab) 37 | - [动态分发](#%E5%8A%A8%E6%80%81%E5%88%86%E5%8F%91) 38 | - [对接口的间接调用](#%E5%AF%B9%E6%8E%A5%E5%8F%A3%E7%9A%84%E9%97%B4%E6%8E%A5%E8%B0%83%E7%94%A8) 39 | - [性能开销](#%E6%80%A7%E8%83%BD%E5%BC%80%E9%94%80) 40 | - [理论:快速回顾现代CPUs](#%E7%90%86%E8%AE%BA%E5%BF%AB%E9%80%9F%E5%9B%9E%E9%A1%BE%E7%8E%B0%E4%BB%A3cpus) 41 | - [实践:性能基准](#%E5%AE%9E%E8%B7%B5%E6%80%A7%E8%83%BD%E5%9F%BA%E5%87%86) 42 | - [特殊例子及编译器技巧](#%E7%89%B9%E6%AE%8A%E4%BE%8B%E5%AD%90%E5%8F%8A%E7%BC%96%E8%AF%91%E5%99%A8%E6%8A%80%E5%B7%A7) 43 | - [空接口](#%E7%A9%BA%E6%8E%A5%E5%8F%A3) 44 | - [拥有标量的接口](#%E6%8B%A5%E6%9C%89%E6%A0%87%E9%87%8F%E7%9A%84%E6%8E%A5%E5%8F%A3) 45 | - [关于零值](#%E5%85%B3%E4%BA%8E%E9%9B%B6%E5%80%BC) 46 | - [关于大小为0的变量](#%E5%85%B3%E4%BA%8E%E5%A4%A7%E5%B0%8F%E4%B8%BA0%E7%9A%84%E5%8F%98%E9%87%8F) 47 | - [组合接口](#%E7%BB%84%E5%90%88%E6%8E%A5%E5%8F%A3) 48 | - [断言](#%E6%96%AD%E8%A8%80) 49 | - [类型断言](#%E7%B1%BB%E5%9E%8B%E6%96%AD%E8%A8%80) 50 | - [类型判断](#%E7%B1%BB%E5%9E%8B%E5%88%A4%E6%96%AD) 51 | - [总结](#%E6%80%BB%E7%BB%93) 52 | - [链接](#%E9%93%BE%E6%8E%A5) 53 | 54 | 55 | 56 | --- 57 | 58 | - *本章假设你已经对 Go 的汇编器比较熟悉了 ([第一章](../chapter1_assembly_primer/README.md)).* 59 | - *当你需要开始研究架构相关的内容时,请假设自己是在使用 `linux/amd64`.* 60 | - *我们会始终保持编译器优化是 **打开** 状态。* 61 | - *引用部分和注释内容都来自于官方文档(包括 Russ Cox "Function Call" 设计文档) 以及代码库,除非特别说明。* 62 | 63 | ## 函数及方法的调用 64 | 65 | 正如 Russ Cox 在他函数调用的设计文档所指出的一样(本章最后有链接),Go 有: 66 | 67 | ..4 种不同类型的函数..: 68 | > - 顶级函数 69 | > - 值 receiver 的方法 70 | > - 指针 receiver 的方法 71 | > - 函数字面量 72 | 73 | ..5 种不同类型的调用: 74 | > - 直接调用顶级函数(`func TopLevel(x int){}`) 75 | > - 直接调用值 receiver 的方法(`func (Value) M(int) {}`) 76 | > - 直接调用指针 receiver 的方法(`func (*Pointer) M(int) {}`) 77 | > - 间接调用接口的方法(`type Interface interface { M(int) }`) 78 | > - 间接调用函数值(`var literal = func(x int) {}`) 79 | 80 | 混合一下,有 10 种可能的函数以及调用类型组合: 81 | > - 直接调用顶级函数 / 82 | > - 直接调用一个值 receiver 的方法 / 83 | > - 直接调用一个指针 receiver 的方法 / 84 | > - 间接调用一个 interface 的方法 / 包含有值方法的值 85 | > - 间接调用一个 interface 的方法 / 包含有值方法的指针 86 | > - 间接调用一个 interface 的方法 / 包含有指针方法的指针 87 | > - 间接调用方法值 / 该值等于顶级方法 88 | > - 间接调用方法值 / 该值等于值方法 89 | > - 间接调用方法值 / 该值等于指针方法 90 | > - 间接调用方法值 / 该值等于函数字面量 91 | > 92 | > (这里用斜线来分离编译时和运行时才能知道的信息。) 93 | 94 | 本章先来复习一下三种直接调用,然后再把注意力转移到 interface 和间接的方法调用上。 95 | 96 | 本章不会覆盖函数字面量的内容,因为研究这方面的内容需要我们对闭包技术比较熟悉..而了解闭包可能还需要花费更多的时间。 97 | 98 | ### 直接调用概述 99 | 100 | 思考一下下面的代码 ([direct_calls.go](./direct_calls.go)): 101 | ```Go 102 | //go:noinline 103 | func Add(a, b int32) int32 { return a + b } 104 | 105 | type Adder struct{ id int32 } 106 | //go:noinline 107 | func (adder *Adder) AddPtr(a, b int32) int32 { return a + b } 108 | //go:noinline 109 | func (adder Adder) AddVal(a, b int32) int32 { return a + b } 110 | 111 | func main() { 112 | Add(10, 32) // direct call of top-level function 113 | 114 | adder := Adder{id: 6754} 115 | adder.AddPtr(10, 32) // direct call of method with pointer receiver 116 | adder.AddVal(10, 32) // direct call of method with value receiver 117 | 118 | (&adder).AddVal(10, 32) // implicit dereferencing 119 | } 120 | ``` 121 | 122 | 看一看这四种调用生成的代码。 123 | 124 | **Direct call of a top-level function** 125 | 126 | 看看 `Add(10, 32)` 的汇编输出: 127 | ```Assembly 128 | 0x0000 TEXT "".main(SB), $40-0 129 | ;; ...omitted everything but the actual function call... 130 | 0x0021 MOVQ $137438953482, AX 131 | 0x002b MOVQ AX, (SP) 132 | 0x002f CALL "".Add(SB) 133 | ;; ...omitted everything but the actual function call... 134 | ``` 135 | 从第一章我们已经知道,函数调用会被翻译成直接跳转指令,目标是 `.text` 段的全局函数符号,参数和返回值会被存储在发起调用者的栈帧上。 136 | 137 | 这个过程比较直观。 138 | 139 | Russ Cox 在它的文档里这样概括这件事: 140 | 141 | > 顶层函数的直接调用: 142 | > 对顶层函数的直接调用会通过栈来传递所有参数,并期望返回值占据连续的栈位置。 143 | 144 | > Direct call of top-level func: 145 | > A direct call of a top-level func passes all arguments on the stack, expecting results to occupy the successive stack positions. 146 | 147 | **Direct call of a method with pointer receiver** 148 | 149 | 先说重要的,receiver 是通过 `adder := Adder{id: 6754}` 来初始化的: 150 | ```Assembly 151 | 0x0034 MOVL $6754, "".adder+28(SP) 152 | ``` 153 | *(我们栈帧上额外的空间是作为帧指针前导的一部分,被预先分配好的,简洁起见,这里没有展示出来。)* 154 | 155 | 然后是对 `adder.AddPtr(10 32)` 的直接方法调用: 156 | ```Assembly 157 | 0x0057 LEAQ "".adder+28(SP), AX ;; move &adder to.. 158 | 0x005c MOVQ AX, (SP) ;; ..the top of the stack (argument #1) 159 | 0x0060 MOVQ $137438953482, AX ;; move (32,10) to.. 160 | 0x006a MOVQ AX, 8(SP) ;; ..the top of the stack (arguments #3 & #2) 161 | 0x006f CALL "".(*Adder).AddPtr(SB) 162 | ``` 163 | 164 | 观察汇编的输出,我们能够清楚地看到对方法的调用(无论 receiver 是值类型还是指针类型)和对函数的调用是相同的,唯一的区别是 receiver 会被当作第一个参数传入。 165 | 166 | 这种情况下,我们使用 loading the effective address (`LEAQ`) 这条指令来将 `"".adder+28(SP)` 加载到栈帧顶部,所以第一个参数 #1 就变成了 `&adder` (如果你对 `LEA` 和 `MOV` 有一些迷惑,你可能需要看看附录里的资料了)。 167 | 168 | 同时注意无论 receiver 的类型是值或是指针,编译器是怎么将其编码成符号名:`"".(*Adder).AddPtr` 的。 169 | 170 | > 方法的直接调用: 171 | > 为了能使用相同的生成代码处理对函数值的间接调用以及直接调用,针对方法生成的代码 172 | > (无论接收器是值还是指针)都使用了与顶层方法同样的调用约定,并把接收器作为第一个 173 | > 参数。 174 | 175 | > Direct call of method: 176 | > In order to use the same generated code for both an indirect call of a func value and for a direct call, the code generated for a method (both value and pointer receivers) is chosen to have the same calling convention as a top-level function with the receiver as a leading argument. 177 | 178 | **Direct call of a method with value receiver** 179 | 180 | 如我们所料,当 receiver 是值类型时,生成的代码和上面的类似。 181 | 来看看 `adder.AddVal(10, 32)`: 182 | ```Assembly 183 | 0x003c MOVQ $42949679714, AX ;; move (10,6754) to.. 184 | 0x0046 MOVQ AX, (SP) ;; ..the top of the stack (arguments #2 & #1) 185 | 0x004a MOVL $32, 8(SP) ;; move 32 to the top of the stack (argument #3) 186 | 0x0052 CALL "".Adder.AddVal(SB) 187 | ``` 188 | 189 | 不过这里有一点 trick 的地方,生成的汇编代码没有什么地方有对 `"".adder+28(SP)` 的引用,尽管这个地址是我们 receiver 所在的地址位置。 190 | 191 | 这是怎么回事呢?因为 receiver 是值类型,且编译器能够通过静态分析推测出其值,这种情况下编译器认为不需要对值从它原来的位置(`28(SP)`)进行拷贝了: 相应的,只要简单的在栈上创建一个新的和 `Adder` 相等的值,把这个操作和传第二个参数的操作进行捆绑,还可以节省一条汇编指令。 192 | 193 | 再次仔细观察,这个方法的符号名字,显式地指明了它接收的是一个值类型的 receiver。 194 | 195 | ### 隐式解引用 196 | 197 | 还有最后一种调用 `(&adder).AddVal(10, 32)`。 198 | 199 | 这种情况,我们使用了一个指针变量来调用一个期望 receiver 是值类型的方法。Go 魔法般地自动自动帮我们把指针解引用并执行了调用。为什么会这样? 200 | 201 | 编译器如何处理这种情况取决于 receiver 是否逃逸到堆上。 202 | 203 | **Case A: receiver 在栈上** 204 | 205 | 如果 receiver 在栈上,且 receiver 本身很小,这种情况只需要很少的汇编指令就可以将其值拷贝到栈顶然后再对 `"".Adder.AddVal` 进行一次直接的方法调用 (这里指的是值类型的 receiver)。 206 | 207 | `(&adder).AddVal(10, 32)` 于是就和下面这种情况很相似了: 208 | ```Assembly 209 | 0x0074 MOVL "".adder+28(SP), AX ;; move (i.e. copy) adder (note the MOV instead of a LEA) to.. 210 | 0x0078 MOVL AX, (SP) ;; ..the top of the stack (argument #1) 211 | 0x007b MOVQ $137438953482, AX ;; move (32,10) to.. 212 | 0x0085 MOVQ AX, 4(SP) ;; ..the top of the stack (arguments #3 & #2) 213 | 0x008a CALL "".Adder.AddVal(SB) 214 | ``` 215 | 216 | case B 尽管比较高效,但研究起来比较烦人。不过还是来看一下吧。 217 | 218 | **Case B: receiver 在堆上** 219 | 220 | receiver 逃逸到堆上的话,编译器需要用更聪明的过程来解决问题了: 先生成一个新方法(该方法 receiver 为指针类型,原始方法 receiver 为值类型),然后用新方法包装原来的 `"".Adder.AddVal`,然后将对原始方法`"".Adder.AddVal`的调用替换为对新方法 `"".(*Adder).AddVal` 的调用。 221 | 包装方法唯一的任务,就是保证 receiver 被正确的解引用,并将解引用后的值和其它参数以及返回值在原始方法和调用方法之间拷贝来拷贝去。 222 | 223 | (*NOTE: 在汇编输出中,我们生成的这个包装方法都会被标记上 ``.*) 224 | 225 | 下面的汇编代码对整个包装方法的过程进行了注释,应该能帮你搞明白这个过程: 226 | ```Assembly 227 | 0x0000 TEXT "".(*Adder).AddVal(SB), DUPOK|WRAPPER, $32-24 228 | ;; ...omitted preambles... 229 | 230 | 0x0026 MOVQ ""..this+40(SP), AX ;; check whether the receiver.. 231 | 0x002b TESTQ AX, AX ;; ..is nil 232 | 0x002e JEQ 92 ;; if it is, jump to 0x005c (panic) 233 | 234 | 0x0030 MOVL (AX), AX ;; dereference pointer receiver.. 235 | 0x0032 MOVL AX, (SP) ;; ..and move (i.e. copy) the resulting value to argument #1 236 | 237 | ;; forward (copy) arguments #2 & #3 then call the wrappee 238 | 0x0035 MOVL "".a+48(SP), AX 239 | 0x0039 MOVL AX, 4(SP) 240 | 0x003d MOVL "".b+52(SP), AX 241 | 0x0041 MOVL AX, 8(SP) 242 | 0x0045 CALL "".Adder.AddVal(SB) ;; call the wrapped method 243 | 244 | ;; copy return value from wrapped method then return 245 | 0x004a MOVL 16(SP), AX 246 | 0x004e MOVL AX, "".~r2+56(SP) 247 | ;; ...omitted frame-pointer stuff... 248 | 0x005b RET 249 | 250 | ;; throw a panic with a detailed error 251 | 0x005c CALL runtime.panicwrap(SB) 252 | 253 | ;; ...omitted epilogues... 254 | ``` 255 | 256 | 显然的,这种包装行为会引入一些成本,因为我们需要将参数拷贝来拷贝去;当原始方法的指令比较少时,这种消耗就是值得考量的了。 257 | 幸运的是,实际情况下编译器会将被包装的方法直接内联到包装方法中来避免这些拷贝消耗(在可行的情况下)。 258 | 259 | 注意符号定义中的 `WRAPPER` 指令,该指令表明这个方法不应该在 backtraces 中出现(避免干扰用户),也不能从原始方法的 panic 中 recover。 260 | 261 | > WRAPPER: 这是一个包装函数并且不应该被禁用recover。 262 | 263 | > WRAPPER: This is a wrapper function and should not count as disabling recover. 264 | 265 | `runtime.panicwrap` 函数,在包装方法的 receiver 是 `nil` 时会 panic,代码浅显易懂;下面是完整的内容 ([src/runtime/error.go](https://github.com/golang/go/blob/bf86aec25972f3a100c3aa58a6abcbcc35bdea49/src/runtime/error.go#L132-L157)): 266 | 267 | ```Go 268 | // panicwrap generates a panic for a call to a wrapped value method 269 | // with a nil pointer receiver. 270 | // 271 | // It is called from the generated wrapper code. 272 | func panicwrap() { 273 | pc := getcallerpc() 274 | name := funcname(findfunc(pc)) 275 | // name is something like "main.(*T).F". 276 | // We want to extract pkg ("main"), typ ("T"), and meth ("F"). 277 | // Do it by finding the parens. 278 | i := stringsIndexByte(name, '(') 279 | if i < 0 { 280 | throw("panicwrap: no ( in " + name) 281 | } 282 | pkg := name[:i-1] 283 | if i+2 >= len(name) || name[i-1:i+2] != ".(*" { 284 | throw("panicwrap: unexpected string after package name: " + name) 285 | } 286 | name = name[i+2:] 287 | i = stringsIndexByte(name, ')') 288 | if i < 0 { 289 | throw("panicwrap: no ) in " + name) 290 | } 291 | if i+2 >= len(name) || name[i:i+2] != ")." { 292 | throw("panicwrap: unexpected string after type name: " + name) 293 | } 294 | typ := name[:i] 295 | meth := name[i+2:] 296 | panic(plainError("value method " + pkg + "." + typ + "." + meth + " called using nil *" + typ + " pointer")) 297 | } 298 | ``` 299 | 300 | 这些就是所有函数和方法的调用方式了,下面我们来研究主菜: interface。 301 | 302 | ## 解构接口 303 | 304 | ### 数据结构概述 305 | 306 | 开始理解 interface 如何工作之前,我们需要先对组成 interface 的数据结构和其在内存中的布局建立基础的心智模型。 307 | 为了达到目的,我们先对 runtime 包里相关的代码做简单的阅览,从 Go 语言实现的角度上来看看 interface 到底长什么样。 308 | 309 | **`iface` 结构体** 310 | 311 | `iface` 是 runtime 中对 interface 进行表示的根类型 ([src/runtime/runtime2.go](https://github.com/golang/go/blob/bf86aec25972f3a100c3aa58a6abcbcc35bdea49/src/runtime/runtime2.go#L143-L146))。 312 | 它的定义长这样: 313 | ```Go 314 | type iface struct { // 16 bytes on a 64bit arch 315 | tab *itab 316 | data unsafe.Pointer 317 | } 318 | ``` 319 | 320 | 一个 interface 就是这样一个非常简单的结构体,内部维护两个指针: 321 | - `tab` 持有 `itab` 对象的地址,该对象内嵌了描述 interface 类型和其指向的数据类型的数据结构。 322 | - `data` 是一个 raw (i.e. `unsafe`) pointer,指向 interface 持有的具体的值。 323 | 324 | 虽然很简单,不过数据结构的定义已经提供了重要的信息: 由于 interface 只能持有指针,*任何用 interface 包装的具体类型,都会被取其地址*。 325 | 这样多半会导致一次堆上的内存分配,编译器会保守地让 receiver 逃逸。 326 | 即使是标量类型,也不例外! 327 | 328 | 只需要几行代码就可以对上述结论进行证明 ([escape.go](./escape.go)): 329 | ```Go 330 | type Addifier interface{ Add(a, b int32) int32 } 331 | 332 | type Adder struct{ name string } 333 | //go:noinline 334 | func (adder Adder) Add(a, b int32) int32 { return a + b } 335 | 336 | func main() { 337 | adder := Adder{name: "myAdder"} 338 | adder.Add(10, 32) // doesn't escape 339 | Addifier(adder).Add(10, 32) // escapes 340 | } 341 | ``` 342 | ```Bash 343 | $ GOOS=linux GOARCH=amd64 go tool compile -m escape.go 344 | escape.go:13:10: Addifier(adder) escapes to heap 345 | # ... 346 | ``` 347 | 348 | 这个分配操作还可以直接通过简单的 benchmark 来可视化 ([escape_test.go](./escape_test.go)): 349 | ```Go 350 | func BenchmarkDirect(b *testing.B) { 351 | adder := Adder{id: 6754} 352 | for i := 0; i < b.N; i++ { 353 | adder.Add(10, 32) 354 | } 355 | } 356 | 357 | func BenchmarkInterface(b *testing.B) { 358 | adder := Adder{id: 6754} 359 | for i := 0; i < b.N; i++ { 360 | Addifier(adder).Add(10, 32) 361 | } 362 | } 363 | ``` 364 | ```Bash 365 | $ GOOS=linux GOARCH=amd64 go tool compile -m escape_test.go 366 | # ... 367 | escape_test.go:22:11: Addifier(adder) escapes to heap 368 | # ... 369 | ``` 370 | ```Bash 371 | $ GOOS=linux GOARCH=amd64 go test -bench=. -benchmem ./escape_test.go 372 | BenchmarkDirect-8 2000000000 1.60 ns/op 0 B/op 0 allocs/op 373 | BenchmarkInterface-8 100000000 15.0 ns/op 4 B/op 1 allocs/op 374 | ``` 375 | 376 | 能够明显地看到每次我们创建新的 `Addifier` 接口并用 `adder` 变量初始化它时,都会发生一次堆内存分配 `sizeof(Addr)` 字节。 377 | 本章晚些时候,我们将会研究简单的标量类型在和 interface 结合时,是如何导致堆内存分配的。 378 | 379 | 现在先把注意力集中在下一个数据结构上: `itab`。 380 | 381 | **`itab` 结构** 382 | 383 | `itab` 是这样定义的 ([src/runtime/runtime2.go](https://github.com/golang/go/blob/bf86aec25972f3a100c3aa58a6abcbcc35bdea49/src/runtime/runtime2.go#L648-L658)): 384 | ```Go 385 | type itab struct { // 40 bytes on a 64bit arch 386 | inter *interfacetype 387 | _type *_type 388 | hash uint32 // copy of _type.hash. Used for type switches. 389 | _ [4]byte 390 | fun [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter. 391 | } 392 | ``` 393 | 394 | `itab` 是 interface 的核心。 395 | 396 | 首先,`itab` 内嵌了 `_type`,`_type` 这个类型是 runtime 对任意 Go 语言类型的内部表示。 397 | `_type` 类型描述了一个“类型”的每一个方面: 类型名字,特性(e.g. 大小,对齐方式...),某种程度上类型的行为(e.g. 比较,哈希...) 也包含在内了。 398 | 在这个例子中,`_type` 字段描述了 interface 所持有的值的类型,`data` 指针所指向的值的类型。 399 | 400 | 其次,我们找到了一个指向 `interfacetype` 的指针,这只是一个包装了 `_type` 和额外的与 interface 相关的信息的字段。 401 | 像你所期望的一样,`inter` 字段描述了 interface 本身的类型。 402 | 403 | 最后,`func` 数组持有组成该 interface 虚(virtual/dispatch)函数表的的函数的指针。 404 | 注意这里的注释中说 `// variable sized` 即“变长”,这表示这里数组所声明的长度是 *非精确*的。 405 | 本章我们就会看到,编译器对该数组的空间分配负责,并且其分配操作所用的大小值和这里表示的大小值是不匹配的。同样的,runtime 会始终使用 raw pointer 来访问这段内存,边界检查在这里不会生效。 406 | 407 | **`_type` 结构** 408 | 409 | 如上所述,`_type` 结构对 Go 的类型给出了完成的描述。 410 | 其定义在 ([src/runtime/type.go](https://github.com/golang/go/blob/bf86aec25972f3a100c3aa58a6abcbcc35bdea49/src/runtime/type.go#L25-L43)): 411 | ```Go 412 | type _type struct { // 48 bytes on a 64bit arch 413 | size uintptr 414 | ptrdata uintptr // size of memory prefix holding all pointers 415 | hash uint32 416 | tflag tflag 417 | align uint8 418 | fieldalign uint8 419 | kind uint8 420 | alg *typeAlg 421 | // gcdata stores the GC type data for the garbage collector. 422 | // If the KindGCProg bit is set in kind, gcdata is a GC program. 423 | // Otherwise it is a ptrmask bitmap. See mbitmap.go for details. 424 | gcdata *byte 425 | str nameOff 426 | ptrToThis typeOff 427 | } 428 | ``` 429 | 430 | 还好这里大多数的字段名字都做到了自解释。 431 | 432 | `nameOff` 和 `typeOff` 类型是 `int32` ,这两个值是链接器负责嵌入的,相对于可执行文件的元信息的偏移量。元信息会在运行期,加载到 `runtime.moduledata` 结构体中 ([src/runtime/symtab.go](https://github.com/golang/go/blob/bf86aec25972f3a100c3aa58a6abcbcc35bdea49/src/runtime/symtab.go#L352-L393)), 如果你曾经研究过 ELF 文件的内容的话,看起来会显得很熟悉。 433 | runtime 提供了一些 helper 函数,这些函数能够帮你找到相对于 `moduledata` 的偏移量,比如 `resolveNameOff` ([src/runtime/type.go](https://github.com/golang/go/blob/bf86aec25972f3a100c3aa58a6abcbcc35bdea49/src/runtime/type.go#L168-L196)) and `resolveTypeOff` ([src/runtime/type.go](https://github.com/golang/go/blob/bf86aec25972f3a100c3aa58a6abcbcc35bdea49/src/runtime/type.go#L202-L236)): 434 | ```Go 435 | func resolveNameOff(ptrInModule unsafe.Pointer, off nameOff) name {} 436 | func resolveTypeOff(ptrInModule unsafe.Pointer, off typeOff) *_type {} 437 | ``` 438 | 也就是说,假设 `t` 是 `_type` 的话,只要调用 `resolveTypeOff(t, t.ptrToThis)` 就可以返回 `t` 的一份拷贝了。 439 | 440 | **`interfacetype` 结构体** 441 | 442 | 最后是 `interfacetype` 结构体 ([src/runtime/type.go](https://github.com/golang/go/blob/bf86aec25972f3a100c3aa58a6abcbcc35bdea49/src/runtime/type.go#L342-L346)): 443 | ```Go 444 | type interfacetype struct { // 80 bytes on a 64bit arch 445 | typ _type 446 | pkgpath name 447 | mhdr []imethod 448 | } 449 | 450 | type imethod struct { 451 | name nameOff 452 | ityp typeOff 453 | } 454 | ``` 455 | 456 | 像之前提过的,`interfacetype` 只是对于 `_type` 的一种包装,在其顶部空间还包装了额外的 interface 相关的元信息。 457 | 在最近的实现中,这部分元信息一般是由一些指向相应名字的 offset 的列表和 interface 所暴露的方法的类型所组成(`[]imethod`)。 458 | 459 | **结论** 460 | 461 | 下面是对 `iface` 的一份总览,我们把所有的子类型都做了展开;这样应该能够更好地帮助我们融会贯通: 462 | ```Go 463 | type iface struct { // `iface` 464 | tab *struct { // `itab` 465 | inter *struct { // `interfacetype` 466 | typ struct { // `_type` 467 | size uintptr 468 | ptrdata uintptr 469 | hash uint32 470 | tflag tflag 471 | align uint8 472 | fieldalign uint8 473 | kind uint8 474 | alg *typeAlg 475 | gcdata *byte 476 | str nameOff 477 | ptrToThis typeOff 478 | } 479 | pkgpath name 480 | mhdr []struct { // `imethod` 481 | name nameOff 482 | ityp typeOff 483 | } 484 | } 485 | _type *struct { // `_type` 486 | size uintptr 487 | ptrdata uintptr 488 | hash uint32 489 | tflag tflag 490 | align uint8 491 | fieldalign uint8 492 | kind uint8 493 | alg *typeAlg 494 | gcdata *byte 495 | str nameOff 496 | ptrToThis typeOff 497 | } 498 | hash uint32 499 | _ [4]byte 500 | fun [1]uintptr 501 | } 502 | data unsafe.Pointer 503 | } 504 | ``` 505 | 506 | 本小节对组成 interface 的不同的数据类型进行了介绍,使我们建立了接口相关知识的心智模型,并帮我们了解了这些部件如何协同工作。 507 | 在下一节中,我们将会学习这些数据结构如何辅助计算。 508 | 509 | ### 创建接口 510 | 511 | 我们已经对 interface 的内部数据结构进行了快速学习,接下来主要聚焦在他们如何被分配以及如何初始化。 512 | 513 | 看一下下面的程序 ([iface.go](./iface.go)): 514 | ```Go 515 | type Mather interface { 516 | Add(a, b int32) int32 517 | Sub(a, b int64) int64 518 | } 519 | 520 | type Adder struct{ id int32 } 521 | //go:noinline 522 | func (adder Adder) Add(a, b int32) int32 { return a + b } 523 | //go:noinline 524 | func (adder Adder) Sub(a, b int64) int64 { return a - b } 525 | 526 | func main() { 527 | m := Mather(Adder{id: 6754}) 528 | 529 | // This call just makes sure that the interface is actually used. 530 | // Without this call, the linker would see that the interface defined above 531 | // is in fact never used, and thus would optimize it out of the final 532 | // executable. 533 | m.Add(10, 32) 534 | } 535 | ``` 536 | 537 | *NOTE: 本章的剩余部分,我们演示一个持有 `T` 类型内容 `I` 类型的 interface,即 ``。比如这里的 `Mather(Adder{id: 6754})` 就实例化了一个 `iface`。 538 | 539 | 主要聚焦在 `iface` 的实例化: 540 | ```Go 541 | m := Mather(Adder{id: 6754}) 542 | ``` 543 | 这一行代码内就完成了很多机关,编译器生成的汇编可以证明: 544 | ```Assembly 545 | ;; part 1: allocate the receiver 546 | 0x001d MOVL $6754, ""..autotmp_1+36(SP) 547 | ;; part 2: set up the itab 548 | 0x0025 LEAQ go.itab."".Adder,"".Mather(SB), AX 549 | 0x002c MOVQ AX, (SP) 550 | ;; part 3: set up the data 551 | 0x0030 LEAQ ""..autotmp_1+36(SP), AX 552 | 0x0035 MOVQ AX, 8(SP) 553 | 0x003a CALL runtime.convT2I32(SB) 554 | 0x003f MOVQ 16(SP), AX 555 | 0x0044 MOVQ 24(SP), CX 556 | ``` 557 | 558 | 像你所看到的,我们将输出划分成了三个逻辑部分。 559 | 560 | **Part 1: 分配 receiver 的空间** 561 | 562 | ```Assembly 563 | 0x001d MOVL $6754, ""..autotmp_1+36(SP) 564 | ``` 565 | 566 | 十进制常量 `6754` 对应的是我们 `Adder` 的 ID,被存储在当前栈帧的起始位置。 567 | 之后编译器就可以根据它的存储位置来用地址对其进行引用了;我们会在 part 3 中看到原因。 568 | 569 | **Part 2: 创建 itab** 570 | 571 | ```Assembly 572 | 0x0025 LEAQ go.itab."".Adder,"".Mather(SB), AX 573 | 0x002c MOVQ AX, (SP) 574 | ``` 575 | 576 | 看起来编译器已经创建了必要的 `itab` 来表示我们的 `iface` interface,并以全局符号 `go.itab."".Adder,"".Mather` 提供给我们使用。 577 | 578 | 我们正在执行创建 `iface` interface 的流程中,为了能够完成工作,我们将该全局变量 `go.itab."".Adder,"".Mather` 的地址使用 LEAQ 指令从 AX 寄存器 load 到栈帧顶。 579 | 这段行为的原因我们也会在 part 3 中解释。 580 | 581 | 文法上,我们可以用下面这行伪代码来代替上面的几行代码: 582 | ```Go 583 | tab := getSymAddr(`go.itab.main.Adder,main.Mather`).(*itab) 584 | ``` 585 | 到此已经完成了我们 interface 的一半工作了。 586 | 587 | 我们来更深入地研究一下 `go.itab."".Adder,"".Mather` 这个符号。 588 | 像往常一样,编译器的 `-S` flag 已经告诉了我们很多信息: 589 | ``` 590 | $ GOOS=linux GOARCH=amd64 go tool compile -S iface.go | grep -A 7 '^go.itab."".Adder,"".Mather' 591 | go.itab."".Adder,"".Mather SRODATA dupok size=40 592 | 0x0000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 593 | 0x0010 8a 3d 5f 61 00 00 00 00 00 00 00 00 00 00 00 00 .=_a............ 594 | 0x0020 00 00 00 00 00 00 00 00 ........ 595 | rel 0+8 t=1 type."".Mather+0 596 | rel 8+8 t=1 type."".Adder+0 597 | rel 24+8 t=1 "".(*Adder).Add+0 598 | rel 32+8 t=1 "".(*Adder).Sub+0 599 | ``` 600 | 601 | 代码很整洁。我们来一句一句地分析一下。 602 | 603 | 第一句声明了符号和它的属性: 604 | ``` 605 | go.itab."".Adder,"".Mather SRODATA dupok size=40 606 | ``` 607 | 和通常一样,由于我们看的是编译器生成的间接目标文件(i.e. 即链接器还没有运行),符号名还没有把 package 名字填充上。其它的没啥新东西。 608 | 除此之外,我们这里得到的是一个 40-字节的全局对象的符号,该符号将被存到二进制文件的 `.rodata` 段中。 609 | 610 | 注意这里的 `dupok` 指令,这个指令会告诉链接器如果这个符号在链接期出现多次的话是 ok 的: 链接器将随意选择其中的一个。 611 | 是什么让 Go 的作者们认为这个符号会出现重复,我不是很清楚。如果你了解更多细节的话,欢迎开一个 issue 来讨论。 612 | 613 | 下面这段代码是和该符号相关的 hexdump 的 40 个字节的数据。也就是说,这是一个 `itab` 结构体被序列化之后的表示方法。 614 | ``` 615 | 0x0000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 616 | 0x0010 8a 3d 5f 61 00 00 00 00 00 00 00 00 00 00 00 00 .=_a............ 617 | 0x0020 00 00 00 00 00 00 00 00 ........ 618 | ``` 619 | 如你所见,这部分数据大部分是由 0 组成的。链接器会负责填充这些 0,我们马上就会看到这些是怎么完成的。 620 | 621 | 注意在这些 0 中,在 offset `0x10+4` 的位置,有 4 个字节的值被设置过了。 622 | 回忆一下 `itab` 结构体的声明,并在在对应的字段上打上注释: 623 | 624 | ```Go 625 | type itab struct { // 40 bytes on a 64bit arch 626 | inter *interfacetype // offset 0x00 ($00) 627 | _type *_type // offset 0x08 ($08) 628 | hash uint32 // offset 0x10 ($16) 629 | _ [4]byte // offset 0x14 ($20) 630 | fun [1]uintptr // offset 0x18 ($24) 631 | // offset 0x20 ($32) 632 | } 633 | ``` 634 | 可以看到 offset `0x10+4` 和 `hash uint32` 字段是匹配的: 也就是说,对应 `main.Adder` 类型的 hash 值已经在我们的目标文件中了。 635 | 636 | 第三即最后一部分列出了提供给链接器的重定向指令: 637 | ``` 638 | rel 0+8 t=1 type."".Mather+0 639 | rel 8+8 t=1 type."".Adder+0 640 | rel 24+8 t=1 "".(*Adder).Add+0 641 | rel 32+8 t=1 "".(*Adder).Sub+0 642 | ``` 643 | 644 | `rel 0+8 t=1 type."".Mather+0` 告诉链接器需要将内容的前 8 个字节(`0+8`) 填充为全局目标符号 `type."".Mather` 的地址。 645 | `rel 8+8 t=1 type."".Adder+0` 然后用 `type."".Adder` 的地址填充接下来的 8 个字节,之后类似。 646 | 647 | 一旦链接器完成了它的工作,执行完了这些指令,40-字节的序列化后的 `itab` 就完成了。 648 | 总体来讲,我们在看的代码类似下面这些伪代码: 649 | ```Go 650 | tab := getSymAddr(`go.itab.main.Adder,main.Mather`).(*itab) 651 | 652 | // NOTE: The linker strips the `type.` prefix from these symbols when building 653 | // the executable, so the final symbol names in the .rodata section of the 654 | // binary will actually be `main.Mather` and `main.Adder` rather than 655 | // `type.main.Mather` and `type.main.Adder`. 656 | // Don't get tripped up by this when toying around with objdump. 657 | tab.inter = getSymAddr(`type.main.Mather`).(*interfacetype) 658 | tab._type = getSymAddr(`type.main.Adder`).(*_type) 659 | 660 | tab.fun[0] = getSymAddr(`main.(*Adder).Add`).(uintptr) 661 | tab.fun[1] = getSymAddr(`main.(*Adder).Sub`).(uintptr) 662 | ``` 663 | 664 | 我们已经得到了一个完整可用的 `itab`,如果能再有一些相关的数据塞进去,就能得到一个完整的更好的 interface 了。 665 | 666 | **Part 3: 分配数据** 667 | 668 | ```Assembly 669 | 0x0030 LEAQ ""..autotmp_1+36(SP), AX 670 | 0x0035 MOVQ AX, 8(SP) 671 | 0x003a CALL runtime.convT2I32(SB) 672 | 0x003f MOVQ 16(SP), AX 673 | 0x0044 MOVQ 24(SP), CX 674 | ``` 675 | 676 | 在 part 1 我们说过,现在栈顶`(SP)` 保存着 `go.itab."".Adder."".Mather` 的地址(参数 #1)。 677 | 同时 part 2 我们在 `""..autotmp_1+36(SP)` 位置存储了一个十进制常量 `$6754`: 我们用 8(SP) 来将该栈顶下方的该变量(参数 #2) load 到寄存器中。 678 | 679 | 这两个指针是我们传给 `runtime.convT2I32` 函数的两个参数,该函数将最后的步骤粘起来,创建并返回我们完整的 interface。 680 | 我们再仔细看一下 ([src/runtime/iface.go](https://github.com/golang/go/blob/bf86aec25972f3a100c3aa58a6abcbcc35bdea49/src/runtime/iface.go#L433-L451)): 681 | ```Go 682 | func convT2I32(tab *itab, elem unsafe.Pointer) (i iface) { 683 | t := tab._type 684 | /* ...omitted debug stuff... */ 685 | var x unsafe.Pointer 686 | if *(*uint32)(elem) == 0 { 687 | x = unsafe.Pointer(&zeroVal[0]) 688 | } else { 689 | x = mallocgc(4, t, false) 690 | *(*uint32)(x) = *(*uint32)(elem) 691 | } 692 | i.tab = tab 693 | i.data = x 694 | return 695 | } 696 | ``` 697 | 698 | 所以 `runtime.convT2I32` 做了 4 件事情: 699 | 1. 它创建了一个 `iface` 的结构体 `i` (这里学究一点的话,是它的 caller 创建的这个结构体..没啥两样)。 700 | 2. 它将我们刚给 `i.tab` 赋的值赋予了 `itab` 指针。 701 | 3. 它 **在堆上分配了一个 `i.tab._type` 的新对象 `i.tab._type`**,然后将第二个参数 `elem` 指向的值拷贝到这个新对象上。 702 | 4. 将最后的 interface 返回。 703 | 704 | 这个过程比较直截了当,尽管第三步在这种特定 case 下包含了一些 tricky 的实现细节,这些麻烦的细节是因为我们的 `Adder` 是一个标量类型。 705 | 我们会在 [接口的特殊例子](#拥有标量的接口) 一节来研究标量类型和 interface 交互的更多细节。 706 | 707 | 现在我们已经完成了下面这些工作(伪代码): 708 | ```Go 709 | tab := getSymAddr(`go.itab.main.Adder,main.Mather`).(*itab) 710 | elem := getSymAddr(`""..autotmp_1+36(SP)`).(*int32) 711 | 712 | i := runtime.convTI32(tab, unsafe.Pointer(elem)) 713 | 714 | assert(i.tab == tab) 715 | assert(*(*int32)(i.data) == 6754) // same value.. 716 | assert((*int32)(i.data) != elem) // ..but different (al)locations! 717 | ``` 718 | 719 | 总结一下目前所有的内容,这里是一份完整带注释的汇编代码,包含了所有 3 个部分: 720 | ```Assembly 721 | 0x001d MOVL $6754, ""..autotmp_1+36(SP) ;; create an addressable $6754 value at 36(SP) 722 | 0x0025 LEAQ go.itab."".Adder,"".Mather(SB), AX ;; set up go.itab."".Adder,"".Mather.. 723 | 0x002c MOVQ AX, (SP) ;; ..as first argument (tab *itab) 724 | 0x0030 LEAQ ""..autotmp_1+36(SP), AX ;; set up &36(SP).. 725 | 0x0035 MOVQ AX, 8(SP) ;; ..as second argument (elem unsafe.Pointer) 726 | 0x003a CALL runtime.convT2I32(SB) ;; call convT2I32(go.itab."".Adder,"".Mather, &$6754) 727 | 0x003f MOVQ 16(SP), AX ;; AX now holds i.tab (go.itab."".Adder,"".Mather) 728 | 0x0044 MOVQ 24(SP), CX ;; CX now holds i.data (&$6754, somewhere on the heap) 729 | ``` 730 | 记住,这些代码都是 `m := Mather(Adder{id: 6754})` 这一行代码生成的。 731 | 732 | 最终,我们得到了完整的,可以工作的 interface。 733 | 734 | ### 从可执行文件中重建`itab` 735 | 736 | 前一节中,我们从编译器生成的目标文件中 dump 出了 `go.itab."".Adder,"".Mather`,并看到在一串 0 中出现了 hash 值: 737 | ``` 738 | $ GOOS=linux GOARCH=amd64 go tool compile -S iface.go | grep -A 3 '^go.itab."".Adder,"".Mather' 739 | go.itab."".Adder,"".Mather SRODATA dupok size=40 740 | 0x0000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 741 | 0x0010 8a 3d 5f 61 00 00 00 00 00 00 00 00 00 00 00 00 .=_a............ 742 | 0x0020 00 00 00 00 00 00 00 00 ........ 743 | ``` 744 | 745 | 为了能更好地理解链接器如何分配这些数据的地址,我们会研究生成的 ELF 文件,并手动重建组成我们的 `iface` 的 `itab` 的数据。 746 | 747 | 这样可以让我们观察 `itab` 在链接器完成工作之后又是长什么样子的。 748 | 749 | 先来最重要的事情,我们来把 `iface` 编译好: `GOOS=linux GOARCH=amd64 go build -o iface.bin iface.go`。 750 | 751 | **Step 1: Find `.rodata`** 752 | 753 | 我们来打印一下 section 头,以研究 `.rodata` 部分,`readelf` 这个工具可以很方便的完成这件事: 754 | ```Bash 755 | $ readelf -St -W iface.bin 756 | There are 22 section headers, starting at offset 0x190: 757 | 758 | Section Headers: 759 | [Nr] Name 760 | Type Address Off Size ES Lk Inf Al 761 | Flags 762 | [ 0] 763 | NULL 0000000000000000 000000 000000 00 0 0 0 764 | [0000000000000000]: 765 | [ 1] .text 766 | PROGBITS 0000000000401000 001000 04b3cf 00 0 0 16 767 | [0000000000000006]: ALLOC, EXEC 768 | [ 2] .rodata 769 | PROGBITS 000000000044d000 04d000 028ac4 00 0 0 32 770 | [0000000000000002]: ALLOC 771 | ## ...omitted rest of output... 772 | ``` 773 | 我们现在需要的是 section 中十进制的 offset 值,所以结合 linux 的 pipe 来组合一些命令: 774 | ```Bash 775 | $ readelf -St -W iface.bin | \ 776 | grep -A 1 .rodata | \ 777 | tail -n +2 | \ 778 | awk '{print "ibase=16;"toupper($3)}' | \ 779 | bc 780 | 315392 781 | ``` 782 | 783 | 这个输出表示 `fseek` 了 315392 个字节才能让我们定位到 `.rodata` section。 784 | 这下我们就只需要将文件文中 map 到虚拟内存地址中了。 785 | 786 | **Step 2: Find the virtual-memory address (VMA) of `.rodata`** 787 | 788 | VMA 是当我们的二进制文件被 OS load 到内存时,section 被 map 到的虚拟地址。也就是说,这是我们在运行时引用的符号的具体地址。 789 | 790 | 我们关注 VMA,是因为我们没法通过调用 `readelf` 或者 `objdump` 来找到想要的符号的 offset(就我所知)。另一方面,我们想知道的是运行时的符号的虚拟地址。 791 | 只要再进行一些简单的数学运算,我们应该就可以在 VMA 和 offset 之间建立联系,并最终找到我们想要的符号偏移量了。 792 | 793 | 找到 `.rodata` 的 VMA 和寻找它的 offset 没啥区别,只有一列有区别: 794 | ```Bash 795 | $ readelf -St -W iface.bin | \ 796 | grep -A 1 .rodata | \ 797 | tail -n +2 | \ 798 | awk '{print "ibase=16;"toupper($2)}' | \ 799 | bc 800 | 4509696 801 | ``` 802 | 803 | 我们已知的信息: `.rodata` 段的偏移量是 ELF 文件中的 `$315392`(= `0x04d000`) 位置,该位置会在运行期被映射到虚拟地址 `$4509696`(=`0x44d000`)。 804 | 805 | 现在我们需要正在定位的符号的 VMA 和符号的大小: 806 | - VMA 将(间接)允许我们在可执行文件中间接进行定位。 807 | - 其大小将让我们知道找到了 offset 之后,需要读多少个字节的数据就能把其数据读出来。 808 | 809 | **Step 3: Find the VMA & size of `go.itab.main.Adder,main.Mather`** 810 | 811 | `objdump` 对我们有下面这些用处。 812 | 813 | 首先,找到符号: 814 | ```Bash 815 | $ objdump -t -j .rodata iface.bin | grep "go.itab.main.Adder,main.Mather" 816 | 0000000000475140 g O .rodata 0000000000000028 go.itab.main.Adder,main.Mather 817 | ``` 818 | 819 | 然后获取到它的十进制形式的虚拟地址: 820 | ```Bash 821 | $ objdump -t -j .rodata iface.bin | \ 822 | grep "go.itab.main.Adder,main.Mather" | \ 823 | awk '{print "ibase=16;"toupper($1)}' | \ 824 | bc 825 | 4673856 826 | ``` 827 | 828 | 最后获取到十进制的符号大小: 829 | ```Bash 830 | $ objdump -t -j .rodata iface.bin | \ 831 | grep "go.itab.main.Adder,main.Mather" | \ 832 | awk '{print "ibase=16;"toupper($5)}' | \ 833 | bc 834 | 40 835 | ``` 836 | 837 | 所以 `go.itab.main.Adder,main.Mather` 运行时会被映射到 `$4673856`(=`0x475140`) 这个虚拟地址,并且其大小为 40 个字节(我们之前也知道了,这个就是 `itab` 数据结构的大小) 838 | 839 | **Step 4: Find & extract `go.itab.main.Adder,main.Mather`** 840 | 现在我们有了研究二进制文件中 `go.itab.main.Adder,main.Mather` 这个符号所需要的全部要素。 841 | 842 | 下面是对我们已知信息的备忘: 843 | ``` 844 | .rodata offset: 0x04d000 == $315392 845 | .rodata VMA: 0x44d000 == $4509696 846 | 847 | go.itab.main.Adder,main.Mather VMA: 0x475140 == $4673856 848 | go.itab.main.Adder,main.Mather size: 0x24 = $40 849 | ``` 850 | 851 | 如果 `$315392` (`.rodata` 的 offset) 映射到 $4509696 (`.rodata` 的 VMA) 并且 `go.itab.main.Adder,main.Mather` 的 VMA 是 `$4673856`, 然后 `go.itab.main.Adder,main.Mather` 在可执行文件中的的 offset 是: 852 | `sym.offset = sym.vma - section.vma + section.offset = $4673856 - $4509696 + $315392 = $479552`. 853 | 854 | 因为我们已经知道了 offset 和数据的大小,我们可以掏出我们的好伙伴 `dd` 来将这些原始字节直接从可执行文件中搞出来: 855 | ```Bash 856 | $ dd if=iface.bin of=/dev/stdout bs=1 count=40 skip=479552 2>/dev/null | hexdump 857 | 0000000 bd20 0045 0000 0000 ed40 0045 0000 0000 858 | 0000010 3d8a 615f 0000 0000 c2d0 0044 0000 0000 859 | 0000020 c350 0044 0000 0000 860 | 0000028 861 | ``` 862 | 863 | 看起来我们获得了显著的胜利。。不过是真的胜利了么?也许我们只是 dump 出了 40 个随机的字节呢,和我们想要的数据根本无关呢?谁知道呢? 864 | 有一个办法能帮我们确认这件事: 让我们将二进制 dump(offset `0x10+4` -> `0x615f3d8a`) 的 type hash 和 runtime 加载的 ([iface_type_hash.go](./iface_type_hash.go)) 进行对比: 865 | ```Go 866 | // simplified definitions of runtime's iface & itab types 867 | type iface struct { 868 | tab *itab 869 | data unsafe.Pointer 870 | } 871 | type itab struct { 872 | inter uintptr 873 | _type uintptr 874 | hash uint32 875 | _ [4]byte 876 | fun [1]uintptr 877 | } 878 | 879 | func main() { 880 | m := Mather(Adder{id: 6754}) 881 | 882 | iface := (*iface)(unsafe.Pointer(&m)) 883 | fmt.Printf("iface.tab.hash = %#x\n", iface.tab.hash) // 0x615f3d8a 884 | } 885 | ``` 886 | 887 | 匹配上了!`fmt.Printf("iface.tab.hash = %#x\n",iface.tab.hash)` 给了我们 `0x615f3d8a` 这个结果,和我们从 ELF 文件了扒出来的内容是一致的。 888 | 889 | **结论** 890 | 891 | 我们为 `iface` 接口重建了完整的 `itab` 结构;这个结构就躺在我们的可执行文件里等待被使用,并且已经有了 runtime 使 interface 按照需求工作所需要的一切信息。 892 | 893 | 当然,因为 `itab` 大多数时候由一堆指向其它数据结构的指针构成,我们还需要跟踪用 `dd` 扒出来的内容中的虚拟地址才能重建出整个运行图。 894 | 895 | 提到指针的话,我们现在对 `iface` 的虚表已经有了清晰的认识;这里是 `go.itab.main.Adder,main.Mather` 内容的一份注解版本: 896 | ```Bash 897 | $ dd if=iface.bin of=/dev/stdout bs=1 count=40 skip=479552 2>/dev/null | hexdump 898 | 0000000 bd20 0045 0000 0000 ed40 0045 0000 0000 899 | 0000010 3d8a 615f 0000 0000 c2d0 0044 0000 0000 900 | # ^^^^^^^^^^^^^^^^^^^ 901 | # offset 0x18+8: itab.fun[0] 902 | 0000020 c350 0044 0000 0000 903 | # ^^^^^^^^^^^^^^^^^^^ 904 | # offset 0x20+8: itab.fun[1] 905 | 0000028 906 | ``` 907 | ```Bash 908 | $ objdump -t -j .text iface.bin | grep 000000000044c2d0 909 | 000000000044c2d0 g F .text 0000000000000079 main.(*Adder).Add 910 | ``` 911 | ```Bash 912 | $ objdump -t -j .text iface.bin | grep 000000000044c350 913 | 000000000044c350 g F .text 000000000000007f main.(*Adder).Sub 914 | ``` 915 | 916 | 毫无意外,`iface` 的虚表持有了两个方法指针: `main.(*Adder).add` 和 `main.(*Adder).sub`。 917 | 好吧,这里*确实*有一点奇怪: 我们从来没有定义过有指针 receiver 的这两个方法。 918 | 编译器代表我们直接生成了这些包装方法(如之前在 [隐式解引用](#隐式解引用) 中描述的),因为它知道我们会需要这些方法: 因为我们的 `Adder` 实现中只提供了值-receiver 的方法,如果某个时刻,我们通过虚表调用任何一个 interface 中的方法,都会需要这里的包装方法。 919 | 920 | 这里应该已经让你对动态 dispatch 在运行期间如何处理,有了初步的不错理解;下一节我们会研究这个问题。 921 | 922 | **Bonus** 923 | 924 | 我写了一个通用的 bash 脚本,可以直接用来 dump 出 ELF 文件中的任何段的任何符号的内容 ([dump_sym.sh](./dump_sym.sh)): 925 | ```Bash 926 | # ./dump_sym.sh bin_path section_name sym_name 927 | $ ./dump_sym.sh iface.bin .rodata go.itab.main.Adder,main.Mather 928 | .rodata file-offset: 315392 929 | .rodata VMA: 4509696 930 | go.itab.main.Adder,main.Mather VMA: 4673856 931 | go.itab.main.Adder,main.Mather SIZE: 40 932 | 933 | 0000000 bd20 0045 0000 0000 ed40 0045 0000 0000 934 | 0000010 3d8a 615f 0000 0000 c2d0 0044 0000 0000 935 | 0000020 c350 0044 0000 0000 936 | 0000028 937 | ``` 938 | 939 | 按说应该哪里是有什么工具可以提供这个脚本的功能的,可能只要给 `binutils` 里的某个工具传一些诡异的 flag 就可以拿到这些内容。。谁知道呢。 940 | 如果你知道已经有工具提供了这个功能的话,不要犹豫,开 issue 告诉我。 941 | 942 | ## 动态分发 943 | 944 | 本节我们终于要讲 interface 最主要的功能了: 动态分发。 945 | 明确一些,我们主要研究动态分发在底层如何工作,并且使用动态分发有什么样的成本。 946 | 947 | ### 对接口的间接调用 948 | 949 | 再回看一下最初的代码 ([iface.go](./iface.go)): 950 | ```Go 951 | type Mather interface { 952 | Add(a, b int32) int32 953 | Sub(a, b int64) int64 954 | } 955 | 956 | type Adder struct{ id int32 } 957 | //go:noinline 958 | func (adder Adder) Add(a, b int32) int32 { return a + b } 959 | //go:noinline 960 | func (adder Adder) Sub(a, b int64) int64 { return a - b } 961 | 962 | func main() { 963 | m := Mather(Adder{id: 6754}) 964 | m.Add(10, 32) 965 | } 966 | ``` 967 | 我们对这些代码背后所发生的事情已经有了深入的理解: `iface` interface 如何创建,在可执行文件中如何布局,最终如何被 runtime 加载。 968 | 之后就只剩一件事情还需要琢磨,就是对 `m.Add(10, 32)` 的间接调用会做些什么事情了。 969 | 970 | 为了让我们能够回忆起之前学到的内容,我们会同时关注 interface 的创建和方法的调用过程: 971 | ```Go 972 | m := Mather(Adder{id: 6754}) 973 | m.Add(10, 32) 974 | ``` 975 | 还好我们已经有了第一行实例化 (`m := Mather(Adder{id: 6754})`) 时完整带注释的汇编代码: 976 | ```Assembly 977 | ;; m := Mather(Adder{id: 6754}) 978 | 0x001d MOVL $6754, ""..autotmp_1+36(SP) ;; create an addressable $6754 value at 36(SP) 979 | 0x0025 LEAQ go.itab."".Adder,"".Mather(SB), AX ;; set up go.itab."".Adder,"".Mather.. 980 | 0x002c MOVQ AX, (SP) ;; ..as first argument (tab *itab) 981 | 0x0030 LEAQ ""..autotmp_1+36(SP), AX ;; set up &36(SP).. 982 | 0x0035 MOVQ AX, 8(SP) ;; ..as second argument (elem unsafe.Pointer) 983 | 0x003a CALL runtime.convT2I32(SB) ;; runtime.convT2I32(go.itab."".Adder,"".Mather, &$6754) 984 | 0x003f MOVQ 16(SP), AX ;; AX now holds i.tab (go.itab."".Adder,"".Mather) 985 | 0x0044 MOVQ 24(SP), CX ;; CX now holds i.data (&$6754, somewhere on the heap) 986 | ``` 987 | 接着是对方法间接调用 (`m.Add(10, 32)`)的汇编代码: 988 | ```Assembly 989 | ;; m.Add(10, 32) 990 | 0x0049 MOVQ 24(AX), AX 991 | 0x004d MOVQ $137438953482, DX 992 | 0x0057 MOVQ DX, 8(SP) 993 | 0x005c MOVQ CX, (SP) 994 | 0x0060 CALL AX 995 | ``` 996 | 997 | 有了之前几小节积累的知识,这些指令对我们来说就很直白了。 998 | 999 | ```Assembly 1000 | 0x0049 MOVQ 24(AX), AX 1001 | ``` 1002 | `runtime.convT2I32` 一返回,`AX` 中就包含了 `i.tab` 的指针;更准确地说是指向 `go.itab."".Adder."".Mather` 的指针。 1003 | 将 `AX` 解引用,然后向前 offset 24 个字节,我们就可以找到 `i.tab.fun` 的位置了,这个地址对应的是虚表的第一个入口。 1004 | 下面的代码帮我们回忆一下 `itab` 长啥样: 1005 | 1006 | ```Go 1007 | type itab struct { // 32 bytes on a 64bit arch 1008 | inter *interfacetype // offset 0x00 ($00) 1009 | _type *_type // offset 0x08 ($08) 1010 | hash uint32 // offset 0x10 ($16) 1011 | _ [4]byte // offset 0x14 ($20) 1012 | fun [1]uintptr // offset 0x18 ($24) 1013 | // offset 0x20 ($32) 1014 | } 1015 | ``` 1016 | 1017 | 之前小节中我们通过从可执行文件中重建 `itab` 结构,已经知道了 `iface.tab.fun[0]` 是指向 `main.(*Adder).add` 的指针,这是编译器生成的包装方法,该方法会继而调用我们原始的 `main.Adder.add` 方法。 1018 | 1019 | ```Assembly 1020 | 0x004d MOVQ $137438953482, DX 1021 | 0x0057 MOVQ DX, 8(SP) 1022 | ``` 1023 | 将 `10` 和 `32` 作为参数 #2 和 #3 存在栈顶。 1024 | 1025 | ```Assembly 1026 | 0x005c MOVQ CX, (SP) 1027 | 0x0060 CALL AX 1028 | ``` 1029 | `runtime.convT2I32` 一返回, `CX` 寄存器就存了 `i.data`,该指针指向 `Adder` 实例。 1030 | 我们将该指针移动到栈顶,作为参数 #1,为了能够满足调用规约: receiver 必须作为方法的第一个参数传入。 1031 | 1032 | 最后,栈建好了,可以执行函数调用了。 1033 | 1034 | 这里给出完整流程的带注释的汇编代码,作为本节的收尾。 1035 | ```Assembly 1036 | ;; m := Mather(Adder{id: 6754}) 1037 | 0x001d MOVL $6754, ""..autotmp_1+36(SP) ;; create an addressable $6754 value at 36(SP) 1038 | 0x0025 LEAQ go.itab."".Adder,"".Mather(SB), AX ;; set up go.itab."".Adder,"".Mather.. 1039 | 0x002c MOVQ AX, (SP) ;; ..as first argument (tab *itab) 1040 | 0x0030 LEAQ ""..autotmp_1+36(SP), AX ;; set up &36(SP).. 1041 | 0x0035 MOVQ AX, 8(SP) ;; ..as second argument (elem unsafe.Pointer) 1042 | 0x003a CALL runtime.convT2I32(SB) ;; runtime.convT2I32(go.itab."".Adder,"".Mather, &$6754) 1043 | 0x003f MOVQ 16(SP), AX ;; AX now holds i.tab (go.itab."".Adder,"".Mather) 1044 | 0x0044 MOVQ 24(SP), CX ;; CX now holds i.data (&$6754, somewhere on the heap) 1045 | ;; m.Add(10, 32) 1046 | 0x0049 MOVQ 24(AX), AX ;; AX now holds (*iface.tab)+0x18, i.e. iface.tab.fun[0] 1047 | 0x004d MOVQ $137438953482, DX ;; move (32,10) to.. 1048 | 0x0057 MOVQ DX, 8(SP) ;; ..the top of the stack (arguments #3 & #2) 1049 | 0x005c MOVQ CX, (SP) ;; CX, which holds &$6754 (i.e., our receiver), gets moved to 1050 | ;; ..the top of stack (argument #1 -> receiver) 1051 | 0x0060 CALL AX ;; you know the drill 1052 | ``` 1053 | 1054 | 我们对 interface 和虚表的工作所需的所有手段都有了清晰的理解。 1055 | 下一节,将会分别从理论和实践角度,对 interface 的使用成本进行评估。 1056 | 1057 | ### 性能开销 1058 | 1059 | 如我们所见,interface 代理的实现主要是由编译器和链接器组合完成的。从性能的角度来讲,这种行为显然是好消息: runtime 干的活越少越好。 1060 | 但还是有一些极端 case,实例化 interface 也需要 runtime 也参与进来(e.g. `runtime.convT2*` 族的函数),尽管实践上这些函数比较少出现。 1061 | 在 [接口的特殊例子](#特殊例子及编译器技巧) 中我们会知道更多的边缘 case。 1062 | 现在我们还是只聚焦在虚方法的调用成本上,忽略掉初始化的那些一次性成本。 1063 | 1064 | 一旦 interface 被正确地实例化了,调用这个 interface 的方法相比于调用静态分发的方法,就只不过是多走一个间接层的问题了(i.e. 解引用 `itab.fun` 数组中对应索引位置的指针)。 1065 | 因此,可以假设这个过程基本上没啥消耗。。这种假设是对的,但也不完全对: 理论稍微有一些 tricky,事实还更加 tricky 一些。 1066 | 1067 | #### 理论:快速回顾现代CPUs 1068 | 1069 | 虚函数调用的这种间接性在*只要从 CPU 的角度来讲是可以预测的话*,其成本就是可以忽略不计的。 1070 | 现代 CPU 都是非常激进的怪兽: 他们会非常激进地缓存,激进地对指令和数据进行预取,激进地对代码进行预执行,甚至会在可能的时候将这些指令并行化。 1071 | 无论我们是否想要,这些额外的工作都会被完成,因此我们应该不要让自己的程序和 CPU 在这方面的优化背道而驰,以免使 CPU 已经运行过的周期都白白浪费。 1072 | 1073 | 这就是虚方法调用很快变成麻烦问题的地方。 1074 | 1075 | 在静态调用的情况下,CPU 实际上可以提前知道即将执行的程序分支,并根据预测将这些分支的指令提前取到。这样能够最大化地利用 CPU 流水线来将程序的分支都提前执行掉。 1076 | 而在动态分发的情况下,CPU 没有办法提前知道程序会向哪个方向执行: 因为 interface 的特性,不到运行期,没有办法知道到底要取谁的计算结果。为了平衡这一点,CPU 使用了各种各样的探索和算法以猜出程序即将执行的到底是哪一个分支(i.e. 分支预测)。 1077 | 1078 | 如果处理器猜中了,我们就可以期望动态分发的效率和静态分发差不多,由于执行位置的指令已经都被提前取进了处理器的缓存。 1079 | 1080 | 如果处理器猜错了的话,就比较麻烦了: 首先,我们需要额外的指令,还需要从主存中加载数据(这会使 CPU 完全失速)到 L1i 缓存中。也可能更糟糕,我们需要付出 CPU 因自身的错误预测而 flush 掉它的指令流水线的成本。 1081 | 动态分发的另一个缺点是其会使内联从定义上就完全不可能实现了: 都不知道要运行什么东西,自然没有办法内联了。 1082 | 1083 | 总而言之,直接调用内联函数 F 和调用有额外的中间层的这些间接调用,在性能方面,理论上会有很大的差距,甚至还可能触发 CPU 的分支误判。 1084 | 1085 | 这就是从理论上分析出的可能性。讨论现代硬件的话,我们需要知晓上述理论。 1086 | 1087 | 让我们来衡量一下这部分成本。 1088 | 1089 | #### 实践:性能基准 1090 | 1091 | 我们运行 benchmark 的 CPU 信息: 1092 | ```Bash 1093 | $ lscpu | sed -nr '/Model name/ s/.*:\s*(.* @ .*)/\1/p' 1094 | Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz 1095 | ``` 1096 | 1097 | 我们把要进行 benchmark 的 interface 定义成这样 ([iface_bench_test.go](./iface_bench_test.go)): 1098 | ```Go 1099 | type identifier interface { 1100 | idInline() int32 1101 | idNoInline() int32 1102 | } 1103 | 1104 | type id32 struct{ id int32 } 1105 | 1106 | // NOTE: Use pointer receivers so we don't measure the extra overhead incurred by 1107 | // autogenerated wrappers as part of our results. 1108 | 1109 | func (id *id32) idInline() int32 { return id.id } 1110 | //go:noinline 1111 | func (id *id32) idNoInline() int32 { return id.id } 1112 | ``` 1113 | 1114 | **Benchmark suite A: 单实例,多次调用,内联 & 非内联** 1115 | 1116 | 我们开头的两个 benchmark 会尝试在 busy-loop 中调用非内联方法,一个是 `*Adder` 值,另一个是 `iface` 的 interface: 1117 | ```Go 1118 | var escapeMePlease *id32 1119 | // escapeToHeap makes sure that `id` escapes to the heap. 1120 | // 1121 | // In simple situations such as some of the benchmarks present in this file, 1122 | // the compiler is able to statically infer the underlying type of the 1123 | // interface (or rather the type of the data it points to, to be pedantic) and 1124 | // ends up replacing what should have been a dynamic method call by a 1125 | // static call. 1126 | // This anti-optimization prevents this extra cleverness. 1127 | // 1128 | //go:noinline 1129 | func escapeToHeap(id *id32) identifier { 1130 | escapeMePlease = id 1131 | return escapeMePlease 1132 | } 1133 | 1134 | var myID int32 1135 | 1136 | func BenchmarkMethodCall_direct(b *testing.B) { 1137 | b.Run("single/noinline", func(b *testing.B) { 1138 | m := escapeToHeap(&id32{id: 6754}).(*id32) 1139 | for i := 0; i < b.N; i++ { 1140 | // CALL "".(*id32).idNoInline(SB) 1141 | // MOVL 8(SP), AX 1142 | // MOVQ "".&myID+40(SP), CX 1143 | // MOVL AX, (CX) 1144 | myID = m.idNoInline() 1145 | } 1146 | }) 1147 | } 1148 | 1149 | func BenchmarkMethodCall_interface(b *testing.B) { 1150 | b.Run("single/noinline", func(b *testing.B) { 1151 | m := escapeToHeap(&id32{id: 6754}) 1152 | for i := 0; i < b.N; i++ { 1153 | // MOVQ 32(AX), CX 1154 | // MOVQ "".m.data+40(SP), DX 1155 | // MOVQ DX, (SP) 1156 | // CALL CX 1157 | // MOVL 8(SP), AX 1158 | // MOVQ "".&myID+48(SP), CX 1159 | // MOVL AX, (CX) 1160 | myID = m.idNoInline() 1161 | } 1162 | }) 1163 | } 1164 | ``` 1165 | 1166 | 我们期望的结果是,两个 benchmark 在跑 A) 的时候极其快,B) 的速度也差不多。 1167 | 1168 | 考虑到 loop 的紧密性,我们可以期望这两个 benchmark 的循环在每次迭代时,都保证其数据(receiver 和虚函数表)和指令(`"".(*id32).idNoInline`)已经在 CPU 的 L1d/L1i 的缓存中了。也就是说,这里的性能可以认为是 CPU-bound。 1169 | 1170 | `BenchmarkMethodCall_interface` 会稍微慢一些(在纳秒级别的评价范围),因为其需要从虚表(已经在 L1 cache 了)查找并拷贝正确的指针,有一些成本。 1171 | 由于 `CALL CX` 指令对这些额外指令的输出有强依赖,这些指令用来查虚表,处理器没辙,只能把这些逻辑都当作线性的流来处理,而没法对虚表相关的内容进行指令级的并行。 1172 | 1173 | 这是我们会觉得 "interface" 版本运行稍慢的主要原因。 1174 | 1175 | 下面是 "直接" 调用版本的结果: 1176 | ```Bash 1177 | $ go test -run=NONE -o iface_bench_test.bin iface_bench_test.go && \ 1178 | perf stat --cpu=1 \ 1179 | taskset 2 \ 1180 | ./iface_bench_test.bin -test.cpu=1 -test.benchtime=1s -test.count=3 \ 1181 | -test.bench='BenchmarkMethodCall_direct/single/noinline' 1182 | BenchmarkMethodCall_direct/single/noinline 2000000000 1.81 ns/op 1183 | BenchmarkMethodCall_direct/single/noinline 2000000000 1.80 ns/op 1184 | BenchmarkMethodCall_direct/single/noinline 2000000000 1.80 ns/op 1185 | 1186 | Performance counter stats for 'CPU(s) 1': 1187 | 1188 | 11702.303843 cpu-clock (msec) # 1.000 CPUs utilized 1189 | 2,481 context-switches # 0.212 K/sec 1190 | 1 cpu-migrations # 0.000 K/sec 1191 | 7,349 page-faults # 0.628 K/sec 1192 | 43,726,491,825 cycles # 3.737 GHz 1193 | 110,979,100,648 instructions # 2.54 insn per cycle 1194 | 19,646,440,556 branches # 1678.852 M/sec 1195 | 566,424 branch-misses # 0.00% of all branches 1196 | 1197 | 11.702332281 seconds time elapsed 1198 | ``` 1199 | 下面是 "interface" 的版本: 1200 | ```Bash 1201 | $ go test -run=NONE -o iface_bench_test.bin iface_bench_test.go && \ 1202 | perf stat --cpu=1 \ 1203 | taskset 2 \ 1204 | ./iface_bench_test.bin -test.cpu=1 -test.benchtime=1s -test.count=3 \ 1205 | -test.bench='BenchmarkMethodCall_interface/single/noinline' 1206 | BenchmarkMethodCall_interface/single/noinline 2000000000 1.95 ns/op 1207 | BenchmarkMethodCall_interface/single/noinline 2000000000 1.96 ns/op 1208 | BenchmarkMethodCall_interface/single/noinline 2000000000 1.96 ns/op 1209 | 1210 | Performance counter stats for 'CPU(s) 1': 1211 | 1212 | 12709.383862 cpu-clock (msec) # 1.000 CPUs utilized 1213 | 3,003 context-switches # 0.236 K/sec 1214 | 1 cpu-migrations # 0.000 K/sec 1215 | 10,524 page-faults # 0.828 K/sec 1216 | 47,301,533,147 cycles # 3.722 GHz 1217 | 124,467,105,161 instructions # 2.63 insn per cycle 1218 | 19,878,711,448 branches # 1564.097 M/sec 1219 | 761,899 branch-misses # 0.00% of all branches 1220 | 1221 | 12.709412950 seconds time elapsed 1222 | ``` 1223 | 1224 | 结果与我们的期望是相符的: "interface" 版本确实稍慢一些,每个迭代慢 0.15 纳秒,或者说慢了 ~8%。 1225 | 8% 一开始听着还挺吓人,但我们需要知道 A) 这个 benchmark 是纳秒级的评估,并且 B) 这个被调用的方法除了被调用之外没有做任何实质性的工作,从而夸大了这个差距。 1226 | 1227 | 观察一下两个 benchmark 的指令数目,我们可以看到基于 interface 的版本比 "直接" 调用的版本多了 ~140 亿条指令(`110,979,100,648` vs. `124,467,105,161`),即使 benchmark 本身只运行了 `6,000,000,000` (`2,000,000,000\*3`) 次迭代。 1228 | 我们之前提过,CPU 没有办法让这些指令并行化,因为 `CALL` 依赖这些指令,这一点在 “每周期指令比例” 上得到了充分的反映: 两个 benchmark 都得到了相似的 IPC(instruction-per-cycle) 比例,虽然 interface 版本整体上需要干更多的活儿。 1229 | 1230 | 缺乏并行的结果最终堆积结果就是造成了 interface 版本的额外的 ~35 亿 CPU 循环周期,这也是这额外的 0.15ns 具体消耗在的地方。 1231 | 1232 | 如果我们让编译器把这个方法调用内联的话,会发生什么呢? 1233 | 1234 | ```Go 1235 | var myID int32 1236 | 1237 | func BenchmarkMethodCall_direct(b *testing.B) { 1238 | b.Run("single/inline", func(b *testing.B) { 1239 | m := escapeToHeap(&id32{id: 6754}).(*id32) 1240 | b.ResetTimer() 1241 | for i := 0; i < b.N; i++ { 1242 | // MOVL (DX), SI 1243 | // MOVL SI, (CX) 1244 | myID = m.idInline() 1245 | } 1246 | }) 1247 | } 1248 | 1249 | func BenchmarkMethodCall_interface(b *testing.B) { 1250 | b.Run("single/inline", func(b *testing.B) { 1251 | m := escapeToHeap(&id32{id: 6754}) 1252 | b.ResetTimer() 1253 | for i := 0; i < b.N; i++ { 1254 | // MOVQ 32(AX), CX 1255 | // MOVQ "".m.data+40(SP), DX 1256 | // MOVQ DX, (SP) 1257 | // CALL CX 1258 | // MOVL 8(SP), AX 1259 | // MOVQ "".&myID+48(SP), CX 1260 | // MOVL AX, (CX) 1261 | myID = m.idNoInline() 1262 | } 1263 | }) 1264 | } 1265 | ``` 1266 | 1267 | 两件事情浮现在面前: 1268 | - `BenchmarkMethodCall_direct`: 感谢内联,调用被简化为一对简单的内存移动指令了。 1269 | - `BenchmarkMethodCall_interface`: 因为动态分发的关系,编译器没办法进行调用内联,因此生成的汇编和之前是完全一样了。 1270 | 1271 | 几乎不用修改 `BenchmarkMethodCall_interface` 的代码,因为原来的代码也没啥变化。 1272 | 1273 | 快速阅览一下"直接"调用的版本: 1274 | ```Bash 1275 | $ go test -run=NONE -o iface_bench_test.bin iface_bench_test.go && \ 1276 | perf stat --cpu=1 \ 1277 | taskset 2 \ 1278 | ./iface_bench_test.bin -test.cpu=1 -test.benchtime=1s -test.count=3 \ 1279 | -test.bench='BenchmarkMethodCall_direct/single/inline' 1280 | BenchmarkMethodCall_direct/single/inline 2000000000 0.35 ns/op 1281 | BenchmarkMethodCall_direct/single/inline 2000000000 0.34 ns/op 1282 | BenchmarkMethodCall_direct/single/inline 2000000000 0.34 ns/op 1283 | 1284 | Performance counter stats for 'CPU(s) 1': 1285 | 1286 | 2464.353001 cpu-clock (msec) # 1.000 CPUs utilized 1287 | 629 context-switches # 0.255 K/sec 1288 | 1 cpu-migrations # 0.000 K/sec 1289 | 7,322 page-faults # 0.003 M/sec 1290 | 9,026,867,915 cycles # 3.663 GHz 1291 | 41,580,825,875 instructions # 4.61 insn per cycle 1292 | 7,027,066,264 branches # 2851.485 M/sec 1293 | 1,134,955 branch-misses # 0.02% of all branches 1294 | 1295 | 2.464386341 seconds time elapsed 1296 | ``` 1297 | 1298 | 如我所料,运行地飞快,调用的消耗几乎没有了。 1299 | 被内联的"直接"调用的版本每次需要 ~0.34ns,“interface” 的版本慢了 ~475%,相比前面的 8% 是断崖般的性能差别。 1300 | 1301 | 注意,方法固有的分支消失了,从而使 CPU 能够闲下来更有效地并行执行剩下的指令,达到了 4.61 的 IPC ratio。 1302 | 1303 | **Benchmark suite B: 多实例,很多非内联调用,small/big/peseudo-random 迭代** 1304 | 1305 | 这第二个 benchmark 系列,主要研究真实世界的场景,比如对象 slice 的迭代的同时进行方法调用。 1306 | 为了更好的模仿真实场景,我们把内联关闭掉,真实世界的函数都足够复杂,大概率不会被编译器内联(也是存在像标准库中 `sort.Interface` interface 这样的反例的)。 1307 | 1308 | 先定义 3 个类似的 benchmark,只在访问对象 slice 上稍有差别;目的是为了模拟对 cache 亲和性的逐级降低: 1309 | 1. 第一个 case,迭代器按顺序访问数组,调用方法,然后访问完一个元素之后加一。 1310 | 1. 第二个 case,迭代器依然按顺序访问数组,不过这次其增加的数值比单行的 cache-line 大一些。 1311 | 1. 最后一个 case,迭代器按伪随机的步骤来访问 slice。 1312 | 1313 | 所有的三种 case,我们都需要确保数组足够大,不要能直接装进处理器的 cache 中,以模拟(不太精确)一个忙碌的服务器,正在 CPU cache 和主存都承载高压力的情况下运作。 1314 | 1315 | 下面是对处理器属性的复述,我们会根据这些信息设计我们的 benchmark: 1316 | ```Bash 1317 | $ lscpu | sed -nr '/Model name/ s/.*:\s*(.* @ .*)/\1/p' 1318 | Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz 1319 | $ lscpu | grep cache 1320 | L1d cache: 32K 1321 | L1i cache: 32K 1322 | L2 cache: 256K 1323 | L3 cache: 6144K 1324 | $ getconf LEVEL1_DCACHE_LINESIZE 1325 | 64 1326 | $ getconf LEVEL1_ICACHE_LINESIZE 1327 | 64 1328 | $ find /sys/devices/system/cpu/cpu0/cache/index{1,2,3} -name "shared_cpu_list" -exec cat {} \; 1329 | # (annotations are mine) 1330 | 0,4 # L1 (hyperthreading) 1331 | 0,4 # L2 (hyperthreading) 1332 | 0-7 # L3 (shared + hyperthreading) 1333 | ``` 1334 | 1335 | 这是 "直接" 调用的 benchmark 系列(benchmark 标记为 `baseline` 计算的是独立获取 receiver 的成本),这样我们能从最终的结果把消耗扣出来: 1336 | ```Go 1337 | const _maxSize = 2097152 // 2^21 1338 | const _maxSizeModMask = _maxSize - 1 // avoids a mod (%) in the hot path 1339 | 1340 | var _randIndexes = [_maxSize]int{} 1341 | func init() { 1342 | rand.Seed(42) 1343 | for i := range _randIndexes { 1344 | _randIndexes[i] = rand.Intn(_maxSize) 1345 | } 1346 | } 1347 | 1348 | func BenchmarkMethodCall_direct(b *testing.B) { 1349 | adders := make([]*id32, _maxSize) 1350 | for i := range adders { 1351 | adders[i] = &id32{id: int32(i)} 1352 | } 1353 | runtime.GC() 1354 | 1355 | var myID int32 1356 | 1357 | b.Run("many/noinline/small_incr", func(b *testing.B) { 1358 | var m *id32 1359 | b.Run("baseline", func(b *testing.B) { 1360 | for i := 0; i < b.N; i++ { 1361 | m = adders[i&_maxSizeModMask] 1362 | } 1363 | }) 1364 | b.Run("call", func(b *testing.B) { 1365 | for i := 0; i < b.N; i++ { 1366 | m = adders[i&_maxSizeModMask] 1367 | myID = m.idNoInline() 1368 | } 1369 | }) 1370 | }) 1371 | b.Run("many/noinline/big_incr", func(b *testing.B) { 1372 | var m *id32 1373 | b.Run("baseline", func(b *testing.B) { 1374 | j := 0 1375 | for i := 0; i < b.N; i++ { 1376 | m = adders[j&_maxSizeModMask] 1377 | j += 32 1378 | } 1379 | }) 1380 | b.Run("call", func(b *testing.B) { 1381 | j := 0 1382 | for i := 0; i < b.N; i++ { 1383 | m = adders[j&_maxSizeModMask] 1384 | myID = m.idNoInline() 1385 | j += 32 1386 | } 1387 | }) 1388 | }) 1389 | b.Run("many/noinline/random_incr", func(b *testing.B) { 1390 | var m *id32 1391 | b.Run("baseline", func(b *testing.B) { 1392 | for i := 0; i < b.N; i++ { 1393 | m = adders[_randIndexes[i&_maxSizeModMask]] 1394 | } 1395 | }) 1396 | b.Run("call", func(b *testing.B) { 1397 | for i := 0; i < b.N; i++ { 1398 | m = adders[_randIndexes[i&_maxSizeModMask]] 1399 | myID = m.idNoInline() 1400 | } 1401 | }) 1402 | }) 1403 | } 1404 | ``` 1405 | "interface" 版本的 benchmark 系列完全一样,除了数组是由 interface 而不是指向具体类型的指针: 1406 | ```Go 1407 | func BenchmarkMethodCall_interface(b *testing.B) { 1408 | adders := make([]identifier, _maxSize) 1409 | for i := range adders { 1410 | adders[i] = identifier(&id32{id: int32(i)}) 1411 | } 1412 | runtime.GC() 1413 | 1414 | /* ... */ 1415 | } 1416 | ``` 1417 | 1418 | “直接”调用的系列,我们得到如下结果: 1419 | ```Bash 1420 | $ go test -run=NONE -o iface_bench_test.bin iface_bench_test.go && \ 1421 | benchstat <( 1422 | taskset 2 ./iface_bench_test.bin -test.cpu=1 -test.benchtime=1s -test.count=3 \ 1423 | -test.bench='BenchmarkMethodCall_direct/many/noinline') 1424 | name time/op 1425 | MethodCall_direct/many/noinline/small_incr/baseline 0.99ns ± 3% 1426 | MethodCall_direct/many/noinline/small_incr/call 2.32ns ± 1% # 2.32 - 0.99 = 1.33ns 1427 | MethodCall_direct/many/noinline/big_incr/baseline 5.86ns ± 0% 1428 | MethodCall_direct/many/noinline/big_incr/call 17.1ns ± 1% # 17.1 - 5.86 = 11.24ns 1429 | MethodCall_direct/many/noinline/random_incr/baseline 8.80ns ± 0% 1430 | MethodCall_direct/many/noinline/random_incr/call 30.8ns ± 0% # 30.8 - 8.8 = 22ns 1431 | ``` 1432 | 没啥出乎意料的地方: 1433 | 1. `small_incr`: 因为 *极其* cache 友好,我们得到了和之前在单实例上循环类似的结果。 1434 | 2. `big_incr`: 因为强制 CPU 每次迭代的时候取新的 cache-line,我们看到了严重的延迟增加,虽然这个和执行调用本身没啥关系: ~6ns 可以归因于基线,剩下的则是对 receiver 进行解引用以得到它的 `id` 字段以及拷贝返回值带来的影响。 1435 | 3. `random_incr`: 和 `big_incr` 一样 `big_incr`, 除了在延迟上的增加是因为 A) 伪随机访问和 B) 获取预计算数组的下一个索引(这样会导致 cache miss)。 1436 | 1437 | 如上述逻辑所述,不管用哪种方法驱逐 CPU d-cache 并不会显著地影响方法的直接调用(无论是否内联),只会让其周边的东西都稍微慢一点。 1438 | 1439 | 动态分发的话呢? 1440 | ```Bash 1441 | $ go test -run=NONE -o iface_bench_test.bin iface_bench_test.go && \ 1442 | benchstat <( 1443 | taskset 2 ./iface_bench_test.bin -test.cpu=1 -test.benchtime=1s -test.count=3 \ 1444 | -test.bench='BenchmarkMethodCall_interface/many/inline') 1445 | name time/op 1446 | MethodCall_interface/many/noinline/small_incr/baseline 1.38ns ± 0% 1447 | MethodCall_interface/many/noinline/small_incr/call 3.48ns ± 0% # 3.48 - 1.38 = 2.1ns 1448 | MethodCall_interface/many/noinline/big_incr/baseline 6.86ns ± 0% 1449 | MethodCall_interface/many/noinline/big_incr/call 19.6ns ± 1% # 19.6 - 6.86 = 12.74ns 1450 | MethodCall_interface/many/noinline/random_incr/baseline 11.0ns ± 0% 1451 | MethodCall_interface/many/noinline/random_incr/call 34.7ns ± 0% # 34.7 - 11.0 = 23.7ns 1452 | ``` 1453 | 结果看起来都极其相似,除了整体上稍微慢了一些,因为我们每次迭代都拷贝了两个 quad-word(i.e. 两个字段都是 `identifier` interface 类型)到 slice 外部而不是一个(指向 `id32` 的指针)。 1454 | 1455 | 这个和 "直接"调用的性能差不多多,因为 slice 中的所有 interface 们都共享同一个 `itab`(i.e. 他们都是 `iface` interface),他们附属的虚表从来没有从 L1d cache 中离开,所以每次迭代获取正确的方法指针都是没啥成本的了。 1456 | 1457 | 同样的,组成 `main.(*id32).idNoInline` 方法体的指令也从来没有离开过 L1i cache。 1458 | 1459 | 你可能会这么想,实践中我一个 interface 的 slice 可能会包含有很多不同的底层类型(就是 vtable),这会导致对 L1i 和 L1d cache 的驱逐效果,因为不同的 vtable 会把其它的挤出 cache。 1460 | 然而这个想法只是理论上成立,有这样的想法,可能是因为你之前写过其它 OOP 语言,比如 C++,并从中积累到的经验。在这种语言中是鼓励使用深层嵌套的类继承,这也是它们用来抽象的主要工具。 1461 | 1462 | 如果继承树本身非常大的话,其相关的 vtable 的数量也会大到一定程度,并能在迭代这种持有大量实现的虚类时,把 CPU cache 挤出去了(可以想想 GUI 框架中,所有类型都是 `Widget`,并存储在一个像图一样的数据结构中);在 C++ 中尤其如此,虚类倾向于定义非常复杂的行为,有时候可能有几十个方法,然后就形成了很大的 vtable 以及到 L1d cache 的较大压力。 1463 | 1464 | Go,在一方面有着完全不同的范式: OOP 被完全扔掉了,类型系统被打平,interface 经常被定义得很小,具有具体的行为(平均下来只有较少的方法,只要实现就可以隐式满足接口)。而不是一种基于复杂的多层继承的抽象。 1465 | 在实践上,我发现 Go 语言很少需要在有各种不同的底层类型的 interface 上进行迭代。当然,你可能比我知道更多的 case。 1466 | 1467 | 下面是打开了内联的 “直接” 调用的版本的结果,给那些好奇心旺盛的人: 1468 | ```Bash 1469 | name time/op 1470 | MethodCall_direct/many/inline/small_incr 0.97ns ± 1% # 0.97ns 1471 | MethodCall_direct/many/inline/big_incr/baseline 5.96ns ± 1% 1472 | MethodCall_direct/many/inline/big_incr/call 11.9ns ± 1% # 11.9 - 5.96 = 5.94ns 1473 | MethodCall_direct/many/inline/random_incr/baseline 9.20ns ± 1% 1474 | MethodCall_direct/many/inline/random_incr/call 16.9ns ± 1% # 16.9 - 9.2 = 7.7ns 1475 | ``` 1476 | 编译器如果可以内联该调用的话,会使"直接"调用版本大概比 "interface" 版本快 2 到 3 倍。 1477 | 之前也提过,因为现在的编译器的局限性,实践中大多数的函数都不会被内联,所以这种性能提升是非常少见的。当然,更为常见的是你没有别的选择,只能采用这种基于虚表的调用。 1478 | 1479 | **总结** 1480 | 1481 | 想要尽量有效地衡量虚表的函数调用看起来是一件比较复杂的尝试,因为其效果是众多交错复杂的边际效应,再加上现代硬件的复杂实现组成的共同结果。 1482 | 1483 | *在 Go 语言中*, 感谢语言的设计范式,考虑到当前编译器限制和内联的话,可以认为动态分发基本上是没什么成本的。 不过如果还是怀疑这一点的话,可以随时对代码的 hot 路径进行评估,并对性能评测进行计算,来确定到底动态分发是否会为自己的系统带来问题。 1484 | 1485 | (NOTE: 本书的晚些章节会研究编译器的内联能力) 1486 | 1487 | ## 特殊例子及编译器技巧 1488 | 1489 | 本节会 review 一些和 interface 打交道时,每天都会碰到的特殊 case。 1490 | 1491 | 现在你对 interface 的工作原理应该已经有了清晰的认识,所以下面我们的讲解会简略一些。 1492 | 1493 | ### 空接口 1494 | 1495 | 空接口的的数据结构和直觉推测差不多: 一个不带 `itab` 的 `eface` 结构。 1496 | 这样做有两个原因: 1497 | 1. 空接口中没有任何方法,和动态分发相关的东西都可以从数据结构中移除。 1498 | 2. 干掉虚表之后,接口本身的类型,这里注意不要和接口中数据的类型混了,始终都是相同的(i.e. 我们说的是 *这个* 空接口而不是 *一个* 空接口) 1499 | 1500 | *NOTE: 和前面我们给 `iface` 用的记号差不多,我们把持有 T 类型数据的空接口标记为 `eface`* 1501 | 1502 | `eface` 是表示 runtime 中空接口的根类型 ([src/runtime/runtime2.go](https://github.com/golang/go/blob/bf86aec25972f3a100c3aa58a6abcbcc35bdea49/src/runtime/runtime2.go#L148-L151)). 1503 | 定义差不多是这样: 1504 | ```Go 1505 | type eface struct { // 16 bytes on a 64bit arch 1506 | _type *_type 1507 | data unsafe.Pointer 1508 | } 1509 | ``` 1510 | 其中 `_type` 持有 `data` 指针指向的数据类型的信息。 1511 | `itab` 被完全干掉了。 1512 | 1513 | 尽管空接口理论上可以重用 `iface` 数据结构(因为 `iface` 可以算是 `eface` 的一个超集),runtime 还是选择对这两种 interface 进行区分,主要有两个理由: 为了节省空间,以及代码清晰。 1514 | 1515 | ### 拥有标量的接口 1516 | 1517 | 本章早些部分的 ([#解构接口](#数据结构概述)),我们提到了即使将一个标量类型存到 interface 里也会导致堆内存分配。 1518 | 现在是时机来研究原因和过程了。 1519 | 1520 | 看看这两个 benchmark ([eface_scalar_test.go](./eface_scalar_test.go)): 1521 | ```Go 1522 | func BenchmarkEfaceScalar(b *testing.B) { 1523 | var Uint uint32 1524 | b.Run("uint32", func(b *testing.B) { 1525 | for i := 0; i < b.N; i++ { 1526 | Uint = uint32(i) 1527 | } 1528 | }) 1529 | var Eface interface{} 1530 | b.Run("eface32", func(b *testing.B) { 1531 | for i := 0; i < b.N; i++ { 1532 | Eface = uint32(i) 1533 | } 1534 | }) 1535 | } 1536 | ``` 1537 | ```Bash 1538 | $ go test -benchmem -bench=. ./eface_scalar_test.go 1539 | BenchmarkEfaceScalar/uint32-8 2000000000 0.54 ns/op 0 B/op 0 allocs/op 1540 | BenchmarkEfaceScalar/eface32-8 100000000 12.3 ns/op 4 B/op 1 allocs/op 1541 | ``` 1542 | 1. 对于简单的赋值操作来说,这已经是两个数量级的差距了,并且 1543 | 2. 这里可以看到第二个 benchmark 需要在每次迭代的时候分配 4 个额外字节。 1544 | 1545 | 显然,第二个 case 中代码背后隐藏了沉重的机关,这些机关被关掉了: 我们需要研究一下生成的汇编内容。 1546 | 1547 | 第一个 benchmark,编译器如愿生成赋值操作: 1548 | ```Assembly 1549 | ;; Uint = uint32(i) 1550 | 0x000d MOVL DX, (AX) 1551 | ``` 1552 | 1553 | 第二个 benchmark 要复杂很多: 1554 | ```Assembly 1555 | ;; Eface = uint32(i) 1556 | 0x0050 MOVL CX, ""..autotmp_3+36(SP) 1557 | 0x0054 LEAQ type.uint32(SB), AX 1558 | 0x005b MOVQ AX, (SP) 1559 | 0x005f LEAQ ""..autotmp_3+36(SP), DX 1560 | 0x0064 MOVQ DX, 8(SP) 1561 | 0x0069 CALL runtime.convT2E32(SB) 1562 | 0x006e MOVQ 24(SP), AX 1563 | 0x0073 MOVQ 16(SP), CX 1564 | 0x0078 MOVQ "".&Eface+48(SP), DX 1565 | 0x007d MOVQ CX, (DX) 1566 | 0x0080 MOVL runtime.writeBarrier(SB), CX 1567 | 0x0086 LEAQ 8(DX), DI 1568 | 0x008a TESTL CX, CX 1569 | 0x008c JNE 148 1570 | 0x008e MOVQ AX, 8(DX) 1571 | 0x0092 JMP 46 1572 | 0x0094 CALL runtime.gcWriteBarrier(SB) 1573 | 0x0099 JMP 46 1574 | ``` 1575 | 这还 *只* 是赋值,不是完整的 benchmark! 1576 | 我们一句一句研究一下这段代码。 1577 | 1578 | **Step 1: Create the interface** 1579 | 1580 | ```Assembly 1581 | 0x0050 MOVL CX, ""..autotmp_3+36(SP) 1582 | 0x0054 LEAQ type.uint32(SB), AX 1583 | 0x005b MOVQ AX, (SP) 1584 | 0x005f LEAQ ""..autotmp_3+36(SP), DX 1585 | 0x0064 MOVQ DX, 8(SP) 1586 | 0x0069 CALL runtime.convT2E32(SB) 1587 | 0x006e MOVQ 24(SP), AX 1588 | 0x0073 MOVQ 16(SP), CX 1589 | ``` 1590 | 1591 | 第一段代码实例化了之后要赋值给 `Eface` 的空接口 `eface`。 1592 | 1593 | 在创建 interface 的章节 ([#创建接口](#创建接口)) 我们已经研究过类似的代码了,这里的代码除了调用的是 `runtime.convT2I32` 而不是 `runtime.convT2E32`,没啥区别。 1594 | 1595 | 看来 `runtime.convT2I32` 和 `runtime.convT2E32` 都是同一个大家族的函数成员,这个家族的函数的工作就是用标量实例化一个特定的 interface 或者空 interface(特殊情况下也可能是 string 或者 slice)。 1596 | 该函数族由 10 个符号组成,是由`(eface/iface, 16/32/64/string/slice)` 两两组合而成: 1597 | ```Go 1598 | // empty interface from scalar value 1599 | func convT2E16(t *_type, elem unsafe.Pointer) (e eface) {} 1600 | func convT2E32(t *_type, elem unsafe.Pointer) (e eface) {} 1601 | func convT2E64(t *_type, elem unsafe.Pointer) (e eface) {} 1602 | func convT2Estring(t *_type, elem unsafe.Pointer) (e eface) {} 1603 | func convT2Eslice(t *_type, elem unsafe.Pointer) (e eface) {} 1604 | 1605 | // interface from scalar value 1606 | func convT2I16(tab *itab, elem unsafe.Pointer) (i iface) {} 1607 | func convT2I32(tab *itab, elem unsafe.Pointer) (i iface) {} 1608 | func convT2I64(tab *itab, elem unsafe.Pointer) (i iface) {} 1609 | func convT2Istring(tab *itab, elem unsafe.Pointer) (i iface) {} 1610 | func convT2Islice(tab *itab, elem unsafe.Pointer) (i iface) {} 1611 | ``` 1612 | (*可以看到,没有 `convT2E8` 和 `convT2I8` 方法;这是因为我们在本节末尾所描述的编译器优化*) 1613 | 1614 | 所有函数干的事情都一样,他们唯一的区别就是返回的值的类型(`iface` vs. `eface`)以及他们在堆上分配的内存大小。 1615 | 1616 | 再来更仔细地研究一下比如 `runtime.convT2E32` ([src/runtime/iface.go](https://github.com/golang/go/blob/bf86aec25972f3a100c3aa58a6abcbcc35bdea49/src/runtime/iface.go#L308-L325)): 1617 | ```Go 1618 | func convT2E32(t *_type, elem unsafe.Pointer) (e eface) { 1619 | /* ...omitted debug stuff... */ 1620 | var x unsafe.Pointer 1621 | if *(*uint32)(elem) == 0 { 1622 | x = unsafe.Pointer(&zeroVal[0]) 1623 | } else { 1624 | x = mallocgc(4, t, false) 1625 | *(*uint32)(x) = *(*uint32)(elem) 1626 | } 1627 | e._type = t 1628 | e.data = x 1629 | return 1630 | } 1631 | ``` 1632 | 1633 | 这个函数初始化了 `eface` 结构体的 `_type` 字段,该结构由调用方(记住: 返回值都是由 caller 在它自己的栈帧上分配的)作为第一个参数传入。 1634 | `eface` 的 `data` 字段依赖于第二个参数 `elem`: 1635 | - 如果 `elem` 是零值, `e.data` 就被初始化,指向 `runtime.zeroVal`,这是 runtime 提供的一个用来代表零值的特殊全局变量。我们在下一节会更多地讨论这个特殊变量。 1636 | - 如果 `elem` 非零,函数会在堆上分配 4 个字节(`x = mallocgc(4, t, false)`),初始化 `elem`(`*(*uint32)(x) = *(*uint32)(elem)`)这个指针指向的这 4 个字节的内容,然后把指针指向 `e.data`。 1637 | 1638 | 这种情况下,`e._type` 持有 `type.uint32` 的地址(`LEAQ type.uint32(SB), AX`),这是由标准库实现的,其地址只有链接期才能由 stdlib 知晓: 1639 | ```Bash 1640 | $ go tool nm eface_scalar_test.o | grep 'type\.uint32' 1641 | U type.uint32 1642 | ``` 1643 | (`U` 表示该符号不在目标文件中定义,并将(很可能)由另一个目标,在链接期提供(i.e. 这个 case 的情况下是标准库)) 1644 | 1645 | **Step 2: Assign the result (part 1)** 1646 | 1647 | ```Assembly 1648 | 0x0078 MOVQ "".&Eface+48(SP), DX 1649 | 0x007d MOVQ CX, (DX) ;; Eface._type = ret._type 1650 | ``` 1651 | `runtime.convT2E32` 的结果被赋值给我们的 `Eface` 变量了..真的么? 1652 | 1653 | 实际上,现在来看,返回值只有 `_type` 字段被赋值给了 `Eface._type`,`data` 字段没办法被拷贝。 1654 | 1655 | **Step 3: Assign the result (part 2) or ask the garbage collector to** 1656 | 1657 | ```Assembly 1658 | 0x0080 MOVL runtime.writeBarrier(SB), CX 1659 | 0x0086 LEAQ 8(DX), DI ;; Eface.data = ret.data (indirectly via runtime.gcWriteBarrier) 1660 | 0x008a TESTL CX, CX 1661 | 0x008c JNE 148 1662 | 0x008e MOVQ AX, 8(DX) ;; Eface.data = ret.data (direct) 1663 | 0x0092 JMP 46 1664 | 0x0094 CALL runtime.gcWriteBarrier(SB) 1665 | 0x0099 JMP 46 1666 | ``` 1667 | 1668 | 这里显而易见的复杂度,是由将返回的 `eface` 的 `data` 指针赋值给 `Eface.data` 造成的影响: 由于我们操作了程序的内存图(i.e. 哪部分内存引用了另外的哪部分内存),这种情况下需要将这种变更通知垃圾收集器,因为这时候在后台可能有正在运行的垃圾收集任务。 1669 | 1670 | 这也被称为 write barrier,是 Go 的 *并发* 垃圾收集的直接结果。 1671 | 如果听起来有点懵,先不要太担心;本书的下一个章节将提供 Go 垃圾收集的全面审视。 1672 | 现在的话,只要记住在汇编中看到对 `runtime.gcWriteBarrier` 的调用代码的话,一定是和指针操作相关,并且必须要通知垃圾收集器就行了。 1673 | 1674 | 总结一下,最后一段代码做了下面两件事情: 1675 | - 如果 write-barrier 当前不活跃,把 `ret.data` 赋值给 `Eface.data` (`MOVQ AX, 8(DX)`)。 1676 | - 如果 write-barrier 活跃的话,礼貌地要求垃圾收集器替我们把工作给做了 (`LEAQ 8(DX), DI` + `CALL runtime.gcWriteBarrier(SB)`). 1677 | 1678 | (*同样的,先不要太在意这里的函数调用*) 1679 | 1680 | 瞧,我们已经得到了一个保存了 (`uint32`) 标量类型的完整的 interface。 1681 | 1682 | **总结** 1683 | 1684 | 把标量值绑定到 interface 上实践中并不会经常发生,从很多方面讲都会导致较大的成本,因此了解背后的原理就比较重要了。 1685 | 1686 | 提到成本的话,我们已经提过编译器实现了各种各样的 trick 来避免特定情况下的内存分配;我们以 3 种编译器层面做的优化的例子来结束本节的内容。 1687 | 1688 | **Interface trick 1: Byte-sized values** 1689 | 1690 | 先来看这段实例化 `eface` 的代码的 benchmark ([eface_scalar_test.go](./eface_scalar_test.go)): 1691 | ```Go 1692 | func BenchmarkEfaceScalar(b *testing.B) { 1693 | b.Run("eface8", func(b *testing.B) { 1694 | for i := 0; i < b.N; i++ { 1695 | // LEAQ type.uint8(SB), BX 1696 | // MOVQ BX, (CX) 1697 | // MOVBLZX AL, SI 1698 | // LEAQ runtime.staticbytes(SB), R8 1699 | // ADDQ R8, SI 1700 | // MOVL runtime.writeBarrier(SB), R9 1701 | // LEAQ 8(CX), DI 1702 | // TESTL R9, R9 1703 | // JNE 100 1704 | // MOVQ SI, 8(CX) 1705 | // JMP 40 1706 | // MOVQ AX, R9 1707 | // MOVQ SI, AX 1708 | // CALL runtime.gcWriteBarrier(SB) 1709 | // MOVQ R9, AX 1710 | // JMP 40 1711 | Eface = uint8(i) 1712 | } 1713 | }) 1714 | } 1715 | ``` 1716 | ```Bash 1717 | $ go test -benchmem -bench=BenchmarkEfaceScalar/eface8 ./eface_scalar_test.go 1718 | BenchmarkEfaceScalar/eface8-8 2000000000 0.88 ns/op 0 B/op 0 allocs/op 1719 | ``` 1720 | 1721 | 可以注意到,在值为一个字节大小的情况下,编译器会避免调用 `runtime.convT2E`/`runtime.convT2I` 和相关的堆内存分配。取而代之,直接重用了我们需要的, runtime 暴露给我们的已经初始化好的, 1 个字节大小的值: `LEAQ runtime.staticbytes(SB), R8`. 1722 | 1723 | `runtime.staticbytes` 可以在 [src/runtime/iface.go](https://github.com/golang/go/blob/bf86aec25972f3a100c3aa58a6abcbcc35bdea49/src/runtime/iface.go#L619-L653) 找到,长下面这样: 1724 | ```Go 1725 | // staticbytes is used to avoid convT2E for byte-sized values. 1726 | var staticbytes = [...]byte{ 1727 | 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 1728 | 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 1729 | 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 1730 | 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, 1731 | 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 1732 | 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f, 1733 | 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f, 1734 | 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f, 1735 | 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x8b, 0x8c, 0x8d, 0x8e, 0x8f, 1736 | 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0x9b, 0x9c, 0x9d, 0x9e, 0x9f, 1737 | 0xa0, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xab, 0xac, 0xad, 0xae, 0xaf, 1738 | 0xb0, 0xb1, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xbb, 0xbc, 0xbd, 0xbe, 0xbf, 1739 | 0xc0, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xcb, 0xcc, 0xcd, 0xce, 0xcf, 1740 | 0xd0, 0xd1, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xdb, 0xdc, 0xdd, 0xde, 0xdf, 1741 | 0xe0, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xeb, 0xec, 0xed, 0xee, 0xef, 1742 | 0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff, 1743 | } 1744 | ``` 1745 | 只要使用这个数组中正确的偏移量,编译器就可以有效地避免额外的堆内存分配,并可以引用用一个字节的任意值了。 1746 | 1747 | 不过这里有些不对头,你能看出来么? 1748 | 生成的代码会嵌入和 write-barrier 相关的所有工具,尽管我们操作的指针只是持有了一些全局变量的地址,而这些全局变量的生命周期和整个程序是一样的。 1749 | 也就是说,`runtime.staticbytes` 永远都不会被垃圾收集,无论它被哪一部分持有引用,或者没有被引用,所以我们不需要在意这种情况下的 write-barrier 成本。 1750 | 1751 | **Interface trick 2: Static inference** 1752 | 1753 | 这是用编译时才知道的值来实例化 `eface` 所做的 benchmark ([eface_scalar_test.go](./eface_scalar_test.go)): 1754 | ```Go 1755 | func BenchmarkEfaceScalar(b *testing.B) { 1756 | b.Run("eface-static", func(b *testing.B) { 1757 | for i := 0; i < b.N; i++ { 1758 | // LEAQ type.uint64(SB), BX 1759 | // MOVQ BX, (CX) 1760 | // MOVL runtime.writeBarrier(SB), SI 1761 | // LEAQ 8(CX), DI 1762 | // TESTL SI, SI 1763 | // JNE 92 1764 | // LEAQ "".statictmp_0(SB), SI 1765 | // MOVQ SI, 8(CX) 1766 | // JMP 40 1767 | // MOVQ AX, SI 1768 | // LEAQ "".statictmp_0(SB), AX 1769 | // CALL runtime.gcWriteBarrier(SB) 1770 | // MOVQ SI, AX 1771 | // LEAQ "".statictmp_0(SB), SI 1772 | // JMP 40 1773 | Eface = uint64(42) 1774 | } 1775 | }) 1776 | } 1777 | ``` 1778 | ```Bash 1779 | $ go test -benchmem -bench=BenchmarkEfaceScalar/eface-static ./eface_scalar_test.go 1780 | BenchmarkEfaceScalar/eface-static-8 2000000000 0.81 ns/op 0 B/op 0 allocs/op 1781 | ``` 1782 | 1783 | 从生成的汇编可以看出来编译器将 `runtime.convT2E64` 的调用完全优化没了,并且通过加载自动生成的全局变量地址直接构建好了空 interface,这个全局地址已经包含了我们想要的值: `LEAQ "".statictmp_0(SB), SI` (注意这里的 `(SB)` 部分,代表的是一个全局变量)。 1784 | 1785 | 用之前我们搞的脚本 `dump_sym.sh`,能够更好地把正在发生的事情可视化出来: 1786 | ```Bash 1787 | $ GOOS=linux GOARCH=amd64 go tool compile eface_scalar_test.go 1788 | $ GOOS=linux GOARCH=amd64 go tool link -o eface_scalar_test.bin eface_scalar_test.o 1789 | $ ./dump_sym.sh eface_scalar_test.bin .rodata main.statictmp_0 1790 | .rodata file-offset: 655360 1791 | .rodata VMA: 4849664 1792 | main.statictmp_0 VMA: 5145768 1793 | main.statictmp_0 SIZE: 8 1794 | 1795 | 0000000 002a 0000 0000 0000 1796 | 0000008 1797 | ``` 1798 | 像期望的一样, `main.statictmp_0` 是一个 8-字节变量,其值为 `0x000000000000002a` 就是 `$42`. 1799 | 1800 | **Interface trick 3: Zero-values** 1801 | 1802 | 最后一个技巧,看看下面这个用零值实例化 `eface` 的 benchmark ([eface_scalar_test.go](./eface_scalar_test.go)): 1803 | ```Go 1804 | func BenchmarkEfaceScalar(b *testing.B) { 1805 | b.Run("eface-zeroval", func(b *testing.B) { 1806 | for i := 0; i < b.N; i++ { 1807 | // MOVL $0, ""..autotmp_3+36(SP) 1808 | // LEAQ type.uint32(SB), AX 1809 | // MOVQ AX, (SP) 1810 | // LEAQ ""..autotmp_3+36(SP), CX 1811 | // MOVQ CX, 8(SP) 1812 | // CALL runtime.convT2E32(SB) 1813 | // MOVQ 16(SP), AX 1814 | // MOVQ 24(SP), CX 1815 | // MOVQ "".&Eface+48(SP), DX 1816 | // MOVQ AX, (DX) 1817 | // MOVL runtime.writeBarrier(SB), AX 1818 | // LEAQ 8(DX), DI 1819 | // TESTL AX, AX 1820 | // JNE 152 1821 | // MOVQ CX, 8(DX) 1822 | // JMP 46 1823 | // MOVQ CX, AX 1824 | // CALL runtime.gcWriteBarrier(SB) 1825 | // JMP 46 1826 | Eface = uint32(i - i) // outsmart the compiler (avoid static inference) 1827 | } 1828 | }) 1829 | } 1830 | ``` 1831 | ```Bash 1832 | $ go test -benchmem -bench=BenchmarkEfaceScalar/eface-zero ./eface_scalar_test.go 1833 | BenchmarkEfaceScalar/eface-zeroval-8 500000000 3.14 ns/op 0 B/op 0 allocs/op 1834 | ``` 1835 | 1836 | 首先,注意我们如何利用 `uint32(i - i)`,而不是 `uint32(0)` 来避免编译器进行优化 #2(static inference)。 1837 | (*是的,我们可以只声明一个全局的零值变量,这样编译器就会被强制采用更保守的路线。。不过再次的,我们是为了能有一点乐子。不要那么较真*) 1838 | 1839 | 生成的代码看起来比较正常,分配的 case。。还是没有发生分配,什么情况? 1840 | 1841 | 早些时候剖析`runtime.convT2E32` 的时候有提到,这里的内存分配能够用类似 #1(单字节值) 的技巧完全优化掉: 当一些代码需要引用零值变量,编译器会简单地提供给它一个 runtime 暴露出的全局变量,该变量已经被初始化为零了。 1842 | 1843 | 和 `runtime.staticbytes` 类似,我们可以在 runtime 的代码里找到这个变量 ([src/runtime/hashmap.go](https://github.com/golang/go/blob/bf86aec25972f3a100c3aa58a6abcbcc35bdea49/src/runtime/hashmap.go#L1248-L1249)): 1844 | ```Go 1845 | const maxZero = 1024 // must match value in ../cmd/compile/internal/gc/walk.go 1846 | var zeroVal [maxZero]byte 1847 | ``` 1848 | 1849 | 这就结束了我们的优化之旅。 1850 | 我们汇总一下之前的所有 benchmark 来结束这一节: 1851 | ```Bash 1852 | $ go test -benchmem -bench=. ./eface_scalar_test.go 1853 | BenchmarkEfaceScalar/uint32-8 2000000000 0.54 ns/op 0 B/op 0 allocs/op 1854 | BenchmarkEfaceScalar/eface32-8 100000000 12.3 ns/op 4 B/op 1 allocs/op 1855 | BenchmarkEfaceScalar/eface8-8 2000000000 0.88 ns/op 0 B/op 0 allocs/op 1856 | BenchmarkEfaceScalar/eface-zeroval-8 500000000 3.14 ns/op 0 B/op 0 allocs/op 1857 | BenchmarkEfaceScalar/eface-static-8 2000000000 0.81 ns/op 0 B/op 0 allocs/op 1858 | ``` 1859 | 1860 | ### 关于零值 1861 | 1862 | 像我们已经看到的,`runtime.convT2*` 族函数在 interface 持有的数据恰好引用了零值时,会避免堆上的内存分配。 1863 | 这种优化并不为 interface 所特有,在 Go 的 runtime 中被广泛应用,只要有指针指向了零值,就会避免不必要的内存分配,只要将该指针指向一个 runtime 暴露出的始终为零的特殊变量的地址即可。 1864 | 1865 | 可以用一个简单的程序确认 ([zeroval.go](./zeroval.go)): 1866 | ```Go 1867 | //go:linkname zeroVal runtime.zeroVal 1868 | var zeroVal uintptr 1869 | 1870 | type eface struct{ _type, data unsafe.Pointer } 1871 | 1872 | func main() { 1873 | x := 42 1874 | var i interface{} = x - x // outsmart the compiler (avoid static inference) 1875 | 1876 | fmt.Printf("zeroVal = %p\n", &zeroVal) 1877 | fmt.Printf(" i = %p\n", ((*eface)(unsafe.Pointer(&i))).data) 1878 | } 1879 | ``` 1880 | ```Bash 1881 | $ go run zeroval.go 1882 | zeroVal = 0x5458e0 1883 | i = 0x5458e0 1884 | ``` 1885 | 不出所料。 1886 | 1887 | 注意 `//go:linkname` 指令使我们可以引用外部的符号: 1888 | 1889 | > `//go:linkname`指令指示编译器使用"importpath.name"作为源代码中声明为 1890 | > "localname"的变量或函数的目标文件符号名称。由于此指令可以破坏类型系统和包的模块性, 1891 | > 因此只会在导入了"unsafe"的文件中启用。 1892 | 1893 | > The //go:linkname directive instructs the compiler to use “importpath.name” as the object file symbol name for the variable or function declared as “localname” in the source code. Because this directive can subvert the type system and package modularity, it is only enabled in files that have imported "unsafe". 1894 | 1895 | ### 关于大小为0的变量 1896 | 1897 | 和零值类似的套路,Go 程序的常见的一个技巧是使用大小为 0 的对象(例如 `struct{}{}`) 不会进行任何内存分配。 1898 | Go 的官方 spec (本章最后有链接) 这么几句话对此进行了解释: 1899 | 1900 | > 大小为0的结构体或者数组,是指该结构体没有任何字段或者数组中没有任何元素的大小大 1901 | > 于0。两个不同的大小为0的变量可能会在内存中使用相同的地址。 1902 | 1903 | > A struct or array type has size zero if it contains no fields (or elements, respectively) that have a size greater than zero. 1904 | > Two distinct zero-size variables may have the same address in memory. 1905 | 1906 | "may have the same address in memory" 这句话中的 "may" 表明编译器不保证这件事一定发生,尽管实际上总会发生,并且是现在官方 Go 编译器(`gc`)就是这样的。 1907 | 1908 | 用简单的程序来确认 ([zerobase.go](./zerobase.go)): 1909 | ```Go 1910 | func main() { 1911 | var s struct{} 1912 | var a [42]struct{} 1913 | 1914 | fmt.Printf("s = % p\n", &s) 1915 | fmt.Printf("a = % p\n", &a) 1916 | } 1917 | ``` 1918 | ```Bash 1919 | $ go run zerobase.go 1920 | s = 0x546fa8 1921 | a = 0x546fa8 1922 | ``` 1923 | 1924 | 假如想知道地址后面隐藏了什么东西,只要简单地来看一下二进制文件的内容: 1925 | ```Bash 1926 | $ go build -o zerobase.bin zerobase.go && objdump -t zerobase.bin | grep 546fa8 1927 | 0000000000546fa8 g O .noptrbss 0000000000000008 runtime.zerobase 1928 | ``` 1929 | 下面就只需要在 runtime 源代码中找 `runtime.zerobase` 这个变量就行了 ([src/runtime/malloc.go](https://github.com/golang/go/blob/bf86aec25972f3a100c3aa58a6abcbcc35bdea49/src/runtime/malloc.go#L516-L517)): 1930 | ```Go 1931 | // base address for all 0-byte allocations 1932 | var zerobase uintptr 1933 | ``` 1934 | 1935 | 如果我们想要非常非常精确地确认这件事: 1936 | ```Go 1937 | //go:linkname zerobase runtime.zerobase 1938 | var zerobase uintptr 1939 | 1940 | func main() { 1941 | var s struct{} 1942 | var a [42]struct{} 1943 | 1944 | fmt.Printf("zerobase = %p\n", &zerobase) 1945 | fmt.Printf(" s = %p\n", &s) 1946 | fmt.Printf(" a = %p\n", &a) 1947 | } 1948 | ``` 1949 | ```Bash 1950 | $ go run zerobase.go 1951 | zerobase = 0x546fa8 1952 | s = 0x546fa8 1953 | a = 0x546fa8 1954 | ``` 1955 | 1956 | ## 组合接口 1957 | 1958 | interface 组合实在是没啥特殊的,这只是编译器提供的一种语法糖而已。 1959 | 1960 | 看一下下面的程序 ([compound_interface.go](./compound_interface.go)): 1961 | ```Go 1962 | type Adder interface{ Add(a, b int32) int32 } 1963 | type Subber interface{ Sub(a, b int32) int32 } 1964 | type Mather interface { 1965 | Adder 1966 | Subber 1967 | } 1968 | 1969 | type Calculator struct{ id int32 } 1970 | func (c *Calculator) Add(a, b int32) int32 { return a + b } 1971 | func (c *Calculator) Sub(a, b int32) int32 { return a - b } 1972 | 1973 | func main() { 1974 | calc := Calculator{id: 6754} 1975 | var m Mather = &calc 1976 | m.Sub(10, 32) 1977 | } 1978 | ``` 1979 | 1980 | 像往常一样,编译器会生成和 `itab` 对应的 `iface`: 1981 | ```Bash 1982 | $ GOOS=linux GOARCH=amd64 go tool compile -S compound_interface.go | \ 1983 | grep -A 7 '^go.itab.\*"".Calculator,"".Mather' 1984 | go.itab.*"".Calculator,"".Mather SRODATA dupok size=40 1985 | 0x0000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 1986 | 0x0010 5e 33 ca c8 00 00 00 00 00 00 00 00 00 00 00 00 ^3.............. 1987 | 0x0020 00 00 00 00 00 00 00 00 ........ 1988 | rel 0+8 t=1 type."".Mather+0 1989 | rel 8+8 t=1 type.*"".Calculator+0 1990 | rel 24+8 t=1 "".(*Calculator).Add+0 1991 | rel 32+8 t=1 "".(*Calculator).Sub+0 1992 | ``` 1993 | 我们能从重定向指令中看到编译器生成的虚表持有了 `Adder` 和 `Subber` 方法,像我们所期望的一样: 1994 | ``` 1995 | rel 24+8 t=1 "".(*Calculator).Add+0 1996 | rel 32+8 t=1 "".(*Calculator).Sub+0 1997 | ``` 1998 | 1999 | 如之前所述,interface 组合没有什么秘密武器。 2000 | 2001 | 提一句不相关的,这个小程序演示了我们还没见过的一些东西: 因为生成的 `itab` 是为指向构建器的 *指针* 所定制的,与具体的值相对应,这一点在它的符号名 (`go.itab.*"".Calculator,"".Mather`) 上和内嵌的 `_type` (`type.*"".Calculator`) 上都有所反映。 2002 | 2003 | 和我们本章见到的一样,这和命名方法的符号所使用的语义也是一致的。 2004 | 2005 | ## 断言 2006 | 2007 | 我们会以类型断言来结束这一章,会同时从实现和成本两方面来讨论。 2008 | 2009 | ### 类型断言 2010 | 2011 | 先看看这个简单的程序 ([eface_to_type.go](./eface_to_type.go)): 2012 | ```Go 2013 | var j uint32 2014 | var Eface interface{} // outsmart compiler (avoid static inference) 2015 | 2016 | func assertion() { 2017 | i := uint64(42) 2018 | Eface = i 2019 | j = Eface.(uint32) 2020 | } 2021 | ``` 2022 | 2023 | 下面是带注释的汇编版本的 `j = Eface.(uint32)`: 2024 | ```Assembly 2025 | 0x0065 00101 MOVQ "".Eface(SB), AX ;; AX = Eface._type 2026 | 0x006c 00108 MOVQ "".Eface+8(SB), CX ;; CX = Eface.data 2027 | 0x0073 00115 LEAQ type.uint32(SB), DX ;; DX = type.uint32 2028 | 0x007a 00122 CMPQ AX, DX ;; Eface._type == type.uint32 ? 2029 | 0x007d 00125 JNE 162 ;; no? panic our way outta here 2030 | 0x007f 00127 MOVL (CX), AX ;; AX = *Eface.data 2031 | 0x0081 00129 MOVL AX, "".j(SB) ;; j = AX = *Eface.data 2032 | ;; exit 2033 | 0x0087 00135 MOVQ 40(SP), BP 2034 | 0x008c 00140 ADDQ $48, SP 2035 | 0x0090 00144 RET 2036 | ;; panic: interface conversion: is , not 2037 | 0x00a2 00162 MOVQ AX, (SP) ;; have: Eface._type 2038 | 0x00a6 00166 MOVQ DX, 8(SP) ;; want: type.uint32 2039 | 0x00ab 00171 LEAQ type.interface {}(SB), AX ;; AX = type.interface{} (eface) 2040 | 0x00b2 00178 MOVQ AX, 16(SP) ;; iface: AX 2041 | 0x00b7 00183 CALL runtime.panicdottypeE(SB) ;; func panicdottypeE(have, want, iface *_type) 2042 | 0x00bc 00188 UNDEF 2043 | 0x00be 00190 NOP 2044 | ``` 2045 | 2046 | 也没啥出人意料的: 代码比较了 `Eface._type` 持有的地址和 `type.uint32` 持有的地址,之前也见过,这是标准库暴露出的全局符号,它持有的 `_type` 结构描述了 `uint32` 这个类型。 2047 | 如果 `_type` 指针匹配,那么我们可以一切正常地将 `*Eface.data` 赋值给 `j`;否则的话,我们需要调用 `runtime.panicdottypeE` 来抛出 panic 信息,并精确地描述这种不匹配。 2048 | 2049 | `runtime.panicdottypeE` 是一个 _非常_ 简单的函数,只做了顾名思义的工作 ([src/runtime/iface.go](https://github.com/golang/go/blob/bf86aec25972f3a100c3aa58a6abcbcc35bdea49/src/runtime/iface.go#L235-L245)): 2050 | ```Go 2051 | // panicdottypeE is called when doing an e.(T) conversion and the conversion fails. 2052 | // have = the dynamic type we have. 2053 | // want = the static type we're trying to convert to. 2054 | // iface = the static type we're converting from. 2055 | func panicdottypeE(have, want, iface *_type) { 2056 | haveString := "" 2057 | if have != nil { 2058 | haveString = have.string() 2059 | } 2060 | panic(&TypeAssertionError{iface.string(), haveString, want.string(), ""}) 2061 | } 2062 | ``` 2063 | 2064 | **What about performance?** 2065 | 2066 | 来看看我们都得到了些啥: 一堆从主存拿到的 `MOV` 指令,一个*很*容易预测的分支,一个指针解引用(`j = *Eface.data`) (这里是因为一开始我们就用具体值实例化了 interface 变量,否则我们就只能直接拷贝 `Eface.data` 指针了)。 2067 | 2068 | 这里不应该做 benchmark。 2069 | 和我们之前评估的,动态分发的成本类似,理论上这也是几乎没啥成本的。如果说实际会给你带来多少消耗的话,要取决于你的代码路径是否设计得对 cache 友好。 2070 | 再在这里做 benchmark 的话,也得不到真实环境下的结果,可能还会歪曲事实。 2071 | 2072 | 总结一下,我们得到了一个常见的守旧的建议: 按你的场景进行测试,检查你的处理器性能瓶颈,并确定这对你的 hot path 来说到底会不会产生可见的影响。 2073 | 可能会。也可能不会。大多数情况下不会。 2074 | 2075 | ### 类型判断 2076 | 2077 | 类型 switch 稍微有点 trick。看看下面的代码 ([eface_to_type.go](./eface_to_type.go)): 2078 | ```Go 2079 | var j uint32 2080 | var Eface interface{} // outsmart compiler (avoid static inference) 2081 | 2082 | func typeSwitch() { 2083 | i := uint32(42) 2084 | Eface = i 2085 | switch v := Eface.(type) { 2086 | case uint16: 2087 | j = uint32(v) 2088 | case uint32: 2089 | j = v 2090 | } 2091 | } 2092 | ``` 2093 | 2094 | 这个简单的类型 switch 语句被翻译成了如下汇编(已注释): 2095 | ```Assembly 2096 | ;; switch v := Eface.(type) 2097 | 0x0065 00101 MOVQ "".Eface(SB), AX ;; AX = Eface._type 2098 | 0x006c 00108 MOVQ "".Eface+8(SB), CX ;; CX = Eface.data 2099 | 0x0073 00115 TESTQ AX, AX ;; Eface._type == nil ? 2100 | 0x0076 00118 JEQ 153 ;; yes? exit the switch 2101 | 0x0078 00120 MOVL 16(AX), DX ;; DX = Eface.type._hash 2102 | ;; case uint32 2103 | 0x007b 00123 CMPL DX, $-800397251 ;; Eface.type._hash == type.uint32.hash ? 2104 | 0x0081 00129 JNE 163 ;; no? go to next case (uint16) 2105 | 0x0083 00131 LEAQ type.uint32(SB), BX ;; BX = type.uint32 2106 | 0x008a 00138 CMPQ BX, AX ;; type.uint32 == Eface._type ? (hash collision?) 2107 | 0x008d 00141 JNE 206 ;; no? clear BX and go to next case (uint16) 2108 | 0x008f 00143 MOVL (CX), BX ;; BX = *Eface.data 2109 | 0x0091 00145 JNE 163 ;; landsite for indirect jump starting at 0x00d3 2110 | 0x0093 00147 MOVL BX, "".j(SB) ;; j = BX = *Eface.data 2111 | ;; exit 2112 | 0x0099 00153 MOVQ 40(SP), BP 2113 | 0x009e 00158 ADDQ $48, SP 2114 | 0x00a2 00162 RET 2115 | ;; case uint16 2116 | 0x00a3 00163 CMPL DX, $-269349216 ;; Eface.type._hash == type.uint16.hash ? 2117 | 0x00a9 00169 JNE 153 ;; no? exit the switch 2118 | 0x00ab 00171 LEAQ type.uint16(SB), DX ;; DX = type.uint16 2119 | 0x00b2 00178 CMPQ DX, AX ;; type.uint16 == Eface._type ? (hash collision?) 2120 | 0x00b5 00181 JNE 199 ;; no? clear AX and exit the switch 2121 | 0x00b7 00183 MOVWLZX (CX), AX ;; AX = uint16(*Eface.data) 2122 | 0x00ba 00186 JNE 153 ;; landsite for indirect jump starting at 0x00cc 2123 | 0x00bc 00188 MOVWLZX AX, AX ;; AX = uint16(AX) (redundant) 2124 | 0x00bf 00191 MOVL AX, "".j(SB) ;; j = AX = *Eface.data 2125 | 0x00c5 00197 JMP 153 ;; we're done, exit the switch 2126 | ;; indirect jump table 2127 | 0x00c7 00199 MOVL $0, AX ;; AX = $0 2128 | 0x00cc 00204 JMP 186 ;; indirect jump to 153 (exit) 2129 | 0x00ce 00206 MOVL $0, BX ;; BX = $0 2130 | 0x00d3 00211 JMP 145 ;; indirect jump to 163 (case uint16) 2131 | ``` 2132 | 2133 | 再一次,如果你精心地单步调试生存的代码并仔细阅读对应的注释的话,你会发现这里也没啥黑魔法。 2134 | 控制流虽然一开始看上去错综复杂,跳过来跳回去,不过这些代码确实是 Go 原始代码翻译出的最精确的结果。 2135 | 2136 | 有一些有意思的事情需要注意: 2137 | 2138 | **Note 1: Layout** 2139 | 2140 | 首先,注意生成代码的整体布局,这和原始的 switch 语句是比较接近的: 2141 | 1. 我们能找到一块包含初始化指令的块,加载变量的 `_type`,然后为了以防万一检查 `nil` 指针。 2142 | 2. 然后是 N 个逻辑块,每一块对应代码中 switch 语句的其中一个 case。 2143 | 3. 最后一块定义了一种间接表跳转,使控制流能从一个 case 跳到下一个 case时,把已被污染的寄存器恢复原状。 2144 | 2145 | 有了事后的明确认识,第二点非常重要,它说明类型-switch 生成的指令数目只和它所描述的 case 的相关。 2146 | 实践上,这会导致令人惊讶的性能问题,例如,一个有很多 case 的大规模的类型-switch 语句,在猜错分支时,会导致 L1i cache 直接被冲掉。 2147 | 2148 | 另一个有意思的是,我们的 switch 语句的布局,和生成的代码的 case 的顺序没什么关系。在我们 Go 的原始代码中, `case uint16` 先到,然后是 `case uint32`。在编译器生成的汇编代码中,这两个 case 的顺序被调换了,`case uint32` 在前面,而 `case uint16` 在后面。 2149 | 这个特定的 case 下,这种重排操作对我们来说是净赚,不过也只是运气好罢了。实际上如果你花一点时间对类型-switch 做实验的话,尤其是那些有两个 case 以上的 switch,你会发现编译器会以某种固定的启发法对 case 进行洗牌。 2150 | 这种乱序的方法是啥,我不清楚(如果你清楚,欢迎告诉我)。 2151 | 2152 | **Note 2: O(n)** 2153 | 2154 | 第二,注意控制流是完全闭着眼睛从一个 case 跳到下一个的,直到其落在了结果为 true 的 case 并最终到达 switch 语句的边界。 2155 | 2156 | 因为实在太浅显,可能都让人想放弃思考了("肯定还是之前那样啊,还能有啥特殊的"),不过在高层次思考的时候还是比较容易忽视一些问题。实践中,类型断言意味着其成本会随 case 的数量增加线性增长: 其成本为 `O(n)`。 2157 | 同样的,对 N 个 case 的 type-switch 语句进行求值和 N 个类型断言语句有一样的时间复杂度。这里也没有什么魔法。 2158 | 2159 | 只需要几个 benchmark 就可以确认 ([eface_to_type_test.go](./eface_to_type_test.go)): 2160 | ```Go 2161 | var j uint32 2162 | var eface interface{} = uint32(42) 2163 | 2164 | func BenchmarkEfaceToType(b *testing.B) { 2165 | b.Run("switch-small", func(b *testing.B) { 2166 | for i := 0; i < b.N; i++ { 2167 | switch v := eface.(type) { 2168 | case int8: 2169 | j = uint32(v) 2170 | case int16: 2171 | j = uint32(v) 2172 | default: 2173 | j = v.(uint32) 2174 | } 2175 | } 2176 | }) 2177 | b.Run("switch-big", func(b *testing.B) { 2178 | for i := 0; i < b.N; i++ { 2179 | switch v := eface.(type) { 2180 | case int8: 2181 | j = uint32(v) 2182 | case int16: 2183 | j = uint32(v) 2184 | case int32: 2185 | j = uint32(v) 2186 | case int64: 2187 | j = uint32(v) 2188 | case uint8: 2189 | j = uint32(v) 2190 | case uint16: 2191 | j = uint32(v) 2192 | case uint64: 2193 | j = uint32(v) 2194 | default: 2195 | j = v.(uint32) 2196 | } 2197 | } 2198 | }) 2199 | } 2200 | ``` 2201 | ```Bash 2202 | benchstat <(go test -benchtime=1s -bench=. -count=3 ./eface_to_type_test.go) 2203 | name time/op 2204 | EfaceToType/switch-small-8 1.91ns ± 2% 2205 | EfaceToType/switch-big-8 3.52ns ± 1% 2206 | ``` 2207 | 因为考虑更多的 case,第二个类型-switch 几乎花费了两倍的迭代时间。 2208 | 2209 | 这里给读者留一个练习题,试着给上面任意一个 benchmark 增加一个 `case uint32`,会看到他们的性能戏剧性地提升了: 2210 | ```Bash 2211 | benchstat <(go test -benchtime=1s -bench=. -count=3 ./eface_to_type_test.go) 2212 | name time/op 2213 | EfaceToType/switch-small-8 1.63ns ± 1% 2214 | EfaceToType/switch-big-8 2.17ns ± 1% 2215 | ``` 2216 | 使用本章学到的所有工具和知识,你应该能够解释这些数字背后的原理。玩的开心! 2217 | 2218 | **Note 3: Type hashes & pointer comparisons** 2219 | 2220 | 最后,注意每种 case 下的类型比较都是由两个阶段组成的: 2221 | 1. 比较类型 hash(`_type.hash`),然后 2222 | 2. 如果 match 的话,直接比较两个 `_type` 指针的内存地址。 2223 | 2224 | 由于每一个 `_type` 结构都是由编译器一次性生成,并存储在 `.rodata` 段的全局变量中的,编译器保证每一个类型在程序的生命周期内都有唯一的地址。 2225 | 2226 | 在这样的上下文下,这个额外的指针比较就可以帮助我们来确定两者确实一样,而不是发生了哈希碰撞。。不过这也带来了一个显而易见的问题: 为什么不直接进行后面这步比较,而干掉哈希比较呢?尤其是简单的类型断言,像我们前面所见,压根儿都不会用类型哈希。 2227 | 关于答案,我没有确凿的证据,希望能够得到一些提示。像往常一样,如果你知道,就开 issue 告诉我。 2228 | 2229 | 关于类型哈希,我们怎么知道 `$-800397251` 对应 `type.uint32.hash` 而 `$-269349216` 对应 `type.uint16.hash`,你可能比较想知道。比较麻烦的方法,当然 ([eface_type_hash.go](./eface_type_hash.go)): 2230 | ```Go 2231 | // simplified definitions of runtime's eface & _type types 2232 | type eface struct { 2233 | _type *_type 2234 | data unsafe.Pointer 2235 | } 2236 | type _type struct { 2237 | size uintptr 2238 | ptrdata uintptr 2239 | hash uint32 2240 | /* omitted lotta fields */ 2241 | } 2242 | 2243 | var Eface interface{} 2244 | func main() { 2245 | Eface = uint32(42) 2246 | fmt.Printf("eface._type.hash = %d\n", 2247 | int32((*eface)(unsafe.Pointer(&Eface))._type.hash)) 2248 | 2249 | Eface = uint16(42) 2250 | fmt.Printf("eface._type.hash = %d\n", 2251 | int32((*eface)(unsafe.Pointer(&Eface))._type.hash)) 2252 | } 2253 | ``` 2254 | ``` 2255 | $ go run eface_type_hash.go 2256 | eface._type.hash = -800397251 2257 | eface._type.hash = -269349216 2258 | ``` 2259 | 2260 | ## 总结 2261 | 2262 | 这就是 interface 相关的所有内容了。 2263 | 2264 | 我希望这一章能帮你找到你对 interface 和内部原理的所有问题的答案。更重要的是,希望本章给你提供了你想要深入研究任何知识时所需要的所有必要工具和技能。 2265 | 2266 | 如果你有问题或者建议,不要犹豫,开个 issue,带上 `chapter:2` 的前缀! 2267 | 2268 | ## 链接 2269 | 2270 | - [[Official] Go 1.1 Function Calls](https://docs.google.com/document/d/1bMwCey-gmqZVTpRax-ESeVuZGmjwbocYs1iHplK-cjo/pub) 2271 | - [[Official] The Go Programming Language Specification](https://golang.org/ref/spec) 2272 | - [The Gold linker by Ian Lance Taylor](https://lwn.net/Articles/276782/) 2273 | - [ELF: a linux executable walkthrough](https://i.imgur.com/EL7lT1i.png) 2274 | - [VMA vs LMA?](https://www.embeddedrelated.com/showthread/comp.arch.embedded/77071-1.php) 2275 | - [In C++ why and how are virtual functions slower?](https://softwareengineering.stackexchange.com/questions/191637/in-c-why-and-how-are-virtual-functions-slower) 2276 | - [The cost of dynamic (virtual calls) vs. static (CRTP) dispatch in C++](https://eli.thegreenplace.net/2013/12/05/the-cost-of-dynamic-virtual-calls-vs-static-crtp-dispatch-in-c) 2277 | - [Why is it faster to process a sorted array than an unsorted array?](https://stackoverflow.com/a/11227902) 2278 | - [Is accessing data in the heap faster than from the stack?](https://stackoverflow.com/a/24057744) 2279 | - [CPU cache](https://en.wikipedia.org/wiki/CPU_cache) 2280 | - [CppCon 2014: Mike Acton "Data-Oriented Design and C++"](https://www.youtube.com/watch?v=rX0ItVEVjHc) 2281 | - [CppCon 2017: Chandler Carruth "Going Nowhere Faster"](https://www.youtube.com/watch?v=2EWejmkKlxs) 2282 | - [What is the difference between MOV and LEA?](https://stackoverflow.com/a/1699778) 2283 | - [Issue #24631 (golang/go): *testing: don't truncate allocs/op*](https://github.com/golang/go/issues/24631) 2284 | -------------------------------------------------------------------------------- /chapter2_interfaces/compound_interface.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type Adder interface{ Add(a, b int32) int32 } 4 | type Subber interface{ Sub(a, b int32) int32 } 5 | 6 | type Mather interface { 7 | Adder 8 | Subber 9 | } 10 | 11 | type Calculator struct{ id int32 } 12 | 13 | func (c *Calculator) Add(a, b int32) int32 { return a + b } 14 | func (c *Calculator) Sub(a, b int32) int32 { return a - b } 15 | 16 | func main() { 17 | calc := Calculator{id: 6754} 18 | var m Mather = &calc 19 | m.Sub(10, 32) 20 | } 21 | -------------------------------------------------------------------------------- /chapter2_interfaces/direct_calls.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | //go:noinline 4 | func Add(a, b int32) int32 { return a + b } 5 | 6 | type Adder struct{ id int32 } 7 | 8 | //go:noinline 9 | func (adder *Adder) AddPtr(a, b int32) int32 { return a + b } 10 | 11 | //go:noinline 12 | func (adder Adder) AddVal(a, b int32) int32 { return a + b } 13 | 14 | func main() { 15 | Add(10, 32) // direct call of top-level function 16 | 17 | adder := Adder{id: 6754} 18 | adder.AddPtr(10, 32) // direct call of method with pointer receiver 19 | adder.AddVal(10, 32) // direct call of method with value receiver 20 | 21 | (&adder).AddVal(10, 32) // implicit dereferencing 22 | } 23 | -------------------------------------------------------------------------------- /chapter2_interfaces/dump_sym.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | BIN="$1" 4 | test "$BIN" 5 | SECTION="$2" 6 | test "$SECTION" 7 | SYM="$3" 8 | test "$SYM" 9 | 10 | section_offset=$( 11 | readelf -St -W "$BIN" | \ 12 | grep -A 1 "$SECTION" | \ 13 | tail -n +2 | \ 14 | awk '{print toupper($3)}' 15 | ) 16 | section_offset_dec=$(echo "ibase=16;$section_offset" | bc) 17 | echo "$SECTION file-offset: $section_offset_dec" 18 | 19 | section_vma=$( 20 | readelf -St -W "$BIN" | \ 21 | grep -A 1 "$SECTION" | \ 22 | tail -n +2 | \ 23 | awk '{print toupper($2)}' 24 | ) 25 | section_vma_dec=$(echo "ibase=16;$section_vma" | bc) 26 | echo "$SECTION VMA: $section_vma_dec" 27 | 28 | sym_vma=$(objdump -t -j "$SECTION" "$BIN" | grep "$SYM" | awk '{print toupper($1)}') 29 | sym_vma_dec=$(echo "ibase=16;$sym_vma" | bc) 30 | echo "$SYM VMA: $sym_vma_dec" 31 | sym_size=$(objdump -t -j "$SECTION" "$BIN" | grep "$SYM" | awk '{print toupper($5)}') 32 | sym_size_dec=$(echo "ibase=16;$sym_size" | bc) 33 | echo -e "$SYM SIZE: $sym_size_dec\n" 34 | 35 | sym_offset=$(( $sym_vma_dec - $section_vma_dec + $section_offset_dec )) 36 | dd if="$BIN" of=/dev/stdout bs=1 count=$sym_size_dec skip="$sym_offset" 2>/dev/null | hexdump 37 | -------------------------------------------------------------------------------- /chapter2_interfaces/eface_scalar_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func BenchmarkEfaceScalar(b *testing.B) { 8 | var Uint uint32 9 | b.Run("uint32", func(b *testing.B) { 10 | for i := 0; i < b.N; i++ { 11 | // MOVL DX, (AX) 12 | Uint = uint32(i) 13 | } 14 | }) 15 | var Eface interface{} 16 | b.Run("eface32", func(b *testing.B) { 17 | for i := 0; i < b.N; i++ { 18 | // MOVL CX, ""..autotmp_3+36(SP) 19 | // LEAQ type.uint32(SB), AX 20 | // MOVQ AX, (SP) 21 | // LEAQ ""..autotmp_3+36(SP), DX 22 | // MOVQ DX, 8(SP) 23 | // CALL runtime.convT2E32(SB) 24 | // MOVQ 24(SP), AX 25 | // MOVQ 16(SP), CX 26 | // MOVQ "".&Eface+48(SP), DX 27 | // MOVQ CX, (DX) 28 | // MOVL runtime.writeBarrier(SB), CX 29 | // LEAQ 8(DX), DI 30 | // TESTL CX, CX 31 | // JNE 148 32 | // MOVQ AX, 8(DX) 33 | // JMP 46 34 | // CALL runtime.gcWriteBarrier(SB) 35 | // JMP 46 36 | Eface = uint32(i) 37 | } 38 | }) 39 | b.Run("eface8", func(b *testing.B) { 40 | for i := 0; i < b.N; i++ { 41 | // LEAQ type.uint8(SB), BX 42 | // MOVQ BX, (CX) 43 | // MOVBLZX AL, SI 44 | // LEAQ runtime.staticbytes(SB), R8 45 | // ADDQ R8, SI 46 | // MOVL runtime.writeBarrier(SB), R9 47 | // LEAQ 8(CX), DI 48 | // TESTL R9, R9 49 | // JNE 100 50 | // MOVQ SI, 8(CX) 51 | // JMP 40 52 | // MOVQ AX, R9 53 | // MOVQ SI, AX 54 | // CALL runtime.gcWriteBarrier(SB) 55 | // MOVQ R9, AX 56 | // JMP 40 57 | Eface = uint8(i) 58 | } 59 | }) 60 | b.Run("eface-zeroval", func(b *testing.B) { 61 | for i := 0; i < b.N; i++ { 62 | // MOVL $0, ""..autotmp_3+36(SP) 63 | // LEAQ type.uint32(SB), AX 64 | // MOVQ AX, (SP) 65 | // LEAQ ""..autotmp_3+36(SP), CX 66 | // MOVQ CX, 8(SP) 67 | // CALL runtime.convT2E32(SB) 68 | // MOVQ 16(SP), AX 69 | // MOVQ 24(SP), CX 70 | // MOVQ "".&Eface+48(SP), DX 71 | // MOVQ AX, (DX) 72 | // MOVL runtime.writeBarrier(SB), AX 73 | // LEAQ 8(DX), DI 74 | // TESTL AX, AX 75 | // JNE 152 76 | // MOVQ CX, 8(DX) 77 | // JMP 46 78 | // MOVQ CX, AX 79 | // CALL runtime.gcWriteBarrier(SB) 80 | // JMP 46 81 | Eface = uint32(i - i) // outsmart the compiler 82 | } 83 | }) 84 | b.Run("eface-static", func(b *testing.B) { 85 | for i := 0; i < b.N; i++ { 86 | // LEAQ type.uint64(SB), BX 87 | // MOVQ BX, (CX) 88 | // MOVL runtime.writeBarrier(SB), SI 89 | // LEAQ 8(CX), DI 90 | // TESTL SI, SI 91 | // JNE 92 92 | // LEAQ "".statictmp_0(SB), SI 93 | // MOVQ SI, 8(CX) 94 | // JMP 40 95 | // MOVQ AX, SI 96 | // LEAQ "".statictmp_0(SB), AX 97 | // CALL runtime.gcWriteBarrier(SB) 98 | // MOVQ SI, AX 99 | // LEAQ "".statictmp_0(SB), SI 100 | // JMP 40 101 | Eface = uint64(42) 102 | } 103 | }) 104 | } 105 | 106 | func main() { 107 | // So that we can easily compile this and retrieve `main.statictmp_0` 108 | // from the final executable. 109 | BenchmarkEfaceScalar(&testing.B{}) 110 | } 111 | -------------------------------------------------------------------------------- /chapter2_interfaces/eface_to_type.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | var j uint32 4 | var Eface interface{} // outsmart compiler (avoid static inference) 5 | 6 | func assertion() { 7 | i := uint32(42) 8 | Eface = i 9 | 10 | // 0x0065 00101 MOVQ "".Eface(SB), AX ;; AX = Eface._type 11 | // 0x006c 00108 MOVQ "".Eface+8(SB), CX ;; CX = Eface.data 12 | // 0x0073 00115 LEAQ type.uint32(SB), DX ;; DX = type.uint32 13 | // 0x007a 00122 CMPQ AX, DX ;; Eface._type == type.uint32 ? 14 | // 0x007d 00125 JNE 162 ;; no? panic our way outta here 15 | // 0x007f 00127 MOVL (CX), AX ;; AX = *Eface.data 16 | // 0x0081 00129 MOVL AX, "".j(SB) ;; j = AX = *Eface.data 17 | // ;; exit 18 | // 0x0087 00135 MOVQ 40(SP), BP 19 | // 0x008c 00140 ADDQ $48, SP 20 | // 0x0090 00144 RET 21 | // ;; panic: interface conversion: is , not 22 | // 0x00a2 00162 MOVQ AX, (SP) ;; have: Eface._type 23 | // 0x00a6 00166 MOVQ DX, 8(SP) ;; want: type.uint32 24 | // 0x00ab 00171 LEAQ type.interface {}(SB), AX ;; AX = type.interface{} (eface) 25 | // 0x00b2 00178 MOVQ AX, 16(SP) ;; iface: AX 26 | // 0x00b7 00183 CALL runtime.panicdottypeE(SB) ;; func panicdottypeE(have, want, iface *_type) 27 | // 0x00bc 00188 UNDEF 28 | // 0x00be 00190 NOP 29 | j = Eface.(uint32) 30 | } 31 | 32 | func typeSwitch() { 33 | i := uint32(42) 34 | Eface = i 35 | 36 | // ;; switch v := Eface.(type) 37 | // 0x0065 00101 MOVQ "".Eface(SB), AX ;; AX = Eface._type 38 | // 0x006c 00108 MOVQ "".Eface+8(SB), CX ;; CX = Eface.data 39 | // 0x0073 00115 TESTQ AX, AX ;; Eface._type == nil ? 40 | // 0x0076 00118 JEQ 153 ;; yes? exit the switch 41 | // 0x0078 00120 MOVL 16(AX), DX ;; DX = Eface.type._hash 42 | // ;; case uint32 43 | // 0x007b 00123 CMPL DX, $-800397251 ;; Eface.type._hash == type.uint32.hash ? 44 | // 0x0081 00129 JNE 163 ;; no? go to next case (uint16) 45 | // 0x0083 00131 LEAQ type.uint32(SB), BX ;; BX = type.uint32 46 | // 0x008a 00138 CMPQ BX, AX ;; type.uint32 == Eface._type ? (HASH COLLISION?) 47 | // 0x008d 00141 JNE 206 ;; no? clear BX and go to next case (uint16) 48 | // 0x008f 00143 MOVL (CX), BX ;; BX = *Eface.data 49 | // 0x0091 00145 JNE 163 ;; landsite for indirect jump starting at 0x00d3 50 | // 0x0093 00147 MOVL BX, "".j(SB) ;; j = BX = *Eface.data 51 | // ;; exit 52 | // 0x0099 00153 MOVQ 40(SP), BP 53 | // 0x009e 00158 ADDQ $48, SP 54 | // 0x00a2 00162 RET 55 | // ;; case uint16 56 | // 0x00a3 00163 CMPL DX, $-269349216 ;; Eface.type._hash == type.uint16.hash ? 57 | // 0x00a9 00169 JNE 153 ;; no? exit the switch 58 | // 0x00ab 00171 LEAQ type.uint16(SB), DX ;; DX = type.uint16 59 | // 0x00b2 00178 CMPQ DX, AX ;; type.uint16 == Eface._type ? (HASH COLLISION?) 60 | // 0x00b5 00181 JNE 199 ;; no? clear AX and exit the switch 61 | // 0x00b7 00183 MOVWLZX (CX), AX ;; AX = uint16(*Eface.data) 62 | // 0x00ba 00186 JNE 153 ;; landsite for indirect jump starting at 0x00cc 63 | // 0x00bc 00188 MOVWLZX AX, AX ;; AX = uint16(AX) (redundant) 64 | // 0x00bf 00191 MOVL AX, "".j(SB) ;; j = AX = *Eface.data 65 | // 0x00c5 00197 JMP 153 ;; we're done, exit the switch 66 | // ;; indirect jump table 67 | // 0x00c7 00199 MOVL $0, AX ;; AX = $0 68 | // 0x00cc 00204 JMP 186 ;; indirect jump to 153 (exit) 69 | // 0x00ce 00206 MOVL $0, BX ;; BX = $0 70 | // 0x00d3 00211 JMP 145 ;; indirect jump to 163 (case uint16) 71 | switch v := Eface.(type) { 72 | case uint16: 73 | j = uint32(v) 74 | case uint32: 75 | j = v 76 | } 77 | } 78 | 79 | func main() { 80 | assertion() 81 | typeSwitch() 82 | } 83 | -------------------------------------------------------------------------------- /chapter2_interfaces/eface_to_type_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "testing" 4 | 5 | var j uint32 6 | var eface interface{} = uint32(42) 7 | 8 | func BenchmarkEfaceToType(b *testing.B) { 9 | b.Run("switch-small", func(b *testing.B) { 10 | for i := 0; i < b.N; i++ { 11 | switch v := eface.(type) { 12 | case int8: 13 | j = uint32(v) 14 | case uint32: 15 | j = uint32(v) 16 | case int16: 17 | j = uint32(v) 18 | default: 19 | j = v.(uint32) 20 | } 21 | } 22 | }) 23 | b.Run("switch-big", func(b *testing.B) { 24 | for i := 0; i < b.N; i++ { 25 | switch v := eface.(type) { 26 | case int8: 27 | j = uint32(v) 28 | case int16: 29 | j = uint32(v) 30 | case int32: 31 | j = uint32(v) 32 | case uint32: 33 | j = uint32(v) 34 | case int64: 35 | j = uint32(v) 36 | case uint8: 37 | j = uint32(v) 38 | case uint16: 39 | j = uint32(v) 40 | case uint64: 41 | j = uint32(v) 42 | default: 43 | j = v.(uint32) 44 | } 45 | } 46 | }) 47 | } 48 | 49 | func main() {} 50 | -------------------------------------------------------------------------------- /chapter2_interfaces/eface_type_hash.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "unsafe" 6 | ) 7 | 8 | // simplified definitions of runtime's eface & _type types 9 | type eface struct { 10 | _type *_type 11 | data unsafe.Pointer 12 | } 13 | type _type struct { 14 | size uintptr 15 | ptrdata uintptr 16 | hash uint32 17 | /* omitted lotta fields */ 18 | } 19 | 20 | // ----------------------------------------------------------------------------- 21 | 22 | var Eface interface{} 23 | 24 | func main() { 25 | Eface = uint32(42) 26 | fmt.Printf("eface._type.hash = %d\n", 27 | int32((*eface)(unsafe.Pointer(&Eface))._type.hash)) 28 | 29 | Eface = uint16(42) 30 | fmt.Printf("eface._type.hash = %d\n", 31 | int32((*eface)(unsafe.Pointer(&Eface))._type.hash)) 32 | } 33 | -------------------------------------------------------------------------------- /chapter2_interfaces/escape.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type Addifier interface{ Add(a, b int32) int32 } 4 | 5 | type Adder struct{ id int32 } 6 | 7 | //go:noinline 8 | func (adder Adder) Add(a, b int32) int32 { return a + b } 9 | 10 | func main() { 11 | adder := Adder{id: 6754} 12 | adder.Add(10, 32) 13 | Addifier(adder).Add(10, 32) 14 | } 15 | -------------------------------------------------------------------------------- /chapter2_interfaces/escape_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "testing" 4 | 5 | type Addifier interface{ Add(a, b int32) int32 } 6 | 7 | type Adder struct{ id int32 } 8 | 9 | //go:noinline 10 | func (adder Adder) Add(a, b int32) int32 { return a + b } 11 | 12 | func BenchmarkDirect(b *testing.B) { 13 | adder := Adder{id: 6754} 14 | for i := 0; i < b.N; i++ { 15 | adder.Add(10, 32) 16 | } 17 | } 18 | 19 | func BenchmarkInterface(b *testing.B) { 20 | adder := Adder{id: 6754} 21 | for i := 0; i < b.N; i++ { 22 | Addifier(adder).Add(10, 32) 23 | } 24 | } 25 | 26 | func main() {} 27 | -------------------------------------------------------------------------------- /chapter2_interfaces/iface.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type Mather interface { 4 | Add(a, b int32) int32 5 | Sub(a, b int64) int64 6 | } 7 | 8 | type Adder struct{ id int32 } 9 | 10 | //go:noinline 11 | func (adder Adder) Add(a, b int32) int32 { return a + b } 12 | 13 | //go:noinline 14 | func (adder Adder) Sub(a, b int64) int64 { return a - b } 15 | 16 | func main() { 17 | m := Mather(Adder{id: 6754}) 18 | 19 | // This call just makes sure that the interface is actually used. 20 | // Without this call, the linker would see that the interface defined above 21 | // is in fact never used, and thus would optimize it out of the final 22 | // executable. 23 | m.Add(10, 32) 24 | } 25 | -------------------------------------------------------------------------------- /chapter2_interfaces/iface_bench_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "math/rand" 5 | "runtime" 6 | "testing" 7 | ) 8 | 9 | // ----------------------------------------------------------------------------- 10 | 11 | type identifier interface { 12 | idInline() int32 13 | idNoInline() int32 14 | } 15 | 16 | type id32 struct{ id int32 } 17 | 18 | // NOTE: Use pointer receivers so we don't measure the extra overhead incurred by 19 | // autogenerated wrappers as part of our results. 20 | 21 | func (id *id32) idInline() int32 { return id.id } 22 | 23 | //go:noinline 24 | func (id *id32) idNoInline() int32 { return id.id } 25 | 26 | // ----------------------------------------------------------------------------- 27 | 28 | const _maxSize = 2097152 // 2^21 29 | const _maxSizeModMask = _maxSize - 1 // avoids a mod (%) in the hot path 30 | 31 | var _randIndexes = [_maxSize]int{} 32 | 33 | func init() { 34 | rand.Seed(42) 35 | for i := range _randIndexes { 36 | _randIndexes[i] = rand.Intn(_maxSize) 37 | } 38 | } 39 | 40 | var escapeMePlease *id32 41 | 42 | // escapeToHeap makes sure that `id` escapes to the heap. 43 | // 44 | // In simple situations such as some of the benchmarks present in this file, 45 | // the compiler is able to statically infer the underlying type of the 46 | // interface (or rather the type of the data it points to, to be pedantic) and 47 | // ends up replacing what should have been a dynamic method call by a 48 | // static call. 49 | // This anti-optimization prevents this extra cleverness. 50 | // 51 | //go:noinline 52 | func escapeToHeap(id *id32) identifier { 53 | escapeMePlease = id 54 | return escapeMePlease 55 | } 56 | 57 | func BenchmarkMethodCall_direct(b *testing.B) { 58 | adders := make([]*id32, _maxSize) 59 | for i := range adders { 60 | adders[i] = escapeToHeap(&id32{id: int32(i)}).(*id32) 61 | } 62 | runtime.GC() 63 | 64 | var myID int32 65 | 66 | b.Run("single/noinline", func(b *testing.B) { 67 | m := escapeToHeap(&id32{id: 6754}).(*id32) 68 | b.ResetTimer() 69 | for i := 0; i < b.N; i++ { 70 | // CALL "".(*id32).idNoInline(SB) 71 | // MOVL 8(SP), AX 72 | // MOVQ "".&myID+40(SP), CX 73 | // MOVL AX, (CX) 74 | myID = m.idNoInline() 75 | } 76 | }) 77 | b.Run("single/inline", func(b *testing.B) { 78 | m := escapeToHeap(&id32{id: 6754}).(*id32) 79 | b.ResetTimer() 80 | for i := 0; i < b.N; i++ { 81 | // MOVL (DX), SI 82 | // MOVL SI, (CX) 83 | myID = m.idInline() 84 | } 85 | }) 86 | 87 | b.Run("many/noinline/small_incr", func(b *testing.B) { 88 | var m *id32 89 | b.Run("baseline", func(b *testing.B) { 90 | for i := 0; i < b.N; i++ { 91 | m = adders[i&_maxSizeModMask] 92 | } 93 | }) 94 | b.Run("call", func(b *testing.B) { 95 | for i := 0; i < b.N; i++ { 96 | m = adders[i&_maxSizeModMask] 97 | myID = m.idNoInline() 98 | } 99 | }) 100 | }) 101 | b.Run("many/noinline/big_incr", func(b *testing.B) { 102 | var m *id32 103 | b.Run("baseline", func(b *testing.B) { 104 | j := 0 105 | for i := 0; i < b.N; i++ { 106 | m = adders[j&_maxSizeModMask] 107 | j += 32 108 | } 109 | }) 110 | b.Run("call", func(b *testing.B) { 111 | j := 0 112 | for i := 0; i < b.N; i++ { 113 | m = adders[j&_maxSizeModMask] 114 | myID = m.idNoInline() 115 | j += 32 116 | } 117 | }) 118 | }) 119 | b.Run("many/noinline/random_incr", func(b *testing.B) { 120 | var m *id32 121 | b.Run("baseline", func(b *testing.B) { 122 | for i := 0; i < b.N; i++ { 123 | m = adders[_randIndexes[i&_maxSizeModMask]] 124 | } 125 | }) 126 | b.Run("call", func(b *testing.B) { 127 | for i := 0; i < b.N; i++ { 128 | m = adders[_randIndexes[i&_maxSizeModMask]] 129 | myID = m.idNoInline() 130 | } 131 | }) 132 | }) 133 | } 134 | 135 | func BenchmarkMethodCall_interface(b *testing.B) { 136 | adders := make([]identifier, _maxSize) 137 | for i := range adders { 138 | adders[i] = escapeToHeap(&id32{id: int32(i)}) 139 | } 140 | runtime.GC() 141 | 142 | var myID int32 143 | 144 | b.Run("single/noinline", func(b *testing.B) { 145 | m := escapeToHeap(&id32{id: 6754}) 146 | b.ResetTimer() 147 | for i := 0; i < b.N; i++ { 148 | // MOVQ 32(AX), CX 149 | // MOVQ "".m.data+40(SP), DX 150 | // MOVQ DX, (SP) 151 | // CALL CX 152 | // MOVL 8(SP), AX 153 | // MOVQ "".&myID+48(SP), CX 154 | // MOVL AX, (CX) 155 | myID = m.idNoInline() 156 | } 157 | }) 158 | b.Run("single/inline", func(b *testing.B) { 159 | m := escapeToHeap(&id32{id: 6754}) 160 | b.ResetTimer() 161 | for i := 0; i < b.N; i++ { 162 | // MOVQ 24(AX), CX 163 | // MOVQ "".m.data+40(SP), DX 164 | // MOVQ DX, (SP) 165 | // CALL CX 166 | // MOVL 8(SP), AX 167 | // MOVQ "".&myID+48(SP), CX 168 | // MOVL AX, (CX) 169 | myID = m.idInline() 170 | } 171 | }) 172 | 173 | b.Run("many/noinline/small_incr", func(b *testing.B) { 174 | var m identifier 175 | b.Run("baseline", func(b *testing.B) { 176 | for i := 0; i < b.N; i++ { 177 | m = adders[i&_maxSizeModMask] 178 | } 179 | }) 180 | b.Run("call", func(b *testing.B) { 181 | for i := 0; i < b.N; i++ { 182 | m = adders[i&_maxSizeModMask] 183 | myID = m.idNoInline() 184 | } 185 | }) 186 | }) 187 | b.Run("many/noinline/big_incr", func(b *testing.B) { 188 | var m identifier 189 | b.Run("baseline", func(b *testing.B) { 190 | j := 0 191 | for i := 0; i < b.N; i++ { 192 | m = adders[j&_maxSizeModMask] 193 | j += 32 194 | } 195 | }) 196 | b.Run("call", func(b *testing.B) { 197 | j := 0 198 | for i := 0; i < b.N; i++ { 199 | m = adders[j&_maxSizeModMask] 200 | myID = m.idNoInline() 201 | j += 32 202 | } 203 | }) 204 | }) 205 | b.Run("many/noinline/random_incr", func(b *testing.B) { 206 | var m identifier 207 | b.Run("baseline", func(b *testing.B) { 208 | for i := 0; i < b.N; i++ { 209 | m = adders[_randIndexes[i&_maxSizeModMask]] 210 | } 211 | }) 212 | b.Run("call", func(b *testing.B) { 213 | for i := 0; i < b.N; i++ { 214 | m = adders[_randIndexes[i&_maxSizeModMask]] 215 | myID = m.idNoInline() 216 | } 217 | }) 218 | }) 219 | } 220 | 221 | func main() {} 222 | -------------------------------------------------------------------------------- /chapter2_interfaces/iface_type_hash.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "unsafe" 6 | ) 7 | 8 | type Mather interface { 9 | Add(a, b int32) int32 10 | Sub(a, b int64) int64 11 | } 12 | 13 | type Adder struct{ id int32 } 14 | 15 | //go:noinline 16 | func (adder Adder) Add(a, b int32) int32 { return a + b } 17 | 18 | //go:noinline 19 | func (adder Adder) Sub(a, b int64) int64 { return a - b } 20 | 21 | func main() { 22 | m := Mather(Adder{id: 6754}) 23 | 24 | iface := (*iface)(unsafe.Pointer(&m)) 25 | fmt.Printf("iface.tab.hash = %#x\n", iface.tab.hash) 26 | 27 | } 28 | 29 | // simplified definitions of runtime's iface & itab types 30 | 31 | type iface struct { 32 | tab *itab 33 | data unsafe.Pointer 34 | } 35 | type itab struct { 36 | inter uintptr 37 | _type uintptr 38 | hash uint32 39 | _ [4]byte 40 | fun [1]uintptr 41 | } 42 | -------------------------------------------------------------------------------- /chapter2_interfaces/zerobase.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | _ "unsafe" 6 | ) 7 | 8 | //go:linkname zerobase runtime.zerobase 9 | var zerobase uintptr 10 | 11 | func main() { 12 | var s struct{} 13 | var a [42]struct{} 14 | 15 | fmt.Printf("zerobase = %p\n", &zerobase) 16 | fmt.Printf(" s = %p\n", &s) 17 | fmt.Printf(" a = %p\n", &a) 18 | } 19 | -------------------------------------------------------------------------------- /chapter2_interfaces/zeroval.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "unsafe" 6 | ) 7 | 8 | //go:linkname zeroVal runtime.zeroVal 9 | var zeroVal uintptr 10 | 11 | type eface struct{ _type, data unsafe.Pointer } 12 | 13 | func main() { 14 | x := 42 15 | var i interface{} = x - x // outsmart the compiler (avoid static inference) 16 | 17 | fmt.Printf("zeroVal = %p\n", &zeroVal) 18 | fmt.Printf(" i = %p\n", ((*eface)(unsafe.Pointer(&i))).data) 19 | } 20 | -------------------------------------------------------------------------------- /chapter3_garbage_collector/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Soon! 5 | --------------------------------------------------------------------------------