Sora

Sora是什么

Sora是OpenAI发布的一款工具,它结合了语言生成技术Transformer和图像生成技术Diffusion的特点,它能够理解文本并生成视频。

Sora能解决什么问题

  • 将文本描述转换为生动的视频,无需任何动画或视频编辑经验。

Go中的切片

切片与数组的区别

首先需要讨论,什么是数组。

Go语言中的数组与C语言中的数组相似,本质上是一段连续的内存,固定大小,不能够扩展。数组的容量(cap)和长度(len)是固定的,都是数组的大小。

数组是值类型,将一个数组复制给另一个数组时,传递的是一个拷贝。而切片是引用类型。

1
2
3
4
5
6
7
8
9
func main () {
array := []int{0, 1, 2, 3, 4, 5}
slice := array[3:]
fmt.Printf("&array = %p\n", &array)
fmt.Printf("&slice = %p\n", &slice)
}
// output
// &array = 0x1400000c030
// &slice = 0x1400000c048

而切片是数组其中一段内存的引用。其包装的数组称为该切片的底层数组。修改底层数组,同样会造成切片的内容被修改。

1
2
3
4
5
6
7
8
9
10
func main () {
array := []int{0, 1, 2, 3, 4, 5}
slice := array[3:]
array[3] = -1
fmt.Println(array)
fmt.Println(slice)
}
// output
// [0 1 2 -1 4 5]
// [-1 4 5]

引用同一个底层数组的情况下,修改切片也会造成底层数组的变化

1
2
3
4
5
6
7
8
func main () {
array := []int{0, 1, 2, 3, 4, 5}
slice := array[3:]
slice[0] = -1
fmt.Println(array)
}
// output
// [0 1 2 -1 4 5]

超过切片的容量将会进行扩容。扩容会新申请内存(一般为当前大小的2倍),并把原来的数组拷贝进去。这意味着底层数组会进行切换。

1
2
3
4
5
6
7
8
9
10
11
func main () {
array := []int{0, 1, 2, 3, 4, 5}
slice := array[3:]
slice = append(slice, -1)
slice[0] = -1
fmt.Println(slice)
fmt.Println(array)
}
// output
// [-1 4 5 -1]
// [0 1 2 3 4 5]

初始化的方法

初始化数组的方法,

1
2
array := [10]int
var array [10]int

不能使用make方式.

初始化切片的方法,

1
2
3
4
var slice []int // 初始只能通过append方式添加值
slice = array[1:5] // 可以通过索引和append方式设置值
slice := make([]int, 0, 10) // 初始只能通过append方式添加值
slice := make([]int, 10, 10) // 可以通过索引和append方式设置值

这里有个坑,通过make方式声明切片时,如果设置长度,那么初始下的底层数组是该长度的零值数组。

1
2
3
4
5
6
func main () {
slice := make([]int, 10 ,10)
fmt.Println(slice)
}
// output
// [0 0 0 0 0 0 0 0 0 0]

切片如何清空

重新申请内存

1
slice = []int{}
1
2
3
4
5
6
7
8
9
10
11
12
13
func main () {
func main () {
slice := make([]int, 10 ,10)
slice[0] = 0
fmt.Printf("slice cap = %d, len = %d, address = %p\n", cap(slice), len(slice), &slice)
slice = []int{}
slice = append(slice, 1)
fmt.Printf("new slice cap = %d, len = %d, address = %p\n", cap(slice), len(slice), &slice)
}
}
// output
// slice cap = 10, len = 10, address = 0x14000114018
// new slice cap = 1, len = 1, address = 0x14000114018

可以看到容量和长度均被重置。

赋值nil

1
slice = nil
1
2
3
4
5
6
7
8
9
10
11
func main () {
slice := make([]int, 10 ,10)
slice[0] = 0
fmt.Printf("slice cap = %d, len = %d, address = %p\n", cap(slice), len(slice), &slice)
slice = nil
slice = append(slice, 1)
fmt.Printf("new slice cap = %d, len = %d, address = %p\n", cap(slice), len(slice), &slice)
}
// output
// slice cap = 10, len = 10, address = 0x14000114018
// new slice cap = 1, len = 1, address = 0x14000114018

