Golang编译优化秘籍:公共子表达式消除详解‌

概述

在高性能程序设计中,编译器优化扮演着至关重要的角色。其中,“公共子表达式消除”(Common Subexpression Elimination,简称 CSE)是静态单赋值(SSA)或三地址码优化阶段常见的一种优化技术。通过识别程序中重复计算的表达式并复用其结果,可以显著降低冗余计算次数、减少运行时开销,从而提升程序性能。本文将以 Go 语言(Golang)为切入,深入剖析编译器如何识别并消除公共子表达式,并通过代码示例、图解与详细说明帮助读者更直观地理解这一优化过程。


一、什么是“公共子表达式”?为什么要消除?

  • 公共子表达式(Common Subexpression):在同一作用域或基本块中,指两个或多个位置出现了相同的、无副作用且操作数一致的表达式。例如:

    a := x*y + z
    b := x*y - w

    其中 x*y 就是一个公共子表达式,它在两处被重复计算。

  • 为什么要消除?

    1. 减少冗余计算
      如果 x*y 的计算开销较大,那么重复执行会浪费 CPU 周期。
    2. 降低能耗
      在高并发或资源受限的设备上,减少不必要计算可降低能耗。
    3. 提升性能
      将公共子表达式提取到临时变量后可以显著减少计算次数,特别是在循环或热点路径中,效果尤为明显。

二、Go 编译器中的 CSE 实现概况

Go 语言的编译器(gc)在内部会将源代码转换成 SSA(Static Single Assignment)中间表示,并在 SSA 阶段进行一系列优化,其中就包含 CSE。以下是编译器处理流程的简要概括:

  1. 前端解析 & 类型检查
    将源码解析为 AST(抽象语法树),并进行类型检查。
  2. 生成三地址码 / SSA 形式
    把 AST 转换成 IR(中间表示),生成 SSA 节点,每个变量只赋值一次。
  3. SSA 阶段优化
    包括:死代码删除、常量传播、拷贝传播、公共子表达式消除、循环不变代码外提(LICM)等。
  4. 生成汇编或机器码
    优化后的 SSA 最终被转换为底层指令,输出为 .s 汇编或可执行文件。

在 SSA 阶段,编译器需要扫描同一基本块(Basic Block)或多块可达的路径,识别表达式结构并判断其操作数是否相同且未被修改,以判断该表达式是否是公共的。若条件满足,则将其替换为先前计算过的临时变量。


三、示例演示:简单 CSE 优化

1. 代码示例

package main

import (
    "fmt"
)

func compute(a, b, c int) int {
    // 假设 x 和 y 都是外部传入变量,以下表达式有公共子表达式 a*b
    x := a*b + c
    y := a*b - c
    return x + y
}

func main() {
    res := compute(3, 4, 5) // 3*4 = 12
    fmt.Println(res)
}

在上面这段代码中:

  • a*b 出现在两处,分别是 x := a*b + cy := a*b - c
  • 在未经过 CSE 优化时,编译器会在两处都生成对 a*b 的计算;
  • 若启用 CSE,则可以将 a*b 先存入一个临时变量,然后复用该结果。

2. 编译器优化前后的对比

2.1 未优化(伪汇编示例)

假设我们手工将函数转为伪汇编(注意:下列汇编仅为示意,不代表真实 Go 汇编指令):

compute:
    MOVQ   a, RAX        # RAX = a
    IMULQ  b, RAX        # RAX = a * b
    MOVQ   RAX, R8       # 临时 R8 = a * b
    ADDQ   c, RAX        # RAX = (a * b) + c
    MOVQ   RAX, R9       # R9 = x

    MOVQ   a, RAX        # RAX = a
    IMULQ  b, RAX        # RAX = a * b  <-- 重复计算
    MOVQ   RAX, R10      # 临时 R10 = a * b
    SUBQ   c, RAX        # RAX = (a * b) - c
    MOVQ   RAX, R11      # R11 = y

    ADDQ   R9, R11       # R11 = x + y
    RET

可以看到,a * b 执行了两次 IMULQ 操作。

2.2 优化后(使用 CSE)

如果编译器识别到 a*b 是公共子表达式,则会先计算存放到临时变量,再在后续使用中复用:

