Skip to content

Commit b456b6b

Browse files
Merge pull request #53 from zhangzhuang15/dev
Dev
2 parents 0bf6472 + 9a91351 commit b456b6b

File tree

15 files changed

+985
-44
lines changed

15 files changed

+985
-44
lines changed

.vitepress/config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,10 @@ export default defineConfig({
359359
text: 'crossbeam 学习笔记',
360360
link: '/blog/crossbeam-learning-notes'
361361
},
362+
{
363+
text: 'GPU介绍',
364+
link: '/blog/gpu'
365+
},
362366
{
363367
text: "博客文章阅读系列",
364368
collapsed: true,

docs/blog/crossbeam-learning-notes.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,15 @@ struct CachedPadded<T> {
2727
}
2828
```
2929

30+
一个C语言内存对齐的例子:
31+
```c
32+
struct M {
33+
int64_t a;
34+
int32_t b;
35+
}
36+
```
37+
a必须8字节对齐,b必须4字节对齐,而默认情况下,M就必须按照成员中,内存对齐要求最大的那个对齐,也就是8字节对齐。这就意味着,b不足8字节,需要在b的后边补充4字节,这4个字节就是padding。实际上,我们还可以让M按照8的整数倍对齐,比如按照16字节对齐,32字节对齐。但是,我们不能按照4字节对齐。这是因为按照4字节对齐,b占用的起始内存地址一定是4的整数倍,但是对于a来说,它的起始内存地址不一定是8的整数倍。在x86_64平台来说,可能没什么,但是在arm平台中,如果没有对齐,访问a的时候就会出现硬件错误。
38+
3039
## BackOff
3140
用于控制线程自旋。在自旋的基础上,如果超过指定次数的尝试后,调用操作系统API使得线程交出CPU,等待下一次操作系统调度后,重新执行。BackOff的执行效果就是,让线程自旋一会儿,或者自旋一会儿后交出CPU。
3241

@@ -148,3 +157,27 @@ Release/Acquire的内存顺序,在落实到汇编代码层面的时候,根
148157

149158
但是,aarch64体系就不同了,它是弱内存顺序的类型,会有单独的汇编指令实现Release/Acquire的内存顺序要求,比如读取数据的时候会用ldar(load-accquire)指令,写入数据用stlr(store-release)指令。
150159
:::
160+
161+
162+
## sync tool
163+
### WaitGroup
164+
WaitGroup提供一个wait方法,当所有的线程都执行到wait的时候,就会唤醒所有线程继续往下执行。
165+
166+
WaitGroup内部用`Arc`包含了一个Inner,Inner由 `Mutex<usize>``CondVar`构成。在 wait 方法被调用时,会修改 `Mutex<usize>`, 令其内部的数据减1,如果不等于0,意味着还有其他的线程没有执行wait方法呢,于是当前线程会利用`CondVar.wait`进入阻塞状态;如果等于0,意味着所有的线程都执行过wait方法了,于是当前线程使用`CondVar.notify_all`唤醒所有线程。WaitGroup在多线程之间传送时,必须在当前线程内,调用WaitGroup.clone创造一个副本,将副本发送给另一个线程,也就是在创建副本的时候,`Mutex<usize>`内部的数据增1.
167+
168+
### Parker
169+
Parker是阻塞线程和恢复线程的工具,当调用`parker.park`的时候,就会把当前线程挂起,当调用`parker.unpark`的时候,就会使得某个线程从挂起中恢复。
170+
171+
做到这些也不难。Parker内部有一个原子数据,一个Mutex, 一个CondVar。阻塞和恢复线程,就是通过Mutex和CondVar完成的。原子数据用来标记当前现场的情况,是刚从挂起中恢复,还是空闲。
172+
173+
174+
## channel
175+
Channel的实现原理并不难。按照Channel的使用形式来看,它应该有一个东西,让用户写入数据,这就是Sender;它还要有一个东西,让用户读取数据,这就是Receiver。而作为数据的存储地,Channel应该有一个数据容器,比如数组或者列表。数组的容量是有限的,这样的Channel就是 bounded Channel。列表的容量是无没有限制的,这取决于操作系统的内存,这样的Channel就是 unbounded Channel。后边,我们把这种数据容器称为channel。这样,Channel就等同于 Sender + Receiver + channel。
176+
177+
Sender和Receiver共享channel的引用,这样就可以做到一个发送数据,一个接受数据。如果发送方和接收方不是操作同一个channel,那么它们之间无法达成数据交流。
178+
179+
你定会好奇,channel什么时候被drop呢?别担心,在 Sender 和 Recevier 内部,拥有计数器。当Sender或者Receiver被drop的时候,计数器会减1,当计数器为0的时候,它们就会触发 channel 的 drop。
180+
181+
还有一个问题是,如果channel满了,如何阻塞Sender? 如果channel空了,如何阻塞Receiver呢?秘密在channel身上,它除了封装读、写数据的操作外,还拥有Senders属性和Receivers属性,分别记录被阻塞的Sender和Receiver。要阻塞Sender的时候,只需要将Sender写入到channel.Senders,然后调用上一节中提到的Parker.park,就能实现阻塞,当然也可以使用Rust标准库的thread::park,也可以使用上一节提到的Mutex+CondVar的方法。Receiver的情况同理,写入到Receviers。
182+
183+
crossbeam在实现的时候做了优化,比如用无锁技术去实现写入和读取,在正式阻塞线程之前,会用自旋锁的方法反复尝试争取资源,阻塞和恢复线程的封装在Context里实现,而Context放在了thread_local里,Senders和Receivers会持有Context的引用。

docs/blog/gpu.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
title: "GPU介绍"
3+
page: true
4+
aside: true
5+
---
6+
7+
# GPU介绍
8+
以英伟达cuda下的GPU为准
9+
10+
## GPU结构
11+
![GPU结构简图](/gpu-structure.png)
12+
13+
值得注意:
14+
1. GPU拥有多个SM
15+
2. 每个SM拥有多个CUDA核心
16+
3. 每个SM的CUDA核心可以访问L1缓存和共享内存
17+
4. 每个CUDA核心负责执行一个线程
18+
5. CUDA核心只负责数据计算和逻辑计算,不负责控制,SM负责控制
19+
6. 所有的SM共享L2 cache
20+
7. GPU显存全局共享
21+
8. 访存速度:寄存器 > 共享内存 >= L1缓存 > L2 缓存 > GPU显存
22+
23+
## GPU计算流程
24+
1. CPU申请一部分主机内存,存入要计算的数据
25+
2. CPU向GPU发送指令,让GPU从GPU显存中开辟一块内存空间
26+
3. CPU向GPU发送指令,GPU将要计算的数据从主机内存复制到GPU显存刚才开辟的空间
27+
4. CPU向GPU发送计算指令,GPU驱动SM开始并行计算
28+
5. GPU将计算结果从GPU显存拷贝到主机内存,通知CPU拿结果
29+
30+
## CUDA编程
31+
使用CUDA编程,驱动GPU工作。CUDA可以简单理解为一个定制化的C语言,其语法结构主体上和C/C++很像,不能用普通的C/C++编译,要使用专门的编译器编译。CUDA编写的程序是并行运行的,跑在每一个CUDA核心上。假设有一个矩阵乘法运算,A矩阵(M行K列)和B矩阵(K行N列),如果放在CPU计算,CPU要计算出M*N中每一个数据,GPU却不同,每个CUDA核心只需要计算M*N中的一个数据即可。由于GPU的CUDA核心众多,可以完成M*N个CUDA核心并行计算,而CPU的核心数量少,无法支持那么多的并行量,比如 M3 Pro 入门版芯片的CPU核心是6+6的,意味着最多也就16个线程的并行计算。
32+
33+
34+
<Giscus />

docs/blog/learning-cpp.md

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1127,6 +1127,138 @@ int main() {
11271127
}
11281128
```
11291129
1130+
### module
1131+
C++20支持Module,代码编写单元是Module,不再是 `.h` + `.cpp` 的组合。
1132+
1133+
`.h`有着缺陷:
1134+
1. 拖慢编译速度
1135+
2. 符号污染
1136+
3. 重复声明
1137+
1138+
在开始介绍如何使用之前,请确保c++编译器支持C++20标准。我这里以macOS平台的Clang为例,Clang版本号是19.1.5, 安装方法是 `brew install llvm`。虽然Clang官网文档里说,Clang15已经支持C++20 module的很多特性,但是经我实验,发现`global module` `module partition` 支持的有问题。
1139+
1140+
:::code-group
1141+
```cpp [src/util/util.cppm]
1142+
export module util;
1143+
export import :math;
1144+
export import :console;
1145+
```
1146+
1147+
```cpp [src/util/console.cppm]
1148+
module;
1149+
#include <string>
1150+
export module util:console;
1151+
1152+
export namespace util {
1153+
void error_toast(std::string s);
1154+
}
1155+
```
1156+
1157+
```cpp [src/util/console.cpp]
1158+
module;
1159+
#include <iostream>
1160+
#include <string>
1161+
module util:console;
1162+
1163+
namespace util {
1164+
void error_toast(std::string s) {
1165+
std::cout << "error: " << s << std::endl;
1166+
}
1167+
}
1168+
```
1169+
1170+
```cpp [src/util/math.cppm]
1171+
export module util:math;
1172+
1173+
namespace util {
1174+
int add(int a, int b);
1175+
}
1176+
```
1177+
1178+
```cpp [src/util/math.cpp]
1179+
module util:math;
1180+
1181+
namespace util {
1182+
int add(int a, int b) {
1183+
return a + b;
1184+
}
1185+
}
1186+
```
1187+
1188+
```cpp [src/main.cpp]
1189+
import util;
1190+
1191+
int main() {
1192+
util::error_toast("help me");
1193+
return 0;
1194+
}
1195+
```
1196+
1197+
```makefile [src/makefile]
1198+
CC = /opt/homebrew/opt/llvm/bin/clang++
1199+
Flags = -std=c++20
1200+
1201+
1202+
version:
1203+
$(CC) --version
1204+
1205+
clear:
1206+
rm -f *.o
1207+
rm -f *.pcm
1208+
rm -rf util/*.o
1209+
1210+
build-util:
1211+
$(CC) $(Flags) --precompile util/console.cppm -o util-console.pcm
1212+
$(CC) $(Flags) -c util/console.cpp -fmodule-file=util:console=util-console.pcm -o util.console.impl.o
1213+
$(CC) $(Flags) --precompile util/math.cppm -o util-math.pcm
1214+
$(CC) $(Flags) -c util/math.cpp -fmodule-file=util:math=util-math.pcm -o util.math.impl.o
1215+
$(CC) $(Flags) --precompile util/util.cppm -fmodule-file=util:console=util-console.pcm \
1216+
-fmodule-file=util:math=util-math.pcm -o util.pcm
1217+
1218+
build: build-util
1219+
$(CC) $(Flags) -c main.cpp -fmodule-file=util=util.pcm \
1220+
-fmodule-file=util:console=util-console.pcm \
1221+
-fmodule-file=util:math=util-math.pcm \
1222+
-o main.o
1223+
$(CC) $(Flags) main.o util.console.impl.o util.math.impl.o \
1224+
-o main
1225+
1226+
run: clear build
1227+
@./main
1228+
make clear
1229+
1230+
.PHONY: clear build-util build run version
1231+
```
1232+
:::
1233+
1234+
编译:
1235+
```shell
1236+
make build
1237+
```
1238+
1239+
运行:
1240+
```shell
1241+
./main
1242+
```
1243+
1244+
`.cppm`就是采取模块化的cpp文件扩展名,一般来讲,我们只在这个文件里编写函数、类、结构体等等声明,但是也能给出实现。考虑到扩展,不建议在`.cppm`中给出实现。而`.cppm`里的声明,我们放在`.cpp`里实现。
1245+
1246+
你可以看到,`src/util/console.cppm`里写的是声明,`src/util/console.cpp`是模块的实现。二者的区别在于,声明的一方,用 `export module` 交代模块名;实现的一方没有`export`。无论是哪一个,如果想要引入`.h`,必须放在`module;`后边, 本模块名的前边(`export module util:console`, `module util:console`).
1247+
1248+
`util:console`令你很奇怪吧?这个就是`module partition`, 标识它是 `module util`的一部分,因此你在 `src/util/util.cppm` 里看到 `export import :console;`,意思就是把 `util:console`声明的东西引入进来,并且暴露出去,供上层调用。
1249+
1250+
module最麻烦的地方就是编译逻辑。你可以这样理解,`.cppm`要编译为`.pcm`,这个`.pcm`的作用是:
1251+
1. 在编译`.cpp`的时候,遇到`import <module_name>`了,告诉编译器到哪里找到`<module_name>`的符号信息
1252+
2. 在编译另外一个`.cppm`的时候,遇到`import <module_name>`了,告诉编译器到哪里找到`<module_name>`的符号信息
1253+
1254+
所以,你看到了,在编译`util.cppm`的时候,我们用到了`util-console.pcm``util-math.pcm`。在编译`main.cpp`的时候,我们用到了`util.pcm`,`util-console.pcm``util-math.pcm`
1255+
1256+
但最终,是`.o`文件编译为最终的可执行文件,和`.pcm`无关了。值得注意的是,如果`.cppm`你不仅声明了,还给出定义了,你除了将这个文件编译为`.pcm`,还要编译为`.o`,毕竟你给出了实现。
1257+
1258+
`util-console.pcm`的名字是有讲究的,对于声明了`export module A``.cppm`文件,我们编译的结果应命名为`A.pcm`,对于声明了`export module A:B``.cppm`文件,我们编译的结果应命名为`A-B.pcm`
1259+
1260+
更详细的指引,请看[Clang 15.0.0 | Standard C++ Modules](https://releases.llvm.org/15.0.0/tools/clang/docs/StandardCPlusPlusModules.html#quick-start)
1261+
11301262

11311263
## lvalue, rvalue and movable semantic
11321264
lvalue: 有明确内存地址的数据;

docs/blog/swift-package-manager.md

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ aside: true
55
---
66

77
## Description
8-
我是一个很痴迷Rust的人,就是因为它里边有个叫做类型模式匹配的东西,表达能力极强。偶然的时候,我发现Swift里边也有,又看了下其他语法,感觉Swift也很不错,想深入了解一下。我是写前端的,要想写起来一个前端项目,npm/pnpm/yarn这样的工具必不可少,webpack/rollup/vite这样的工具也不能丢。在Rust, 这些功能由统一的工具cargo实现,非常方便。那么,我要了解swift,自然而然就想到了它有没有包管理工具,它是如何拆分模块的。
8+
我是一个很痴迷Rust的人,因为它里边有个叫做类型模式匹配的东西,表达能力极强。我发现Swift里边也有,又看了下这门语言的语法,感觉Swift也很不错,想深入了解一下。我是写前端的,要想写一个前端项目,npm/pnpm/yarn这样的工具必不可少,webpack/rollup/vite这样的工具也不能丢。在Rust, 这些功能由统一的工具cargo实现,非常方便。那么,我要了解swift,自然而然就想到了它有没有包管理工具,它是如何拆分模块的。
99

1010
## Fucking Module
1111
在夸swift之前,我必须要喷它,它的module管理太垃圾了。在前端,你如果想使用一个模块,你需要:
@@ -14,8 +14,8 @@ import { hello } from "../hello.js"
1414
```
1515
Good:
1616
- 能看到我要使用的函数是什么
17-
- 能看到这个函数在哪个地方
18-
- 即便是自定义的模块,只需按照文件路径引入即可
17+
- 能看到这个函数定义在哪个地方
18+
- 自定义的模块,只需按照文件路径引入即可
1919

2020
但是,swift就不一样:
2121
```swift
@@ -24,13 +24,13 @@ import Foundation
2424

2525
Bad:
2626
- Foundation 在哪里我不知道
27-
- 用什么函数不知道
27+
- 什么函数可以用,我不知道
2828
- 如果是自定义的模块,该怎么引入也不知道
2929

3030
问了GPT,也没告诉我什么有效信息。我决定自己趟一次浑水。
3131

3232
## swift package manager
33-
swift据说由若干个包管理工具,我只尝试了官方的工具。
33+
swift据说有多个包管理工具,我只尝试了官方的工具。
3434

3535
创建个项目看看吧。
3636
```shell
@@ -420,6 +420,58 @@ let package = Package(
420420
)
421421
```
422422

423+
## 入口文件
424+
在上边的例子中,`Sources/Main`里边,只有一个`main.swift`,如果我增加一个swift文件行不行,如果把`main.swift`换成另一个文件名,行不行呢?
425+
426+
::code-group
427+
```swift [Package.swift]
428+
// swift-tools-version: 6.0
429+
// The swift-tools-version declares the minimum version of Swift required to build this package.
430+
431+
import PackageDescription
432+
433+
let package = Package(
434+
name: "module-demo",
435+
targets: [
436+
// Targets are the basic building blocks of a package, defining a module or a test suite.
437+
// Targets can depend on other targets in this package and products from dependencies.
438+
.executableTarget(
439+
name: "hello",
440+
path: "Sources/Main"
441+
)
442+
]
443+
)
444+
```
445+
```swift [Sources/Main/b.swift]
446+
func hello() {
447+
print("hello peter")
448+
}
449+
```
450+
```swift [Sources/Main/a.swift]
451+
func main() {
452+
hello()
453+
}
454+
hello()
455+
```
456+
:::
457+
458+
执行:
459+
```shell
460+
swift run hello
461+
```
462+
很遗憾,无法执行。因为swift无法知道a.swift和b.swift到底谁才是入口文件。
463+
464+
给出swift入口文件的方法有两种,第一种就是给出`main.swift`,也就是说将`a.swift`改为`main.swift`。另外一种是将`a.swift`的内容调整为:
465+
```swift
466+
@main
467+
struct App {
468+
static func main() throws {
469+
hello()
470+
}
471+
}
472+
```
473+
474+
423475

424476
## 感受
425477
只能说功能都覆盖到了,但是和他宣传的样子相比,实在算不上简单,教程也少,Fuck! 和 Go,Rust相比,特别是Rust相比,就是个弟弟。

0 commit comments

Comments
 (0)