可以看到容量和长度均被重置。

重新切片

1
slice = slice[:0]
1
2
3
4
5
6
7
8
9
10
11
func main () {
slice := make([]int, 10 ,10)
slice[0] = 0
fmt.Printf("slice cap = %d, len = %d, address = %p\n", cap(slice), len(slice), &slice)
slice = slice[:0]
slice = append(slice, 1)
fmt.Printf("new slice cap = %d, len = %d, address = %p\n", cap(slice), len(slice), &slice)
}
// output
// slice cap = 10, len = 10, address = 0x1400011e018
// new slice cap = 10, len = 1, address = 0x1400011e018

可以看到容量没有被重置,但长度被重置。

这里可以想到,如果容量重置后,后续append会切换底层数组。因此会带来额外的消耗。

可以使用基准测试评估下性能,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import "testing"

func BenchmarkSliceEmpty(b *testing.B) {
array := []int{0, 1}
slice := array[1:]
for i := 0; i < b.N; i++ {
slice = []int{}
slice = append(slice, -1)
}
}

func BenchmarkSliceNil(b *testing.B) {
array := []int{0, 1}
slice := array[1:]
for i := 0; i < b.N; i++ {
slice = nil
slice = append(slice, -1)
}
}
func BenchmarkSliceReSlice(b *testing.B) {
array := []int{0, 1}
slice := array[1:]
for i := 0; i < b.N; i++ {
slice = slice[:0]
slice = append(slice, -1)
}
}

测试结果如下,

1
2
3
4
5
# go test --bench .
BenchmarkSliceEmpty-8 91432641 12.58 ns/op
BenchmarkSliceNil-8 90686667 12.61 ns/op
BenchmarkSliceReSlice-8 1000000000 0.3131 ns/op
PASS

能够明显看出,重新切片slice = slice[:0]是性能最好的方式。

Go外部修改切片会影响传递给协程内部的切片吗

先说结论,会影响。并且可以将这个结论推广到其他引用类型。

  • map
  • pointer
  • channel
  • interface
  • function

请看如下测试代码,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main

import (
"fmt"
"time"
)

func main () {
array := []int{0, 1}
slice := array[0:]
fmt.Println(slice)

ch := make(chan int)
go func (s []int) {
time.Sleep(1 * time.Second)
s[0] = -1
fmt.Println("in goroutine", slice)
ch <- 1
}(slice)

slice[1] = -2
fmt.Println("before groutine", slice)

<-ch
fmt.Println("after goroutine", slice)
}

这是由于切片时引用类型,修改时修改的同一块内存。

有朋友会问,那数组不是引用类型,下面这段代码,为什么和切片是同样的运行效果,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func main () {
array := []int{0, 1}
fmt.Println("init", array)

ch := make(chan int)
go func (s []int) {
time.Sleep(1 * time.Second)
s[0] = -1
fmt.Println("in goroutine", array)
ch <- 1
}(array)

array[1] = -2
fmt.Println("before groutine", array)

<-ch
fmt.Println("after goroutine", array)
}

//output
// init [0 1]
// before groutine [0 -2]
// in goroutine [-1 -2]
// after goroutine [-1 -2]

这是由于数组传递进函数时,传递的其实是它的切片(注意到函数的定义了吗func (s []int))

页面置换算法

虚拟内存技术

  • 在程序执行中,当cpu所需的信息不在内存中时,操作系统负责将所需要的信息从外存调入内存。
  • 如果调入内存的空间不够,由操作系统将内存中暂时用不到的信息换出到外存中。

问题::哪些页面需要从内存中换出来,哪些有需要被调入呢?

页面置换算法

常见的有如下几种,

最佳(Optimal, OPT)页面置换算法

最佳页面置换算法选择被淘汰页面将是以后永不使用的,或者在最长时间内不被访问的页面,这样可以保证最低的缺页率。

但,关键在于,如何预知未来,因此这是个理想情况的算法,不可能实现。

先进先出(FIFO)页面置换算法

顾名思义,先进先出算法优先淘汰最先调入的页面,也是在内存中驻留最久的页面。使用队列实现。

最近最久未使用(Least Recently Used, LRU)页面置换算法