compute (优化后):
    MOVQ   a, RAX        # RAX = a
    IMULQ  b, RAX        # RAX = a * b
    MOVQ   RAX, R8       # R8 = tmp = a * b

    MOVQ   R8, RAX       # RAX = tmp
    ADDQ   c, RAX        # RAX = tmp + c = x
    MOVQ   RAX, R9       # R9 = x

    MOVQ   R8, RAX       # RAX = tmp
    SUBQ   c, RAX        # RAX = tmp - c = y
    MOVQ   RAX, R11      # R11 = y

    ADDQ   R9, R11       # R11 = x + y
    RET

这样,“乘法”指令 IMULQ 仅执行一次,后续复用寄存器 R8 中的值。


四、图解:表达式树与基本块示意

下面用一个简单的 ASCII 图示 来说明“公共子表达式”在表达式树(Expression Tree)中的表现,以及在基本块内识别的思路。

1. 原始表达式的树状结构

对于 x := a*b + cy := a*b - c,分别得到的表达式树如下:

       (+)                    (-)
      /   \                  /   \
    (*)    c        和     (*)    c
   /   \                  /   \
  a     b                a     b

可以看到,两棵树中左侧子树 (*)(即 a * b)完全相同,这就是公共子表达式。CSE 要做的事情就是提取这部分子树。

2. 基本块(Basic Block)内的检测流程

假设把上述代码进一步拆分为 同一个基本块(没有跳转分支),伪代码如下:

t1 = a * b
x  = t1 + c
t2 = a * b   ←→ 检测到 “a * b” 与 t1 一致,可复用
y  = t2 - c
ret = x + y

编译器在 SSA 阶段,会维护一个“表达式到已计算临时变量”的映射(常称为“值表,Value Numbering”)。当看到第二次 a * b 时,就能够查到它已经对应了 t1,于是复用之得到 t2 = t1。真正后台的 SSA 伪码类似:

t1 = a * b
x  = t1 + c
t2 = t1       // 这里直接复用
y  = t2 - c

五、深入剖析 Go SSA 阶段 CSE 细节

Go 编译器(gc)的 SSA 优化主要发生在 src/cmd/compile/internal/ssa 包中。以下几点是理解 Go CSE 实现的关键:

  1. 值编号(Value Numbering)

    • Go 编译器为 SSA 中的每个操作分配一个“值编号”(value number)。相同操作(opcode 相同、操作数编号相同)的指令会被标记为等价。
    • 当发现编号相同的两个 SSA 指令时,就能判定它们是公共子表达式。
  2. 场景限制

    • 同一基本块内:最简单的场景,只需要在当前基本块内部检测。
    • 不同基本块间:Go SSA 也支持跨块 CSE,但仅在“没有中间写操作改变操作数”的情况下才可。也就是说,若在两个基本块之间有写操作(比如 ab 的赋值/别处调用可能修改了寄存器/内存),则不能跨块复用。
    • 内存访问表达式:针对 *pp[i] 等,编译器需额外检测中间是否有可能改变 p 或其底层对象。如果有潜在写操作,则不做 CSE。
  3. SSA 指令举例

    • SSA 中会产生类似 MUL a bADD t1 c 等操作。编译器内部为每个指令分配一个唯一标识符,维护一个哈希表(map)用于查找“等价”的 SSA 值。如果遇到等价值,就直接返回已存在的 Value,而不是生成新指令。
  4. 对 Go 语言特性的兼容

    • Go 中存在逃逸分析(escape analysis)、指针别名、**内存屏障(Write Barrier)**等特殊场景,可能会使得看似相同的表达式由于底层副作用而无法消除。
    • 例如,*p + *p 如果在两次读取之间有可能被其他 goroutine 修改,则不应消除。Go SSA 通过对“内存桶(memory bucket)”和“指针别名”信息的跟踪来判断安全性。

综上,Go 编译器在 SSA 阶段会尽量在安全的前提下识别公共子表达式,并复用已存在的 Value,从而减少指令生成。


六、示例:通过 go build -gcflags 观察 CSE 效果

Go 提供了 -gcflags="-m"-gcflags="-m -l -N" 等编译选项用于查看编译器优化报告。通过 -m 可以查看内联、逃逸分析等信息;通过更高等级的 -m -l -N 可以关闭内联和逃逸优化,方便对比。
下面示例演示如何用 -gcflags 查看 CSE 是否生效(不同 Go 版本行为可能略有差异,以 Go 1.20+ 为准)。

1. 准备示例文件 cse_demo.go

package main

import "fmt"

func compute(a, b, c int) int {
    x := a*b + c
    y := a*b - c
    return x + y
}

