浅谈解释型语言 PHP 和编译型语言 Go 特性
浅谈解释型语言 PHP
和编译型语言 Go
特性
分享人:zxy_coding
时长:40 min
写在前面
本次分享的目的旨在互相交流,欢迎会后大家多多讨论交流。不会花过多的时间在细节上,同时请各位大佬轻喷。
在分享之前,请允许我简单的带大家温习下一些会提到的点:
- 高级语言 vs 低级语言: 这两者是一个相对概念,指的是语言的抽象程度,而不是其优劣。高级语言具有更高的抽象性,能够帮助开发者专注于业务逻辑,而无需过多关心底层硬件的实现细节。
PHP
就是一种典型的高级语言,它提供了面向对象编程等高级特性,让代码更加简洁易懂。使用高级语言编程时,开发者不需要考虑内存管理或硬件操作细节,因为编译器或解释器会将这些高级代码翻译成机器可以理解的语言。
低级语言:更接近计算机硬件,开发者需要直接操作内存、寄存器等底层资源。通常指的是机器语言和汇编语言,机器语言和汇编常用于系统编程、硬件驱动开发等需要与底层硬件打交道的领域。
机器语言:这是计算机可以直接执行的二进制代码,由0和1组成,它对程序员来说是难以理解和编写的。
汇编语言:它使用助记符(如 MOV
, ADD
)来代表机器指令,相对于机器语言来说更易于理解,与计算机的硬件架构紧密相关,需要开发者对处理器的指令集有深入的了解。
- 一个常见的问题
我们工作中经常会遇到在本地开发环境编译和运行正常的代码,在生产环境却无法正常工作。这个问题的原因可能有很多,在这里我们只讨论其中原因之一 --> 不同机器使用不同的指令集。
复杂指令集计算机(CISC)和精简指令集计算机(RISC)是两种遵循不同设计理念的指令集,从名字我们就可以推测出这两种指令集的区别:
复杂指令集:通过增加指令的类型减少需要执行的指令数;
精简指令集:使用更少的指令类型实现目标的计算任务;
AST
抽象语法树:是一种以树状的方式表示的语法结构,构造方式分为自顶而下和自底向上两种,其每一个节点都表示源代码中的一个元素,每一个子树表示一个语法元素。比如 (1 + 1)* 2
一、计算机是如何认识代码的?
计算机的核心是
CPU
,它通过复杂的电子指令集来解释和执行机器代码。这些代码是高度抽象的高级语言(如PHP
和Go
)经过不同程度的转换,最终生成到计算机可以直接执行低级语言。这一过程分为多步,而PHP
和Go
则通过不同的机制完成这些步骤。
(一)、机器语言与汇编语言
- 机器语言:计算机硬件直接理解和执行的二进制代码,由一系列的0和1组成,对应于
CPU
的指令集。 - 汇编语言:一种低级语言,它使用助记符来代表机器指令,更易于理解和记忆。
(二)、代码的编译/解释过程
- 编译可分为两部分,每个部分又可以细分为三个阶段。这两部分别是编译前端和编译后端,也称分析部分和合成部分。编译器的前端一般承担着词法分析、语法分析、类型检查和中间代码生成几部分工作,而编译器后端主要负责目标代码的生成和优化,也就是将中间代码翻译成目标机器能够运行的二进制机器码。
(三)、CPU如何执行机器码
- 指令周期:
CPU
执行机器码的过程是通过指令周期来完成的,包括取指、解码、执行、访存、写回等步骤。 - 寄存器:
CPU
内部有多个寄存器,用于存储指令、数据和地址等信息。寄存器是CPU
执行指令时最快的数据存储位置。 - 指令指针:程序计数器
PC
是一个特殊的寄存器,它指向下一条要执行的指令的地址。 - 执行引擎:
CPU
的执行引擎根据解码后的指令执行相应的操作,如算术运算、逻辑运算、数据传输等。
(四)、内存与存储
- 指令与数据:机器码中的指令部分告诉
CPU
要执行的操作,数据部分则是操作的对象。 - 内存寻址:
CPU
通过内存地址来访问存储在RAM
中的指令和数据。 - 缓存:
CPU
缓存用于减少访问主内存的次数,提高数据存取速度。
通过上述过程,计算机将人类可读的代码转换为其硬件能够理解的机器码,并执行这些指令来完成特定的计算任务。这个过程涉及到编译器、解释器、CPU
、内存等多个组件的协同工作。
二、PHP 是如何执行的?
(一)、执行分析
PHP 是一种解释型语言,这意味着它的代码并不直接编译成机器代码,而是由解释器(如 Zend 引擎)逐行执行。我们可以通过以下示例来理解 PHP 的工作过程:
$code =<<<'PHP_CODE'
<?php
//这是注释
echo "Hello, world\n";
$data = 1 + 1;
echo $data;
PHP_CODE;
token_get_all($code);
- 词法分析:生成
Tokens
。
在这一步,PHP 会对源码进行词法分析,将代码分解为最小的语言元素,称为“词法单元”(Tokens)。每个 Token 都是程序中不可再分的最小单位,比如关键字、标识符、运算符等。 通过 PHP 内置函数 token_get_all(),我们可以看到代码被解析为以下 Tokens:
array:15 [
[T_OPEN_TAG, "<?php"]
[T_ECHO, "echo"]
[T_CONSTANT_ENCAPSED_STRING, "\"Hello, world\n\""]
";"
[T_VARIABLE, "$data"]
"="
[T_LNUMBER, "1"]
"+"
[T_LNUMBER, "1"]
";"
[T_ECHO, "echo"]
[T_VARIABLE, "$data"]
";"
]
- 语法分析:生成抽象语法树
AST
。
接下来是语法分析,对于
AST
语法树构造的方法通常是自顶而下和自底向上两种,PHP 的语法分析通常是通过自顶向下的解析方法进行的。在编译原理中,自顶向下的解析是指从语法树的根部开始,根据语言的语法规则逐步向下构建语法树,直到整个输入都被解析。一定会从开始符号分析,通过下一个即将入栈的符号判断应该如何对当前堆栈中最右侧的非终结符(𝑆
或 𝑆1
)进行展开,直到整个字符串中不存在任何的非终结符,整个解析过程才会结束。
AST` 通过分析代码的结构,确定语句的合法性,并生成更高级的树形结构,表示程序的语法规则。PHP 7 之后,语法解析生成了更高效的 AST,进一步加快了执行速度。语法树中可以看到程序的各个部分是如何组合的,例如 AST_ASSIGN 表示赋值操作,BINARY_OP 表示加法运算。
示例生成 AST:
php -d ast.dump_bailout=1 -d ast.process=1 -r '$code = "<?php echo \"Hello, world\";"; ast\parse_code($code, 70);'
AST 示例
AST_STMT_LIST
AST_ECHO
STRING("Hello, world")
AST_ASSIGN
VAR('$data')
BINARY_OP('+')
LITERAL('1')
LITERAL('1')
AST_ECHO
VAR('$data')
- 语义分析:检查代码逻辑。
在语法分析之后,PHP 会对代码进行语义分析,检查变量的作用域、类型一致性等逻辑规则,确保代码在运行时不会产生语义错误。
语义错误示例
$data = 1 + "string"; // 语义错误,数字和字符串相加会在运行时产生警告
- 中间码生成:生成
OpCode
。
PHP 不直接生成机器代码,而是将代码转换为中间代码(OpCodes),这是跨平台的一种中间形式,适用于不同的操作系统和 CPU 架构。Zend 引擎将生成的 OpCodes 逐行解释和执行。
示例 OpCode:
echo $data = 1 + 1;
使用 VLD
扩展查看生成的 OpCode
:
line # * op fetch ext return operands
---------------------------------------------------------------------------------
2 0 > ASSIGN !0, 1
2 1 ADD ~1, 1, 1
2 2 ECHO !0
2 3 RETURN 1
OpCode 显示了每条指令的操作类型(如 ASSIGN
、ADD
、ECHO
)及其操作数。PHP 解释器会逐行执行这些指令。
- 代码优化:移除冗余代码。
PHP 在生成中间码的过程中会进行一些优化,比如移除不必要的代码、合并常量等,以提高执行效率
echo $data = 1 + 1;
例如,在上面的加法操作中,1 + 1 可以在编译时直接优化为 2,从而减少运行时计算。
- 机器代码执行:由解释器逐行执行
OpCode
。
PHP 并不会像 Go 一样生成机器代码,而是依赖解释器逐行解释和执行 OpCodes。尽管如此,通过某些优化手段(如 JIT 编译器,将解释型代码转换为机器码),PHP 可以在某些情况下将热点代码转化为机器代码,从而提升性能。
JIT 编译示例:
opcache.enable=1
opcache.jit_buffer_size=100M
opcache.jit=tracing
通过 JIT,PHP 将热点代码块编译为机器码,以提高执行效率。JIT 尤其在 CPU 密集型任务(如科学计算、图像处理等)中表现优异。
(二)、内存分析
分析示例
让我们逐步分析代码 $data = 1 + 1; echo $data; 的内存管理过程:
PHP 的内存管理主要依赖于堆(heap)和栈(stack)来存储变量、函数调用等。
栈用于存储局部变量、函数调用信息,而堆则用于动态分配内存。
- 栈与堆的内存分配
在 PHP 中,栈主要用于存储函数调用信息、局部变量等,堆则用于动态内存分配。对于代码 $data = 1 + 1;以下是内存分配的概述:
- 分配内存:Zend 引擎首先在栈上分配一个存储 $data 的位置,同时在堆上为 2 分配内存。
zval {
string "data" //变量的名字是data
value zend_value //变量的值,联合体
type intger //变量是数值类型
}
-
栈空间(Stack):存储局部变量 $data 和操作的调用信息。在函数调用时,PHP 将局部变量、参数和返回地址存储在栈上。栈是 LIFO(Last In First Out)的数据结构,函数调用结束后,栈帧(stack frame)会被弹出,释放存储空间。
-
堆空间(Heap):存储计算结果 1 + 1,以及后续赋值给 $data 的结果。堆用于存储动态分配的变量,垃圾回收机制会定期扫描并释放不再使用的内存。
- 堆栈管理
在 PHP 中,函数调用和局部变量的管理依赖于栈空间。每当函数调用时,局部变量和参数会被压入栈中,函数执行完毕后,栈会自动弹出这些变量。PHP 的函数调用开销相对较大,特别是在递归调用时,会占用大量栈空间,导致性能下降。
- 引用计数器(Reference Counting)
PHP 使用引用计数器来管理内存,尤其是对变量的引用。每当一个变量被赋值或传递时,引用计数器会增加,只有当计数器归零时,内存才会被回收。
$data = 1 + 1;
-
这行代码创建了一个数值 2,并赋给变量 $data。此时,数值 2 的引用计数为 1,因为 $data 是唯一引用该值的变量。如果后续有其他变量引用该值,引用计数将增加。
-
引用计数:$data 引用了堆中的 2,此时 2 的引用计数为 1。
echo xdebug_debug_zval( 'data');//refcount=1,is_ref=0
- 输出数据:echo $data; 访问 $data 的指针,取出堆中的数值 2,然后将它输出到控制台。
- 释放内存:当代码执行完毕后,$data 不再使用,其引用计数器归零,Zend 引擎通过垃圾回收机制将 Bucket 标记为空闲,回收内存。
无奖问答1
is_ref
是个啥?
is_ref
是个bool值,用来标识这个变量是否是属于引用集合(reference set)。通过这个字节,php引擎才能把普通变量和引用变量区分开来
显式引用赋值:当你使用 & 运算符创建一个引用时,被引用的变量的 is_ref
会被设置为1
引用变量被赋值一个非引用变量,那么原始变量的 is_ref
标志可能会被清除
$another = $data;
echo xdebug_debug_zval( 'data');//refcount=2
- 此时,$data 和 $another 都指向同一个值 2,它的引用计数将变为 2。
unset($data);
echo xdebug_debug_zval( 'data');//refcount=0
- 当引用计数归零时,PHP 会自动通过垃圾回收机制释放内存。
- Zend Memory Manager
Zend 引擎中的 Zend Memory Manager 负责分配和回收内存,Buckets 用于优化小块内存的管理,并通过指针和信号控制内存的释放和引用。当变量不再使用时,PHP 的垃圾回收机制会释放相关的内存。
Zend 引擎管理 PHP 的内存使用,它通过 Zend Memory Manager(ZMM)分配和回收内存。ZMM 使用了内存池的概念,所有的内存分配请求都由 Zend 引擎管理。
在一段比较短的时间内,许多zval结构大小的内存块和其他的小内存块被申请又再被释放,PHP的内存管理也很重视memory_limit(内存限制)
为了满足以上的需求,Zend引擎提供了为了处理请求相关数据提供了一种特殊的内存管理器。请求相关数据是指只需要服务于单个请求,最迟在请求结束时释放的数据。扩展开发者主要接触下表中列出的惯例,虽然一些所提供的便捷功能使用宏实现的,可当函数看待。
内存块与 Free Buckets
ZMM 会将内存划分为多个内存块,称为 Buckets,每个 Bucket 对应特定大小的内存分块。不同大小的内存需求会被分配到不同的内存块中。当某个 Bucket 不再使用时,它将被标记为“空闲”,并加入到 Free Buckets 列表中以便后续分配。
例如,执行 $data = 1 + 1; 时:
- 一个新的 Bucket 被分配给 2 的值。
- 当 $data 不再引用该值时,引用计数器变为 0,垃圾回收机制会将该 Bucket 标记为“空闲”并回收。
Bucket 的内存布局
| Bucket |
|--------|
| RefCount | --> 引用计数器,跟踪当前值被引用的次数
| DataPtr | --> 指向实际存储数值的内存地址
| TypeInfo | --> 数据类型信息 (如 IS_LONG 表示整数)
当 $data 指向数值 2 时,Bucket 的 RefCount 为 1,DataPtr 指向堆中的数据,TypeInfo 表示它是一个整数类型。
- 垃圾回收机制(Garbage Collection, GC)
PHP 通过引用计数器和垃圾回收机制(Garbage Collection, GC)管理堆上的内存 。
PHP 使用垃圾回收机制来管理内存回收,特别是处理循环引用的情况。虽然引用计数器可以处理大多数的内存管理情况,但它无法自动解决循环引用导致的内存
泄露。垃圾回收机制会定期检查内存中的循环引用,并释放无用的内存。对于复杂对象或循环引用,依赖gc_collect_cycles()
来回收垃圾。
$data = 1 + 1;
在该代码中并不会产生循环引用,引用计数器即可管理内存。当执行完代码后,如果 $data 不再被使用,垃圾回收机制将会回收对应的内存块。
- 延伸思考
如何在编码过程中更好地管理内存以及提升性能呢?
🌟 手动触发垃圾回收(GC)&& 使用 unset
及时释放内存
tips: PHP
的引用计数无法自动处理循环引用的问题,可以通过 gc_collect_cycles()
手动触发垃圾回收,
减少内存压力避免内存泄漏。在变量不再需要时,及时使用 unset() 释放内存,尤其是在处理大数据集合或复杂对象时。
-
PHP
的垃圾回收并非立即触发,特别是在处理大数据集合时,可能导致内存占用持续增高,手动触发GC
可以提前释放未使用的内存,避免内存峰值过高。 -
即使
PHP
的垃圾回收机制能够自动释放内存,但主动unset
可以帮助尽早释放不再使用的内存,减少内存占用时间。 -
由于变量未及时释放或者引用关系复杂,长时间运行的
PHP
脚本可能会出现内存泄漏在使用循环、递归或长时间运行的脚本时,监控内存消耗,避免内存泄漏。 -
对象实例化后会占用堆内存,较长的生命周期可能导致内存一直无法释放,特别是在大量对象处理时,及时销毁不必要的对象可以减少内存占用。
$data = largeDataProcessing();
unset($data);
gc_collect_cycles(); // 手动触发 GC 回收未使用内存
注意⚠️ PHP 本身的 GC 已经可以应对大部分场景。手动触发 GC 只建议在处理特别大或复杂的数据结构时使用,否则可能增加性能开销。
✨ 避免不必要的引用
tips 尽量减少变量间不必要地引用,尤其是在数组或对象传递时。
- PHP 的引用计数机制有时会因为复杂的引用关系导致内存无法及时回收,进而增加内存泄漏风险。特别是在循环或递归调用中,这种情况可能更加严重。
function processData(array $data) {
// 避免使用 & 符号的引用传递
foreach ($data as $item) {
// do
}
}
🚩 使用合适的数据结构
tips 根据需要选择合适的数据结构,避免过多的嵌套数组和对象。
- 嵌套的数组和对象结构在 PHP 中会占用更多的内存。对于小数据集,选择简单的数组或标量类型可以有效降低内存使用。
// 尽量减少不必要的数组嵌套
$flatArray = ['key1' => 'value1', 'key2' => 'value2'];
⚠️ 避免全局变量
tips 尽量避免使用全局变量,将变量限制在局部范围内。
- 全局变量会一直占用内存,直到脚本执行结束。如果在函数内部定义局部变量,可以确保内存更早被释放。
function processData() {
$localData = fetchData(); // 使用局部变量
}
🛠 使用 SPL 数据结构
tips 对于特定的数据需求,可以使用 PHP 的 SPL(Standard PHP Library)数据结构,如 SplFixedArray
,以减少内存开销。
- 相比传统数组,
SplFixedArray
在处理固定大小的数据时占用更少的内存。
$fixedArray = new SplFixedArray(100);
(三)、执行性能分析
使用
XHProf
分析。XHProf
是一种轻量级的 PHP 性能分析工具。通过在 PHP 代码中插入XHProf
分析代码,可以收集到函数调用次数、内存占用等详细数据。XHProf
可以生成详细的函数调用栈分析报告,帮助开发者识别代码中的性能瓶颈。
<?php
function calculate_sum($a, $b) {
return $a + $b;
}
// 启用 XHProf
xhprof_enable();
$result = calculate_sum(1000, 2000);
// 停止 XHProf
$xhprof_data = xhprof_disable();
// 保存分析结果
$XHPROF_ROOT = '/path/to/xhprof';
include_once $XHPROF_ROOT . "/xhprof_lib/utils/xhprof_lib.php";
include_once $XHPROF_ROOT . "/xhprof_lib/utils/xhprof_runs.php";
$xhprof_runs = new XHProfRuns_Default();
$run_id = $xhprof_runs->save_run($xhprof_data, "xhprof_testing");
echo "XHProf Run ID: $run_id\n";
以下是运行 calculate_sum 函数后,XHProf 提供的分析结果的示例:
XHProf Run ID: 5f8e3a367a5ab
在 XHProf 的 UI 中,可以通过输入这个 Run ID 来查看详细的性能报告。以下是一个简化的报告示例:
Function Name Calls Incl. Wall Time Excl. Wall Time Incl. CPU Excl. CPU Incl. Memory Excl. Memory
被调用的函数名 函数被调用的次数 总时间(包括子函数)总时间(无子函数)使用CPU时间(含子)使用CPU时间(主) 使用的内存(含子)使用的内存(主)
------------------------------ ------ --------------- --------------- ------------ ------------ ------------ ------------
calculate_sum 1 0.000012 0.000012 0.000010 0.000010 0.000012 0.000012
main() 1 0.000016 0.000000 0.000014 0.000000 0.000016 0.000000
xhprof_disable 1 0.000004 0.000004 0.000004 0.000004 0.000004 0.000004
xhprof_enable 1 0.000002 0.000002 0.000002 0.000002 0.000002 0.000002
XHProfRuns_Default::save_run 1 0.000030 0.000030 0.000028 0.000028 0.000030 0.000030
Totals 5 0.000064 0.000060 0.000054 0.000050 0.000064 0.000060
示例分析图
三、Golang 浅析
总得来看,Go有如下特征
- 简洁的语法和快速的编译速度
- 内置的并发支持(goroutines和channels)
- 强大的标准库
- 跨平台编译能力
- 垃圾回收机制
Go 是一种编译型语言,意味着它的代码在执行之前必须先编译成二进制机器码。这种预编译的特性使得 Go 程序运行效率更高,接近于原生的 CPU 指令执行。
编译的流程就是将高级语言high-level langue
输出为机器码machine code
,Go 的编译器在逻辑上可以被分成四个阶段:词法与语法分析、类型检查和 AST 转换、通用 SSA 生成和最后的机器代码生成。
(一)、编译流程
Go 语言编译器的源代码位于
src/cmd/compile
,其中main.go
文件中标注了编译后可运行的架构
我们用下面code demo
来查看编译的详细过程
package main
import "fmt"
func main() {
a := 1 + 1
b := 10
c := a * b
fmt.Println(c)
}
执行 go build -n
#
# command-line-arguments
#
mkdir -p $WORK/b001/
cat >$WORK/b001/importcfg << 'EOF' # internal
# import config
packagefile fmt=/Users/zhangxingyu/Library/Caches/go-build/ed/edcc4e1dd9b7dc844dee66507f2ffc440c70771ad074d71b3e9a92d47a7e1cff-d
packagefile runtime=/Users/zhangxingyu/Library/Caches/go-build/63/63597b8610205389c71153f7e4bd88c5dc94112e9b6b973919f3299609383898-d
EOF
cd /Users/zhangxingyu/go/Test
/usr/local/go/pkg/tool/darwin_amd64/compile -o $WORK/b001/_pkg_.a -trimpath "$WORK/b001=>" -p main -lang=go1.23 -complete -buildid AjQUujbjn_XiRSPwjAf6/AjQUujbjn_XiRSPwjAf6 -goversion go1.23rc1 -c=4 -shared -nolocalimports -importcfg $WORK/b001/importcfg -pack ./x.go
/usr/local/go/pkg/tool/darwin_amd64/buildid -w $WORK/b001/_pkg_.a # internal
cat >$WORK/b001/importcfg.link << 'EOF' # internal
packagefile command-line-arguments=$WORK/b001/_pkg_.a
packagefile fmt=/Users/zhangxingyu/Library/Caches/go-build/ed/edcc4e1dd9b7dc844dee66507f2ffc440c70771ad074d71b3e9a92d47a7e1cff-d
packagefile runtime=/Users/zhangxingyu/Library/Caches/go-build/63/63597b8610205389c71153f7e4bd88c5dc94112e9b6b973919f3299609383898-d
packagefile errors=/Users/zhangxingyu/Library/Caches/go-build/18/18f8398963c850edd5eb24910741011ca89f66ce32c0433ab228980f2ce60cfd-d
packagefile internal/fmtsort=/Users/zhangxingyu/Library/Caches/go-build/a2/a21dd9b186e7824ae932378d36e39a7ca326bf7e9f59ebcf91b3cd84ab2a43ac-d
packagefile io=/Users/zhangxingyu/Library/Caches/go-build/cc/cc16604022481f4120a1cba23d68720eeef724d804865477f6b4917598733848-d
packagefile math=/Users/zhangxingyu/Library/Caches/go-build/49/49cbe346bd191efb0332a385d6d83be01ccf1e071d66fc11db3e0d3447d8e78a-d
packagefile os=/Users/zhangxingyu/Library/Caches/go-build/b8/b820d3604794695ba1bcaf2b12e94abcbdeb6e57e9bc5a5225863d72c9cda34e-d
packagefile reflect=/Users/zhangxingyu/Library/Caches/go-build/47/473887e7a10d9809ae03f697007145ed5ea920dfdb7be56b01789195bf2875e0-d
packagefile slices=/Users/zhangxingyu/Library/Caches/go-build/bb/bb59640518285f776880ed3b4ff157a947bbcaf90d402c2498224e17c6d78207-d
packagefile strconv=/Users/zhangxingyu/Library/Caches/go-build/e1/e1bcff2d13aaeeaa548a6b3544033a356acbdea97f9c919a92b0d74cb3978c2a-d
packagefile sync=/Users/zhangxingyu/Library/Caches/go-build/34/3422b6e80dc4fb46ada88e3e68a610c949b3addf3666bd448aa0feda8145a927-d
packagefile unicode/utf8=/Users/zhangxingyu/Library/Caches/go-build/89/896116a1b0547587026c982f3e5f55df7fb2ab8efdd4187eb341473328444da5-d
packagefile internal/abi=/Users/zhangxingyu/Library/Caches/go-build/81/815b79245b61ede581729076d83e42593da8e958322795e8c6a6303317e9a286-d
packagefile internal/bytealg=/Users/zhangxingyu/Library/Caches/go-build/0d/0d0ba0b401720287ee59c924a9b29e55fa7e7f3820d541cd0a6686f30219b25b-d
packagefile internal/chacha8rand=/Users/zhangxingyu/Library/Caches/go-build/bb/bb09e3ce1af410e64303c439cc073bcf843646a7e856869722f71e4a1332125d-d
packagefile internal/coverage/rtcov=/Users/zhangxingyu/Library/Caches/go-build/07/07b00d3c720fa4eba842784bd39721b5bbc28c382cd86f759a1b15d72a67e404-d
packagefile internal/cpu=/Users/zhangxingyu/Library/Caches/go-build/ca/ca21276f7ab2028e41051720b59a3055155472d7dea67d3b2bc6500b27ffbf34-d
packagefile internal/goarch=/Users/zhangxingyu/Library/Caches/go-build/3a/3ad1c99540a3526bec3e4cd6ef04b79defd115f0e350db580226587a0e91291a-d
packagefile internal/godebugs=/Users/zhangxingyu/Library/Caches/go-build/e2/e29307445721117857d3a0f7d4a5b657a981eac847cbb9a7afebec9c22e100c1-d
packagefile internal/goexperiment=/Users/zhangxingyu/Library/Caches/go-build/dd/dde5ced1979ac16a2f21851db3990fcaddfffc7258fa2a96cb1883838b998261-d
packagefile internal/goos=/Users/zhangxingyu/Library/Caches/go-build/01/01e7f7834cf96161e01fa8c37b40d2ca041bea99953be39b2ad4412a4ed5dcf9-d
packagefile internal/profilerecord=/Users/zhangxingyu/Library/Caches/go-build/cd/cd3cabece9a5270b7898c01e6a77b6f7b1b9e5b2af90e89d0c347ce23c96889b-d
packagefile internal/runtime/atomic=/Users/zhangxingyu/Library/Caches/go-build/cb/cbf4fcb24db0ad7f7c7b448d60fd77d5f3b1417fc8bcb23a672adce20ee6ff8d-d
packagefile internal/runtime/exithook=/Users/zhangxingyu/Library/Caches/go-build/d2/d29eb156263afeee1e32cb976667460cf64a32562cf021d95f7f5a3c1c894226-d
packagefile internal/stringslite=/Users/zhangxingyu/Library/Caches/go-build/f6/f6f24766074fa0a006a59d6b6e60c8c12e3618cc438242a650c6809fb14b1076-d
packagefile runtime/internal/math=/Users/zhangxingyu/Library/Caches/go-build/90/90d12095007ab6746654883ad3f7bc40e0a09840ed4952f08c74627dd2163038-d
packagefile runtime/internal/sys=/Users/zhangxingyu/Library/Caches/go-build/9c/9c968525cf83e0f537f03e19f94668c9df804444a30f4fef7f54549196b24e0d-d
packagefile internal/reflectlite=/Users/zhangxingyu/Library/Caches/go-build/bb/bb89592fe9dfe8129e818328134d63bb007230f0acafdf12ee7e5b447cd24d8a-d
packagefile cmp=/Users/zhangxingyu/Library/Caches/go-build/81/81927061cce76128ee93e6cef386d786644dc83da4ae9cf3c68646de1d8e1812-d
packagefile math/bits=/Users/zhangxingyu/Library/Caches/go-build/31/317498a35fa02d4296d743a494913082025b124e4b39cc1e1b8061bab8b2e164-d
packagefile internal/filepathlite=/Users/zhangxingyu/Library/Caches/go-build/a3/a37ac4454401c020f84324042278457897a267e18ab215b781f9c26ad650dc64-d
packagefile internal/itoa=/Users/zhangxingyu/Library/Caches/go-build/23/2325435abbde59645c4d8098762954b64c4873da8aa4589d530d543dafddc11c-d
packagefile internal/poll=/Users/zhangxingyu/Library/Caches/go-build/04/04929624e9ad9471dc8f1ae7218f9eb3d58a38e63b1b639a4096dae9e1b84e30-d
packagefile internal/syscall/execenv=/Users/zhangxingyu/Library/Caches/go-build/ad/ad48ad0523afd9e68b1cf4ba969a75e8d919ad6f816821d8ceb1ee63391253ed-d
packagefile internal/syscall/unix=/Users/zhangxingyu/Library/Caches/go-build/fd/fd2d0ac526a5381f9f557ddd7ced90990a61267216847bd55663303364e3bb69-d
packagefile internal/testlog=/Users/zhangxingyu/Library/Caches/go-build/6e/6ee6704fabad9d33c6be7cf50bf97da94e66ee737b6b93d2b8b76ee63a67abd4-d
packagefile io/fs=/Users/zhangxingyu/Library/Caches/go-build/27/276831353965ce667d7fb926e5b6844ab928c0dcbcb79e099794106d4756bbbc-d
packagefile sync/atomic=/Users/zhangxingyu/Library/Caches/go-build/44/443b8c4dd48db93c60d4f1e3c77060aa2f5b37bc5b5efafdd365f2442b5945a9-d
packagefile syscall=/Users/zhangxingyu/Library/Caches/go-build/39/395b73eb8e253503db13ffbbf6c9bd47106fdd44f2158c840e8cc0a5b3b18864-d
packagefile time=/Users/zhangxingyu/Library/Caches/go-build/f8/f8b79ea9beb2269fa5d08f139e05ba989821f1ae59853e6a6cc47fb27da00c5e-d
packagefile internal/unsafeheader=/Users/zhangxingyu/Library/Caches/go-build/2c/2c5da7a4d8cc66e5d56648d3a7ad29c8ca4dd25ada83bea4e0dbd6fb1e24cf5b-d
packagefile iter=/Users/zhangxingyu/Library/Caches/go-build/81/810d8ae67fb9f1c73666cde5a8d19a38cd1533440755dcddce617434473263f9-d
packagefile unicode=/Users/zhangxingyu/Library/Caches/go-build/2c/2cf7d192301df4d5a354d14e6eab3b193db6509695cd138da16b6ed9f781643e-d
packagefile internal/race=/Users/zhangxingyu/Library/Caches/go-build/30/3009ff731c36eca76b31437c6fdee62805f9f66e3e4f6174b6058b89004cf044-d
packagefile internal/byteorder=/Users/zhangxingyu/Library/Caches/go-build/01/012d9719828b4f09bdd2b1946888dd929df229aca19e90a8324b8b38fdc71c4d-d
packagefile internal/oserror=/Users/zhangxingyu/Library/Caches/go-build/4b/4b69313c3dfda694ad98906321395df0647752892041d3a35200574f5aee719c-d
packagefile path=/Users/zhangxingyu/Library/Caches/go-build/d1/d13152ec8ab73935e1b82e97f76ac63fc382bcc778098a9f83fd3f45333dd31a-d
packagefile internal/asan=/Users/zhangxingyu/Library/Caches/go-build/35/35c706bb7262b6b35b593c49c94c10281b6d39df581f6e354f0f2b81e14116ae-d
packagefile internal/msan=/Users/zhangxingyu/Library/Caches/go-build/b4/b4957b9c6b5c95e37d7014a5ad452938ca766a07ac2322afff39352a15052fa9-d
packagefile internal/godebug=/Users/zhangxingyu/Library/Caches/go-build/c4/c427226627d9b5d65ed63d43ca6d0ae2bec065720ca3eaa66cf799c52266d6ec-d
packagefile internal/bisect=/Users/zhangxingyu/Library/Caches/go-build/a8/a8ec5fd5abfe453c8e2e18c8d26dd1b94474f8f7001c46eced0b5f0ba7896315-d
modinfo "0w\xaf\f\x92t\b\x02A\xe1\xc1\a\xe6\xd6\x18\xe6path\tcommand-line-arguments\nbuild\t-buildmode=exe\nbuild\t-compiler=gc\nbuild\tCGO_ENABLED=1\nbuild\tCGO_CFLAGS=\nbuild\tCGO_CPPFLAGS=\nbuild\tCGO_CXXFLAGS=\nbuild\tCGO_LDFLAGS=\nbuild\tGOARCH=amd64\nbuild\tGOOS=darwin\nbuild\tGOAMD64=v1\n\xf92C1\x86\x18 r\x00\x82B\x10A\x16\xd8\xf2"
EOF
mkdir -p $WORK/b001/exe/
cd .
GOROOT='/usr/local/go' /usr/local/go/pkg/tool/darwin_amd64/link -o $WORK/b001/exe/a.out -importcfg $WORK/b001/importcfg.link -buildmode=pie -buildid=2Au8qZkG35Affq9gH0Id/AjQUujbjn_XiRSPwjAf6/AjQUujbjn_XiRSPwjAf6/2Au8qZkG35Affq9gH0Id -extld=clang $WORK/b001/_pkg_.a
/usr/local/go/pkg/tool/darwin_amd64/buildid -w $WORK/b001/exe/a.out # internal
mv $WORK/b001/exe/a.out x
rm -rf $WORK/b001/
内容过长,摘出其核心内容,编译的核心就是通过compile
、buildid
、link
三个命令会编译出可执行文件 a.out
。
1. 词法分析
所有的编译过程其实都是从解析代码的源文件开始的,词法分析的作用就是解析源代码文件,它将文件中的字符串序列转换成
Token
序列,方便后面的处理和解析,我们一般会把执行词法分析的程序称为词法解析器lexer
。
package main
import (
"fmt"
"go/scanner"
"go/token"
)
func main() {
src := `package main
func main() {
a := 1 + 1
b := 10
c := a * b
fmt.Println(c)
}`
fset := token.NewFileSet()
file := fset.AddFile("example.go", fset.Base(), len(src))
var s scanner.Scanner
s.Init(file, []byte(src), nil, scanner.ScanComments)
for {
pos, tok, lit := s.Scan()
if tok == token.EOF {
break
}
fmt.Printf("%s\t%s\t%q\n", fset.Position(pos), tok, lit)
}
}
翻译可得如下 token
example.go:1:1 package "package" // 双引号内是源代码,外部是翻译的 token
example.go:1:9 IDENT "main"
example.go:1:13 ; "\n" //空行会翻译成 ;
example.go:3:1 func "func"
example.go:3:6 IDENT "main"
example.go:3:10 ( ""
example.go:3:11 ) ""
example.go:3:13 { ""
example.go:4:5 IDENT "a"
example.go:4:7 := ""
example.go:4:10 INT "1"
example.go:4:12 + ""
example.go:4:14 INT "1"
example.go:4:15 ; "\n"
example.go:5:5 IDENT "b"
example.go:5:7 := ""
example.go:5:10 INT "10"
example.go:5:12 ; "\n"
example.go:7:5 IDENT "c"
example.go:7:7 := ""
example.go:7:10 IDENT "a"
example.go:7:12 * ""
example.go:7:14 IDENT "b"
example.go:7:15 ; "\n"
example.go:8:5 IDENT "fmt"
example.go:8:8 . ""
example.go:8:9 IDENT "Println"
example.go:8:16 ( ""
example.go:8:17 IDENT "c"
example.go:8:18 ) ""
example.go:8:19 ; "\n"
example.go:9:1 } ""
example.go:9:2 ; "\n"
从上面分析可以看出从源码到Token
的过程和上面PHP
的过程非常相似,Token
的类型无非是变量名、字面量、操作符、分隔符以及关键字。
2. 语法分析
上面词法分析得到了一堆
Token
,语法分析会把Token
转换为抽象语法树即AST
,在这一过程中发现任何语法错误都会终止编译过程并输出报错信息
- 上面提到 PHP 的语法分析是自顶向下的,而Go语言的语法分析使用自底向上的方式。先构造子树,然后再组装成一颗完整的树,这两种不同的分析方法其实也代表了计算机科学中两种不同的思想 — 从抽象到具体和从具体到抽象。
- 每一个
AST
的树节点都会与一个Token
实际位置相对应
3. 语义分析
在完成语法分析后,Go 会遍历抽象语法树中的节点进行类型检查 还有类型推断,查看类型是否匹配,是否进行隐式转化(go 没有隐式转化)。在这个过程中也可能会对抽象语法树进行改写,这不仅能够去除一些不会被执行的代码、对代码进行优化以提高执行效率,而且也会修改 make、new 等关键字对应节点的操作类型。
- Go 属于强类型的编程语言,在编译期间会有更严格的类型限制,也就是编译器会在编译期间发现变量赋值、返回值和函数调用时的类型错误;
4. 中间码生成
无奖问答2 这里有个疑问,为什么不直接将上面的流程获得的
AST
翻译成二进制文件?
这里再回到本章的开头寻找答案,因为有各种各样的操作系统,有不同的 CPU 类型,每一种的位数可能不同;寄存器能够使用的指令也不同,像是复杂指令集与精简指令集等;
- 中间代码的生成过程是从 AST 抽象语法树到 SSA 中间代码的转换过程,在这期间会对语法树中的关键字再进行改写,这种形式的中间码,最重要的一个特性就是最在使用变量之前总是定义变量,并且每个变量只分配一次。
5. 代码优化
Go 的编译文档并没有单独提到代码优化,实际代码优化融合在编译过程中的每一个阶段,
6. 机器码生成
和
mysql
的索引优化中降维一样,机器码的生成过程其实也是对 SSA 中间代码进行降级的过程
- 针对目标 CPU 架构,降级的过程处理了所有机器特定的重写规则并对代码进行了一定程度的优化
cmd/internal/obj
作为汇编器会将这些指令转换成机器码完成这次编译
🍀 小小感慨
当下处于一个 AI 时代,越来越多的芯片厂商诞生,往后掌握编译器后端的优化估计会成为一个有力的优势
(二)、垃圾回收及内存分配
-
STW :(stop the world)是垃圾回收过程中暂停其他程序执行的时刻。虽然 Go 尽量减少这种暂停时间,但在标记阶段的开始和结束时仍需要暂停整个程序。
可以简单理解为,当 Go 的 GC 需要开始标记对象时,它会暂时“冻结”其他正在运行的 goroutine,等标记完成后再继续执行。 -
写屏障:用于确保在垃圾回收期间对内存的修改不会影响到 GC 的工作。在标记阶段,Go 会开启写屏障来跟踪对象的引用变化,保证标记阶段准确无误。
Golang 的垃圾回收器使用 并发三色标记-清除算法,这种设计尽量减少了 STW时间,以提高系统的响应能力。
1. GC过程内存管理详细分析
Go的垃圾回收器主要包含的五个阶段,我们看到虽然采用了并发三色标记和清除,但在一次GC周期内,还是要有2次STW,一次是结束标记,关闭写屏障,
另一次是为下一个周期的并发标记做准备,开启写屏障。
阶段 | 说明 | 赋值器状态 |
---|---|---|
清扫终止 | 为下一个阶段的并发标记做准备工作,启动写屏障 | STW |
标记 | 与赋值器并发执行,写屏障处于开启状态 | 并发 |
标记终止 | 保证一个周期内标记任务完成,停止写屏障 | STW |
内存清扫 | 将需要回收的内存归还到堆中,写屏障处于关闭状态 | 并发 |
内存归还 | 将过多的内存归还给操作系统,写屏障处于关闭状态 | 并发 |
- 标记与清除阶段的负荷
在标记与清除阶段,GC需要遍历堆内存中的所有对象,并进行标记和清除,这也是十分消耗cpu的工作。
- 标记辅助
GC的并发标记并非只是由特定(dedicated) goroutine去完成的,为了保证GC标记清扫的速度不低于业务goroutine分配内存的速度,保证程序不因消耗内存过快过大而被OS OOM(Out Of Memory) Killed,GC引入标记辅助技术,即让每个业务goroutine都有机会参与到GC标记工作中来!并且,这种标记辅助采用的是一种补偿机制,即该业务goroutine分配的内存越多,它要辅助标记的内存就越多。一旦某个业务goroutine被“拉壮丁”执行标记辅助工作,那么该goroutine的业务执行就会暂停,业务逻辑也就无法向前推进。
- 堆内存的释放
当Go GC回收了堆内存之后,如果堆的大小变得比之前小了,那么垃圾回收器会向操作系统归还多余的内存空间。在Linux等操作系统中,操作系统会将这些内存页标记为“未使用”,但是这些内存页并不会立即返回给操作系统,而是留给程序使用,以便程序将来再次申请内存时可以直接使用已经分配的内存页,从而减少内存分配的时间和开销。当程序没有使用这些内存页一段时间后,操作系统会将这些内存页回收,并将它们标记为“可用”,并在需要时重新分配给程序。这个过程是由操作系统的虚拟内存管理机制来完成的,具体的开销取决于操作系统的实现和硬件的性能等因素。
2. 相较其他语言
GC 机制
Go GC的自动内存管理减少了内存泄漏和悬挂指针等问题。然而,GC给开发者带来便利的同时,开销也是不可避免的,和别的语言相较,特性的差异有着明显地区分。如和霸主C++相比,能够在内存分配、指针操作和底层硬件控制上实现精细优化。相比之下,Go的垃圾回收机制虽然简化了内存管理,但却容易在高频内存操作中带来延迟(内存分配和垃圾回收)。
-
同 php 相较,Go 的垃圾回收器可以与程序并发运行,大大降低了 STW 时间。相比之下,PHP 的垃圾回收更简单,但其内存管理是基于引用计数的,这种方式虽然直观,但在处理复杂数据结构时可能会带来循环引用问题,需要额外的机制来打破循环。而 Go 的并发标记-清除算法更具弹性,尤其是在高并发应用场景中,能更好地管理内存。
-
尽管 Go 提供了自动垃圾回收,减少了开发者手动管理内存的复杂性,但与 C++ 相比,Go 的 GC 仍然存在一定的性能开销。在高性能系统或 AAA 游戏开发中,C++ 允许开发者手动控制内存分配和释放,避免了垃圾回收带来的性能损耗。而 Go 的垃圾回收在高频内存分配和回收时,可能会产生明显的停顿或延迟。
内存管理
Go 尽量在降低延迟的同时提高内存管理的效率。但与 C++ 的手动内存管理相比,Go 的 GC 仍然在极端性能场景下表现较为逊色。因此,在内存敏感和高性能计算领域,C++ 依然是更好的选择,而 Go 则更适合开发现代分布式应用和并发服务。
-
PHP 则更适合快速开发和短周期的 Web 应用。PHP 的 GC 使用引用计数机制管理内存,主要用于 Web 请求生命周期的内存释放,而 Go 的 GC 适用于更复杂的并发场景。Go 不需要开发者手动管理内存分配和释放,减少了内存泄漏的风险。而 Go 的 GC 机制适用于长期运行的后台服务,特别是在微服务架构中表现良好。
-
Go 的内存管理更加便捷,开发者无需担心内存泄漏或悬空指针等问题。但对于追求极致性能的场景,如高性能计算、游戏开发等,C++ 提供了更大的灵活性和性能优化空间。