选择最近最长时间未被访问过的页面淘汰,它认为过去一段时间未访问过页面,在最近的将来也不会访问。

使用寄存器实现

  • 为每一个页面分配一个移位寄存器
  • 某个页面被访问时,将对应寄存器置为1
  • 此后,定时器定时将寄存器右移1位,随着时间推移,页面的寄存器值会越来越小
  • 需要页面置换时,将寄存器值最小的置换出去

使用栈实现

每当某个页面被访问时,将该页面号从栈中移除,并置于栈顶。因此栈顶总是最新访问的,栈底是最久未被访问的。当需要置换时,将栈底页面置换出去。

最少使用(Least Recently Used,LFU)页面置换算法

LFU的思想就是未每一个页面设置一个计数器,需要置换时,选择计数最小的那个页面。

时钟(Clock)页面置换算法

简单的Clock算法

首先,内存中的页面具有几个属性,

  • 状态位,是否调入内存
  • 访问位,是否被访问过,如果是,则值为1
  • 修改为,是否被更新过

简单的Clock算法将页面链成一个循环队列,需要置换时,

  • 遍历这个队列,如果遇到访问位为0的就置换出去
  • 如果没有找到,则指针会循环一周将所有使用位都置为0,并停在初始页,将其置换出去。

改进的Clock算法

改进的clock算法除了访问位外,还使用修改位,每一种页面都具有以下四种状态,

访问位 修改位 description
0 0 最佳替换页面
0 1 第二考虑的页面
1 0 该页面可能再次被访问
1 1 该页面很有可能被再次访问

改进的算法步骤如下,

  • 1)从指针位置开始扫描循环队列,在这次扫描中不做任何修改,遇到00类页面,直接置换
  • 2)如果1)失败,则重新扫描,遇到第一个01类页面则置换,这次扫描中,对每个跳过的页面,将其访问位置于0
  • 3)如果2)失败,指针将回到初始位置,并且所有页面的访问位都被置于0了。重复回到步骤1)执行

Golang中的垃圾回收

GC

GC全称为garbage collection,常见的方法有,

  • 引用计数(Python使用了这种方式)
  • 标记-清除
  • 标记-复制
  • 标记-整理

三色标记法

Go使用的三色标记法来做垃圾回收。前面提到的标记算法都有一个问题,运行时会造成STW。三色标记法对“标记”阶段的改进,在不暂停程序的情况下即可完成对象的可达性分析。GC将对象分为3类:

  • 白色:未搜索的对象
  • 灰色:正在搜索的对象
  • 黑色:已经搜索的对象

三色标记法属于增量式GC,回收器首先将所有对象都标记为白色,然后从GC root开始,逐步把所有可达对象变成灰色,再到黑色。最终剩余的白色对象即为不可达对象。

具体实现如下:

  • 初始状态下所有对象都为白色
  • 从root对象开始,扫描所有可达对象标记为灰色,放入待处理队列
  • 从队列中取出一个灰色对象,标记为黑色,将其引用标记为灰色,放入待处理队列
  • 重复步骤,直到队列空

读写屏障技术

三色标记法中gc和用户程序同时运行,其实是存在并发性问题的,为了解决这个问题,go使用了读写屏障技术。

编译器会在编译期间生成一段代码,该代码在运行期间会拦截用户的读取,创建,更新对象指针的操作,相当于一个hook调用,根据hook时机不同,分为不同的屏障技术。由于读屏障Read barrier技术需要在读操作中插入代码片段从而影响用户程序性能,所以一般使用写屏障技术来保证三色标记的稳健性。

Golang中的协程

Golang是云原生时代最令人兴奋的开发语言。其自身简洁的设计,优秀的性能,都让人爱不释手。本文主要记录下学习goroutine时的一些笔记。

什么是协程

首先需要提一下,我们都知道的操作系统中的概念,进程和线程。

进程是操作系统资源分配的最小单位,它包含一些文件描述符,内存等等。线程则是cpu调度的最小单位,它只能运行在进程中,且进程中可以拥有多个线程,线程本身需要使用进程中的资源。进程和线程都受操作系统内核来调度,称为内核态。

而协程,是有编程语言实现的程序结构。受语言框架的调度,或者由开发这自己调度。是一种更轻量级的结构。