func main() {
    fmt.Println(compute(3, 4, 5))
}

2. 编译并查看优化报告

在命令行执行:

go version
# 假设输出:go version go1.21 linux/amd64

go build -gcflags="-m=2" cse_demo.go 2>&1 | grep "CSE"
  • 如果编译器进行了 CSE,报告中可能出现与“value numbering”或“CSE”相关的提示。例如在某些 Go 版本中会显示:

    ./cse_demo.go:6:6: value numbering: a * b reused
  • 或者你可以直接用 go build -gcflags="-m -l -N" cse_demo.go 关闭更多优化,比对关闭前后生成的汇编差异。

3. 对比生成的汇编

直接查看汇编代码(假设输出到 cse_demo.s):

go build -gcflags="-S" -o /dev/null cse_demo.go > cse_demo.s

打开 cse_demo.s,在 compute 函数中查找 IMULQ 指令出现次数:

  • 若只出现一次:表示 CSE 已成功将第二次 a*b 重用;
  • 若出现两次:则说明在该版本编译器下,可能由于某些安全或语义原因,没有执行跨表达式消除。

七、复杂示例:循环内的 CSE

在实际项目中,CSE 在循环体中的收益尤为明显。下面看一个更复杂的示例,展示循环中如何利用 CSE 避免多次重复计算。

1. 代码示例

package main

import (
    "fmt"
    "math"
)

func sumDistances(points []float64, scale float64) float64 {
    var total float64
    for i := 0; i < len(points); i++ {
        // 假设每次都需要计算 scale * points[i]^2
        // 如果不做优化,每次都会执行 pow 和 mul
        total += scale * math.Pow(points[i], 2)
    }
    return total
}

func main() {
    pts := []float64{1.0, 2.0, 3.0, 4.0}
    res := sumDistances(pts, 3.14)
    fmt.Println(res)
}
  • math.Pow(points[i], 2) 相对开销较大,如果 points[i] 被多次使用,应该先缓存其平方值。
  • 但是上述写法中,只有一次 math.Pow,实际循环仍会多次调用函数。CSE 在函数调用层面受到限制,一般只能在单次表达式中识别重复子树。要在循环内手动优化,可改写为:
for i := 0; i < len(points); i++ {
    v := points[i]
    sq := v * v          // 手动计算并缓存 v^2
    total += scale * sq
}

但是,编译器在一些场景下也能做“循环不变代码外提(LICM)”和“内联”(将 math.Pow 内联为乘法)配合使用,从而实现类似效果。具体效果依赖 Go 版本和内联策略。

2. 图解:循环体内的表达式流

┌──────────────────────────────────┐
│ for i := 0; i < N; i++ {        │
│    v  = points[i]               │
│    ps = v * v   ←—— 公共子表达式? │
│    total += scale * ps          │
│ }                                │
└──────────────────────────────────┘
  • 若直接写 scale * math.Pow(v, 2),SSA 阶段会先判断 math.Pow(v, 2) 是否可内联为 v*v(Go1.21+ 常见内联),然后在同一个迭代内只出现一次,CSE 价值不大。但如果在同一迭代体中多次出现 math.Pow(v,2),则可识别为公共子表达式。
  • 若整个循环体把 v 每次都重新赋值,CSE 只能在一次迭代内部做循环内消除,无法跨迭代复用(因为 points[i] 值不同)。跨迭代的冗余消除,需要更深层次的分析和缓存策略。

八、手动与自动:何时需要依赖 CSE,何时手动优化?

虽然编译器已经能够自动做部分 CSE,但在实际性能调优中,还是需要注意以下几点:

  1. 了解编译器优化能力与限制

    • Go 编译器在 SSA 阶段只能识别“纯计算”表达式,且操作数需在消除范围内不发生变化。
    • 对于函数调用、接口类型或可能引发“逃逸”的表达式,一般不会被自动消除。
  2. 手动提取显式公共子表达式

    • 当发现循环内或热点路径里多次使用相同复杂表达式(尤其是函数调用、interface 类型的运算)时,最好手动先计算并缓存到局部变量,再复用。
  3. 借助编译器报告验证

    • 通过 go build -gcflags="-m"-m=2-gcflags="-S" 等参数检查编译器是否做了预期优化。
    • 如果看到编译报告给出了 “value numbering” 或 “CSE” 提示,说明编译器帮你做了优化;若没有,需要考虑手动重构代码。
  4. 保持代码可读性与维护性

    • 手动做过度拆分有时会让代码可读性下降,需要在性能与可读性之间取舍。
    • 建议先写出直观易懂的代码,再通过分析器报告结合基准测试,确定是否真的需要额外优化。

九、完整示例:从源码到汇编,看 CSE 优化全流程

下面给出一段更完整的示例代码,然后展示如何一步步观察编译器如何处理公共子表达式。

1. 完整示例 cse_full.go

package main

import (
    "fmt"
)

// MultiplyAndAdd 展示了一个稍微复杂一点的场景
func MultiplyAndAdd(a, b, d, e int) int {
    // 第一处:a*b + d
    r1 := a*b + d
    // 第二处:a*b - e
    r2 := a*b - e

    // 第三处:(a*b + d) * (a*b - e)
    // 这里又重复出现两次 a*b + d 和 a*b - e
    r3 := (a*b + d) * (a*b - e)

    return r1 + r2 + r3
}

func main() {
    fmt.Println(MultiplyAndAdd(2, 3, 5, 1))
}

该函数中出现了三处与 a*b 相关的表达式:

  1. r1 := a*b + d
  2. r2 := a*b - e
  3. r3 := (a*b + d) * (a*b - e)

如果没有优化,a*b 会在每次出现时都进行一次乘法运算;优化后,应该只计算一次 a*b,并且把 a*b + da*b - e 也做复用。

2. 查看编译器优化报告

在终端运行:

go build -gcflags="-m=2" cse_full.go 2>&1 | grep "value numbering"

(不同 Go 版本输出可能略有差异,下文以可能出现的日志为示例)

假设输出包含:

cse_full.go:7:9: value numbering: a * b reused
cse_full.go:9:17: value numbering: a * b reused
cse_full.go:11:17: value numbering: a * b + d reused
cse_full.go:12:13: value numbering: a * b - e reused
  • 第 7 行:在 r2 := a*b - e 时,发现 a*b 已在第 6 行的 r1 中计算过,因此直接复用;
  • 第 11 行:在 r3 := (a*b + d) * (a*b - e) 中,发现 a*b + da*b - e 都是之前已计算过的表达式,也进行复用。

3. 汇编对比(简化示意)

3.1 未优化(假设情况,仅示意)

MultiplyAndAdd:
    MOVQ   a, RAX
    IMULQ  b, RAX        # RAX = a * b
    MOVQ   RAX, R8       # tmp1 = a * b
    ADDQ   d, RAX        # RAX = (a*b) + d
    MOVQ   RAX, R9       # r1

    MOVQ   a, RAX
    IMULQ  b, RAX        # RAX = a * b  <-- 重复
    MOVQ   RAX, R10      # tmp2 = a * b
    SUBQ   e, RAX        # RAX = (a*b) - e
    MOVQ   RAX, R11      # r2

    MOVQ   a, RAX
    IMULQ  b, RAX        # RAX = a * b  <-- 又一次重复
    MOVQ   RAX, R12      # tmp3 = a * b
    ADDQ   d, RAX        # RAX = (a*b) + d  <-- 重新计算
    MOVQ   RAX, R13      # tmp4

    MOVQ   a, RAX
    IMULQ  b, RAX        # RAX = a * b  <-- 再次重复
    MOVQ   RAX, R14      # tmp5 = a * b
    SUBQ   e, RAX        # RAX = (a*b) - e  <-- 重复计算
    MOVQ   RAX, R15      # tmp6

    IMULQ  R13, R15      # r3 = (a*b+d) * (a*b-e)

    ADDQ   R9, R11       # r1 + r2
    ADDQ   R11, RAX      # (r1+r2) + r3
    RET

可以看到,最差情况里 a*b 共执行了 4 次,还对 a*b + da*b - e 也分别多次计算。

3.2 CSE 优化后(示意)

MultiplyAndAdd (优化后):
    MOVQ   a, RAX
    IMULQ  b, RAX        # RAX = a * b    <-- 只执行一次
    MOVQ   RAX, R8       # tmp_ab = a * b

    # 第一次 r1 = tmp_ab + d
    MOVQ   R8, RAX
    ADDQ   d, RAX
    MOVQ   RAX, R9       # r1

    # 第二次 r2 = tmp_ab - e
    MOVQ   R8, RAX
    SUBQ   e, RAX
    MOVQ   RAX, R11      # r2

    # 第三次 r3 复用 tmp_ab + d
    MOVQ   R8, RAX
    ADDQ   d, RAX
    MOVQ   RAX, R13      # tmp_ab_plus_d

    # 第四次 r3 复用 tmp_ab - e
    MOVQ   R8, RAX
    SUBQ   e, RAX
    MOVQ   RAX, R15      # tmp_ab_minus_e

    IMULQ  R13, R15      # r3 = tmp_ab_plus_d * tmp_ab_minus_e

    ADDQ   R9, R11       # tmp = r1 + r2
    ADDQ   R11, RAX      # tmp + r3
    RET