对于cpu来说执行的程序结构是什么并不重要,只要有上下文,栈等信息就可以进行运算,因此协程也是一致类似的程序结构。go原生使用了协程实现,协程运行在进程中。这里提下Python中的协程是受eventloop管理,或开发者管理,运行在线程内的。

为什么有了进程线程,还需要协程

cpu执行程序,并不是总是顺序执行一个,而是一段时间内执行一个,时间到了就执行另一个,这个涉及到cpu的调度。一旦需要切换,那么不可避免的要保存程序运行时的一些信息。进程,线程的设计会在切换时耗费一定的时间,对于高并发的场景下,频繁的切换会耗时更久,因此,开发人员开发了自己的程序结构-协程,自己调度,优化了这个问题,带来了性能上的提升。

协程的优点,

  • 启动代价很小,可以以很小的栈空间启动
  • 切换成本小,工作在用户态
  • 与内核线程多对多的关系

Goroutine如何进行调度

通过前文已经知道,goroutine主要是受go runtime自身调度的,开发者也可以自行进行调度。

go语言在设计了GMP模型实现调度,

G,goroutine runtime.q 协程实体,代表用户执行流
P, processor runtime.p 逻辑处理器(并不是实际存在),管理着一组goroutine队列。存储这goroutine的上下文等信息,表示执行所需要的资源
M, machine runtime.m 对应内核线程(KSE),表示执行者,底层线程

P的个数可以通过runtime.GOMAXPROCS设定,但不是越多越好。默认值为物理线程数。

如何调度

  • 新启动的协程G会被加入到local processor的队列runQ中,等待调度
  • M想要运行任务就需要与P进行关联
  • P会尝试从本地runQ中取出一个G执行,如果本地没有G,则从Global队列中取,如果Global中没有任务,则回去其他P上的队列中去偷,通常偷一半。
  • 获取到G的P会在M上执行,如果当前的M发生阻塞,则P会切换到其他M上执行,
  • 当M执行完毕会尝试获取一个P来继续执行,通过会从其他os线程偷一个,如果没有偷取到,则将Goroutine放进global队列中。

协程的状态

通过调度说明,我们可以看到,要想实现这样的流程,协程自身必定会有一些状态,

Status value Descrition
Gidle 0 刚刚被分配,还没有初始化
Grunable 1 表示在runqueue上,还没有被运行
Grunning 2 协程可能在执行go代码,不在runqueue上,只与M绑定
Gsyscall 3 在执行系统调用,不在runqueue上,只与M绑定
Gwaiting 4 协程被阻塞(IO,chan,锁),不在runqueue中
Gdead 6 协程没有在执行,也许执行完,或者在free list,也可能正在在初始化,不能确定是否有stack
Gcopystack 8 栈正在复制,没有执行代码,不在runqueue上
Gscan 0x1000 与其他状态结合,表示GC正在扫描这个栈

协程只是一个执行流,而不是运行实体(M才是)。

GM模型

最早go语言使用的是GM模型,这会存在一些问题,

  • 全局队列,M将从全局中获取G进行执行,带来了并发问题,这需要线程级别的锁
  • G传递问题,即切换M带来的复制开销
  • 更大的内存消耗

CAP理论的一些碎碎念

CAP理论

在分布式领域中存在的一个基础理论-CAP,指出了在分布式系统中不可能同时满足3个条件,即,

  • Consistency - 一致性
  • Availability - 可用性
  • Partition tolerance - 分区容忍性

一致性的等级

有弱到强的一致性实现分别有,

  • 最终一致性(大部分的web应用都实现了最终一致性)
  • 单调一致性
  • 因果一致性
  • 顺序一致性
  • 线性一致性
  • 严格一致性

其中严格一致性是理想情况,是不可能实现的。能够实现的最高一致性就是线性一致性,通常单节点的系统即可实现。

可用性的时长

一致性会影响可用性,强一致性带来的就是可用性时长的增加。这个很好理解,要保证数据保持一致,就需要额外做很多检查。

CP与AP的抉择

任何一个分布式系统都要在其中做出取舍,由于现实中存储之间的网络,或是其他物理条件,总是会存在分区的情况,因此CAP的取舍也退化称为CA的取舍。