在此版本里,IMULQ b, RAX 仅执行了一次,tmp_ab + dtmp_ab - e 也各自只在计算时执行了一次。这样,原本可能出现的四次乘法减少到一次,减轻了 CPU 负担。


十、图解:表达式合并后的基本块流程

下面用 ASCII 图示说明优化后,SSA/汇编中指令流程的“流水线”式复用关系:

┌──────────────────────────────────────────────┐
│ t0 = a * b           # 只计算一次               │
│                                    ▲         │
│ ┌────┐                           ┌─┴─┐       │
│ │ t0 │──────────────────────────▶│ RAX│       │
│ └────┘                           └─┬─┘       │
│   │                                │         │
│   │ t1 = t0 + d      r1            │         │
│   ├──────────────▶ (ADD d)         │         │
│   │                                │         │
│   │ t2 = t0 - e      r2             │         │
│   ├──────────────▶ (SUB e)         │         │
│   │                                │         │
│   │ t3 = t1 * t2     r3            │         │
│   └──────────────▶ (IMUL t1, t2)    │         │
│                                    │         │
│ result = r1 + r2 + r3              │         │
└────────────────────────────────────┴─────────┘
  • 第一步:计算 t0 = a * b,并存入寄存器 RAX(或 SSA 中的某个值)。
  • 第二步:直接复用 t0 生成 t1 = t0 + d(即 r1)。
  • 第三步:再次复用 t0 生成 t2 = t0 - e(即 r2)。
  • 第四步:复用 t1t2 生成 t3 = t1 * t2(即 r3)。
  • 最后:将 r1 + r2 + r3 合并得最终结果。

十一、深入理解:CSE 在 Go 编译器中的安全性判断

在一些特定场景下,编译器可能放弃做 CSE。主要原因包括:

  1. 指针别名(Pointer Aliasing)

    • 如果表达式中涉及内存加载(例如 x := *p + *p),编译器需要确定两次加载是否访问同一内存。如果中间有写操作或不确定是否修改,无法消除。
    • Go SSA 通过“内存桶”(memory bucket)跟踪可能的别名,若存在潜在冲突,就回退不做 CSE。
  2. 函数调用与副作用

    • 如果表达式中嵌套了可能有副作用的函数调用,比如 foo(a) + foo(a),除非编译器能确定 foo 是纯函数(sanitize 过)并且无副作用,否则不会做消除。
    • 对于 math.Pow,在部分版本的 Go 编译器中属于内联或内置函数,可视为无副作用;但在老版本中可能不能内联,就不会自动消除。
  3. 并发安全性(Concurrency)

    • 若表达式依赖某个全局变量或共享状态,而在两次计算之间可能被其他 goroutine 修改,也必须放弃 CSE。
    • Go SSA 会根据逃逸分析、内存屏障信息判断是否安全。
  4. 整型溢出 & 内置检查

    • 在 Go 1.14+,整数运算会插入溢出检测(bounds check)。当例如 a*b 存在可能溢出时,编译器可能拆分为溢出检测指令加乘法指令。若两处 a*b 需要不同的溢出处理场景,也无法简单复用。

正是由于上述诸多安全性考量,编译器在 CSE 实现时,不仅做“值相同”的简单判断,还需要结合 SSA 中的“内存桶编号”(表示可能修改该内存的所有操作的编号)、“指令标记”(纯计算或有副作用)等元信息,才能决定是否进行消除。


十二、手把手:如何在本地复现 CSE 检测

下面是一个小教程,帮助你在本地操作,看看 Go 编译器的 CSE 具体情况。

步骤 1:写好示例文件

cat > cse_test.go << 'EOF'
package main

import "fmt"

// 简化示例:重复计算 a*b
func f(a, b, d, e int) int {
    x := a*b + d
    y := a*b - e
    return x + y
}

func main() {
    fmt.Println(f(10, 20, 5, 3))
}
EOF