但CP和AP的选择不是一个绝对的选择题,而是一种折衷,一种偏好。
这就像一个区间的两端,你选择离一致性近一些,那么就会离可用性远一些。

Postgres学习笔记

什么是postgres

广泛使用的对象关系型数据库(ORDBMS)。

ORDBMS和RDBMS的区别

RDBMS代表关系型数据库系统,ORDBMS为RDBMS所有功能提供了面向对象概念的额外支持。该数据库支持类,对象和继承的概念。

详细的区别请看,

Installation

通过docker安装

1
docker run --name some-postgres -p 5432:5432 -e POSTGRES_PASSWORD=your_secrect_password -d postgres

默认用户名为postgres,登陆时输入,

1
docker exec -it some-postgres psql postgres://postgres:your_secrect_password@your_host_ip:5432

使用

语法

Database

创建数据库
  1. 可以使用createdb命令,

    1
    $ create mydb
  2. 可以使用sql,不要忘了加;

    1
    2
    # create database mydb;
    CREATE DATABASE
选择数据库

使用\l命令查看数据库有哪些。

1
2
3
4
5
6
7
8
9
10
11
postgres=# \l
List of databases
Name | Owner | Encoding | Collate | Ctype | Access privileges
-----------+----------+----------+------------+------------+-----------------------
mydb | postgres | UTF8 | en_US.utf8 | en_US.utf8 |
postgres | postgres | UTF8 | en_US.utf8 | en_US.utf8 |
template0 | postgres | UTF8 | en_US.utf8 | en_US.utf8 | =c/postgres +
| | | | | postgres=CTc/postgres
template1 | postgres | UTF8 | en_US.utf8 | en_US.utf8 | =c/postgres +
| | | | | postgres=CTc/postgres
(4 rows)

dropdb

删除数据库,可以使用dropdb命令

Table

创建表
1
2
3
4
5
6
7
CREATE TABLE COMPANY (
ID INT PRIMARY KEY NOT NULL,
NAME TEXT NOT NULL,
AGE INT NOT NULL,
ADDRESS CHAR(50),
SALARY REAL
);

注意最后一行没有逗号。

再创建一张表,

1
2
3
4
5
CREATE TABLE DEPARTMENT(
ID INT PRIMARY KEY NOT NULL,
DEPT CHAR(50) NOT NULL,
EMP_ID INT NOT NULL
);
查看表

输入\d查看所创建的所有表

1
2
3
4
5
6
7
mydb-# \d
List of relations
Schema | Name | Type | Owner
--------+------------+-------+----------
public | company | table | postgres
public | department | table | postgres
(2 rows)

输入\d table查看具体表的信息

1
2
3
4
5
6
7
8
9
10
11
mydb-# \d company
Table "public.company"
Column | Type | Collation | Nullable | Default
---------+---------------+-----------+----------+---------
id | integer | | not null |
name | text | | not null |
age | integer | | not null |
address | character(50) | | |
salary | real | | |
Indexes:
"company_pkey" PRIMARY KEY, btree (id)
删除表

使用DROP TABLE table_name;命令。

Schema

创建schema
1
2
mydb=# create schema myschema;
CREATE SCHEMA

在schema中创建表
mydb=# ```bash
create table mychema.company (
id int not null,
name varchar(20) not null,
age int not null,
address char(25),
salary decimal(18,2),
primary key (id)
);
CREATE TABLE

通过\d看不到schema.table

1
2
3
4
5
6
7
mydb=# \d
List of relations
Schema | Name | Type | Owner
--------+------------+-------+----------
public | company | table | postgres
public | department | table | postgres
(2 rows)

可以通过select操作和\d schema.table查看,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
mydb=# select * from myschema.company;
id | name | age | address | salary
----+------+-----+---------+--------
(0 rows)

mydb=# \d myschema.company
Table "myschema.company"
Column | Type | Collation | Nullable | Default
---------+-----------------------+-----------+----------+---------
id | integer | | not null |
name | character varying(20) | | not null |
age | integer | | not null |
address | character(25) | | |
salary | numeric(18,2) | | |
Indexes:
"company_pkey" PRIMARY KEY, btree (id)