步骤 2:用 -m=2 查看 SSA 报告

go build -gcflags="-m=2" cse_test.go 2>&1 | grep "value numbering"

如果输出类似:

cse_test.go:6:13: value numbering: a * b reused

则说明第二处 a*b 被成功复用。

步骤 3:生成汇编并对比

  • 不带优化(关闭内联、禁止额外优化):

    go build -gcflags="-N -l" -o /dev/null -gcflags="-S" cse_test.go > asm_noopt.s
  • 带默认优化

    go build -gcflags="-S" -o /dev/null cse_test.go > asm_opt.s

打开两个 .s 文件,搜索 IMULQ(假设 x86\_64 平台)。

  • asm_noopt.s 中,你会看到两次 IMULQ
  • asm_opt.s 中,你应该只看到一次 IMULQ,其余使用寄存器复用。

十三、小结与最佳实践

  1. 理解 CSE 概念

    • “公共子表达式消除”是编译器静态优化的重要技术,通过给相同表达式分配“值编号”(Value Numbering),实现重复计算的复用。
  2. Go SSA 优化流程

    • Go 编译器将源代码转为 SSA 形式,在 SSA 阶段做包括 CSE 在内的多种优化。只要表达式纯粹(无副作用)且操作数没被干扰,就可以消除。
  3. 手动 vs. 自动

    • 大多数简单“算术表达式”会被自动消除。但当表达式较复杂(涉及函数调用、内存读写、接口类型等)时,编译器可能不会或无法安全地做自动 CSE。遇到性能瓶颈时,需要手动提取公共子表达式。
  4. 如何验证

    • 使用 go build -gcflags="-m=2" 检查编译器的 SSA 报告,看是否出现 “value numbering: … reused” 提示;
    • 使用 go build -gcflags="-S" 生成汇编,观察 IMULADDMOV 等关键指令的数量变化。
  5. 代码可读性与性能折中

    • CSE 优化有时会让代码引入更多中间变量。保持代码可读性和易维护性与性能优化之间要达到平衡。
    • 先写出清晰的逻辑,再通过基准测试(go test -bench)与编译报告,判断是否需要进一步“手动 CSE”或其他更高级优化。

通过本文的代码示例、汇编对比与 ASCII 图解,相信你对 Golang 编译器如何识别并消除公共子表达式有了较为全面的了解。在实际开发中,既要善用编译器自动优化,也要学会在关键热路径手动进行优化,使程序在性能和可读性之间取得最佳平衡。若需进一步研究,可以深入阅读 Go 源码中 src/cmd/compile/internal/ssa 目录下有关值编号(value numbering)与内存桶(memory buckets)的实现。

最后修改于:2025年06月05日 10:45

评论已关闭

推荐阅读

DDPG 模型解析,附Pytorch完整代码
2024年11月24日
DQN 模型解析,附Pytorch完整代码
2024年11月24日
AIGC实战——Transformer模型
2024年12月01日
Socket TCP 和 UDP 编程基础(Python)
2024年11月30日
python , tcp , udp
如何使用 ChatGPT 进行学术润色?你需要这些指令
2024年12月01日
AI
最新 Python 调用 OpenAi 详细教程实现问答、图像合成、图像理解、语音合成、语音识别(详细教程)
2024年11月24日
ChatGPT 和 DALL·E 2 配合生成故事绘本
2024年12月01日
omegaconf,一个超强的 Python 库!
2024年11月24日
【视觉AIGC识别】误差特征、人脸伪造检测、其他类型假图检测
2024年12月01日
[超级详细]如何在深度学习训练模型过程中使用 GPU 加速
2024年11月29日
Python 物理引擎pymunk最完整教程
2024年11月27日
MediaPipe 人体姿态与手指关键点检测教程
2024年11月27日
深入了解 Taipy:Python 打造 Web 应用的全面教程
2024年11月26日
基于Transformer的时间序列预测模型
2024年11月25日
Python在金融大数据分析中的AI应用(股价分析、量化交易)实战
2024年11月25日
AIGC Gradio系列学习教程之Components
2024年12月01日
Python3 `asyncio` — 异步 I/O,事件循环和并发工具
2024年11月30日
llama-factory SFT系列教程:大模型在自定义数据集 LoRA 训练与部署
2024年12月01日
Python 多线程和多进程用法
2024年11月24日
Python socket详解,全网最全教程
2024年11月27日