架构

原理

汇编入门

寄存器

CPU里有名为寄存器的存储电路,在机器语言中相当于变量的功能。具有代表性的寄存器如下,

  • AX - accumulator, 累加寄存器
  • CX - counter, 计数寄存器
  • DX - data,数据寄存器
  • BX - base,基址寄存器
  • SP - stack pointer,栈指针寄存器
  • BP - base pointer,基址指针寄存器
  • SI - source index,源变址寄存器
  • DI - destination index,目的变址寄存器

这些寄存器都是16位的。X这个字符表示extend,意为这些寄存器由8位扩展到了16位。

除了这8个16位寄存器之外,cpu还有8个8位寄存器,

  • AL - accumulator low, 累加寄存器低位
  • AH - accumulator high,累加寄存器高位
  • BL - base low,基址寄存器低位
  • BH - base high,基址寄存器高位
  • CH - counter high,计数寄存器高位
  • CL - counter low,计数寄存器低位
  • DL - data low,数据寄存位低位
  • DH - data high,数据寄存器高位

顺便提一下,在这些寄存器中数据存储都使用的是小端法,为什么使用小端法,可以参考(字节序探析:大端与小端的比较)[https://www.ruanyifeng.com/blog/2022/06/endianness-analysis.html]

段寄存器 - segment register
以下段寄存器均为16位,

  • ES - extra segment,附加段寄存器
  • CS - code segment,代码段寄存器
  • SS - stack segment,栈段寄存器
  • DS - data segment,数据段寄存器
  • FS - segment part 2,没有名称
  • GS - segment part 3,没有名称

32位寄存器

  • EAX
  • ECX
  • EDX
  • EBX
  • ESP
  • EBP
  • ESI
  • EDI

E仍然是Extend的含义,标识从16位扩展到了32位

有64位寄存器吗?

当然是有的,请参考(64位和32位的寄存器和汇编的比较)[https://blog.csdn.net/qq_29343201/article/details/51278798],但是作为汇编学习入门,我们一般使用16位寄存器来学习。

汇编入门

先看一段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
entry:
MOV AX,0 ; 初始化寄存器
MOV SS,AX
MOV SP,0x7c00
MOV DS,AX
MOV ES,AX

MOV SI,msg
putloop:
MOV AL,[SI]
ADD SI,1 ; 给SI加1
CMP AL,0
JE fin
MOV AH,0x0e ; 显示一个文字
MOV BX,15 ; 指定字符颜色
INT 0x10 ; 调用显卡BIOS
JMP putloop
fin:
HLT ; 让CPU停止,等待指令
JMP fin ; 跳转到fin继续执行,即无限循环

msg:
DB 0x0a, 0x0a ; 换行2次
DB "hello, world"
DB 0x0a ; 换行
DB 0

RESB 0x7dfe-$

DB 0x55, 0xaa

MOV

MOV的含义是赋值

1
MOV AX,0

表示将0赋值给AX(累加寄存器),相当于AX=0。

1
MOV SI,msg

标识将msg赋值给SI(源变址寄存器)。这里的msg就是下文的msg标号,代表了一段汇编程序。赋值后,msg所在地址(不是内存中的地址,而是cpu中的存储)就会被赋值给SI,后续其他代码就可以直接使用了。

1
MOV AL,[SI]

这里的方括号[SI],指的是内存中的SI。内存指的是主板上的内存设备。

MOV指令的数据传送源和传送目的地不仅可以是寄存器或常数,也可以是内存地址。

1
2
MOV BYTE [678],123
MOV WORD [678],123

这个指令是要用内存的“678”号地址来保存“123”这个数值。使用BYTE时,只有678号地址中的数据会有反应(8位),而使用WORD时,678,679号地址中的数据都会做出反应(16位)。

这里要提到一点,

对于16位机器来说吗,1字等于2字节,1字节等于8位,即1字等于16位。但在8位机器中,1字仍然时1字节。(32位中,1字=4字节)。

ADD

ADD是加法指令。ADD SI,1即为SI=SI+1

CMP

CMP源自compare,意为比较。CMP AL,0就是将AL中的值和0进行比较。

JMP

跳转

JE - jump if equal