Stanford CS149 (Fall 2023) Parallel Computing
Programming Assignment 1: Analyzing Parallel Program Performance on a
Quad-Core CPU
Github repo
环境配置
鉴于应该都不是 Stanford 的学生,只能本地配置环境了。我使用的是 WSL
虚拟环境,Windows 大概是不能直接用的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 ~ » neofetch mizukicry@S-Terminal .-/+oossssoo+/-. mizukicry@S-Terminal `:+ssssssssssssssssss+:` -------------------- -+ssssssssssssssssssyyssss+- OS: Ubuntu 22.04.4 LTS on Windows 10 x86_64 .ossssssssssssssssssdMMMNysssso. Kernel: 5.10.16.3-microsoft-standard-WSL2 /ssssssssssshdmmNNmmyNMMMMhssssss/ Uptime: 33 mins +ssssssssshmydMMMMMMMNddddyssssssss+ Packages: 710 (dpkg) /sssssssshNMMMyhhyyyyhmNMMMNhssssssss/ Shell: zsh 5.8.1 .ssssssssdMMMNhsssssssssshNMMMdssssssss. Terminal: Windows Terminal +sssshhhyNMMNyssssssssssssyNMMMysssssss+ CPU: AMD Ryzen 9 7940H w/ Radeon 780M Graphics (16) @ 3.991GHz ossyNMMMNyMMhsssssssssssssshmmmhssssssso GPU: ceb6:00:00.0 Microsoft Corporation Device 008e ossyNMMMNyMMhsssssssssssssshmmmhssssssso Memory: 583MiB / 7560MiB +sssshhhyNMMNyssssssssssssyNMMMysssssss+ .ssssssssdMMMNhsssssssssshNMMMdssssssss. /sssssssshNMMMyhhyyyyhdNMMMNhssssssss/ +sssssssssdmydMMMMMMMMddddyssssssss+ /ssssssssssshdmNNNNmyNMMMMhssssss/ .ossssssssssssssssssdMMMNysssso. -+sssssssssssssssssyyyssss+- `:+ssssssssssssssssss+:` .-/+oossssoo+/-.
下载 ISPC
1 2 3 4 wget https://github.com/ispc/ispc/releases/download/v1.23.0/ispc-v1.23.0-linux.tar.gz tar -xvf ispc-v1.23.0-linux.tar.gz sudo mv ispc-v1.23.0-linux/bin/ispc /usr/local/bin rm -rf ispc-v1.23.0-linux.tar.gz ispc-v1.23.0-linux
Clone 作业代码
1 2 git clone git@github.com:stanford-cs149/asst1.git cd asst1
Program 1:
Parallel Fractal Generation Using Threads
生成分型图像:
1 2 3 cd prog1_mandelbrot_threads make ./mandelbrot
可以查看目录下生成的 mandelbrot-serial.ppm
图片,使用
./mandelbrot --view 2
生成 Image 2
Image 1
Image 2
完成程序的多线程部分,对比不同线程数(2 ~ 8)的 Speedup
几行代码的事,就是把矩形等分,看懂接口就行
1 2 3 4 5 6 7 8 9 void workerThreadStart (WorkerArgs *const args) { int startRow = args->height / args->numThreads * args->threadId; mandelbrotSerial (args->x0, args->y0, args->x1, args->y1, args->width, args->height, startRow, args->threadId == args->numThreads - 1 ? args->height - startRow : args->height / args->numThreads, args->maxIterations, args->output); }
修改 main.cpp
中线程数量观察 Speedup(本地 WSL
测试仅图一乐,连续 4 轮取极大值):
2
1.99
1.70
3
1.65
2.22
4
2.46
2.63
5
2.51
3.02
6
3.30
3.44
7
3.47
3.89
8
4.10
4.32
16
7.66
7.46
可以观察到 Speedup 不是线性增长,且在 3
线程时反而出现了下降。观察发现每个线程计算的时间与区域有关,像素亮度取决于计算成本,对每个线程单独计时也可以验证这一点。观察
Image 1 就不难理解为什么 3 线程的 Speedup 反而下降了
于是可以采取 Interleaved Assignment 而非 Blocked Assignment
均摊计算
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 void mandelbrotSerialInterleaved ( float x0, float y0, float x1, float y1, int width, int height, int startRow, int skipRows, int maxIterations, int output[]) { float dx = (x1 - x0) / width; float dy = (y1 - y0) / height; for (int j = startRow; j < height; j += skipRows) { for (int i = 0 ; i < width; ++i) { float x = x0 + i * dx; float y = y0 + j * dy; int index = (j * width + i); output[index] = mandel (x, y, maxIterations); } } }
1 2 3 4 5 6 7 8 9 10 11 extern void mandelbrotSerialInterleaved (float x0, float y0, float x1, float y1, int width, int height, int startRow, int skipRows, int maxIterations, int output[]) ;void workerThreadStart (WorkerArgs *const args) { mandelbrotSerialInterleaved ( args->x0, args->y0, args->x1, args->y1, args->width, args->height, args->threadId, args->numThreads, args->maxIterations, args->output); }
2
1.99
1.97
3
3.00
2.95
4
3.98
3.90
8
7.80
7.60
16
13.95
13.60
Program 2:
Vectorizing Code Using SIMD Intrinsics
使用 CS149intrin.h
中提供的类 SIMD 函数完成
clampedExpVector
函数
其实也就是照着一句一句地翻译,只是分支判断变成了位运算。不过第一次写这种东西还是有点烧脑,写得很慢
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 void clampedExpVector (float *values, int *exponents, float *output, int N) { __cs149_vec_float x; __cs149_vec_int y; __cs149_vec_int zero = _cs149_vset_int(0 ), one = _cs149_vset_int(1 ); __cs149_vec_float limit = _cs149_vset_float(9.999999f ); __cs149_vec_float result; __cs149_mask maskAll, mask, maskLoop; for (int i = 0 ; i < N; i += VECTOR_WIDTH) { maskAll = _cs149_init_ones(N - i); _cs149_vload_float(x, values + i, maskAll); _cs149_vload_int(y, exponents + i, maskAll); _cs149_veq_int(mask, y, zero, maskAll); _cs149_vset_float(result, 1.f , mask); mask = _cs149_mask_not(mask); mask = _cs149_mask_and(mask, maskAll); _cs149_vmove_float(result, x, mask); _cs149_vsub_int(y, y, one, mask); while (_cs149_vgt_int(mask, y, zero, mask), _cs149_cntbits(mask) > 0 ) { _cs149_vmult_float(result, result, x, mask); _cs149_vsub_int(y, y, one, mask); } _cs149_vgt_float(mask, result, limit, maskAll); _cs149_vmove_float(result, limit, mask); _cs149_vstore_float(output + i, result, maskAll); } }
结果如下:
2
77.6%
4
70.5%
8
66.7%
16
65.0%
结果比较显然,因为当 Vector Width
越大,其中分支不一样的概率越高,利用率越低
Extra credit: 实现一个数组求和,要求复杂度
(N / VECTOR_WIDTH + VECTOR_WIDTH)
或者
(N / VECTOR_WIDTH + log2(VECTOR_WIDTH))
很显然主要取决于最后怎么把结果加起来,log2(VECTOR_WIDTH)
显然就是每次把相邻的两个加起来减少一半,一种经典用法是手动 \(O(\log)\) 的 BitCount
1 2 3 4 5 6 7 8 9 10 11 12 13 float arraySumVector (float *values, int N) { __cs149_vec_float sum = _cs149_vset_float(0.f ), v; __cs149_mask maskAll = _cs149_init_ones(); for (int i = 0 ; i < N; i += VECTOR_WIDTH) { _cs149_vload_float(v, values + i, maskAll); _cs149_vadd_float(sum, sum, v, maskAll); } for (int i = 1 ; i < VECTOR_WIDTH; i <<= 1 ) { _cs149_hadd_float(sum, sum); _cs149_interleave_float(sum, sum); } return sum.value[0 ]; }
测试的时候忘了 N
必须设为 2
的幂,试了好久一直没过()
Program 3
Part 1. A Few ISPC Basics
测试 ISPC 版本的 mandelbrot 图像生成速度
由于是 8-wide 的指令,预测 Speedup 接近 8.0,但实测 Image 1 Speedup
为 6.21,Image 2 Speedup 为
5.19。推测是由于不同实例的计算时间不同,导致部分 ALU 闲置,最终 Speedup
低于理论值
在 SIMD
指令的基础上再使用多核加速。由于每个核是独立执行,相比单核版本理论加速倍数就是核数
双核条件下实测 Image 1 Speedup 6.16 -> 11.94,Image 2 Speedup 5.20
-> 8.91。测试结果中 Image 1 结果符合预期,Image 2 加速为
1.7x,略低于预期,推测是由于 Image 2 上下图像不对称,运行时间不同
增加 task 数量可以进一步增加 Speedup。理论上 task
数量等于核数时最快,若支持超线程功能可以再翻倍
Program 4: Iterative
sqrt
4.92x speedup from ISPC, 35.59x speedup from task ISPC
显然全部设成同样的值,让每个实例时间相同
1 2 3 4 for (unsigned int i = 0 ; i < N; i++){ values[i] = 2.5f ; }
6.35x speedup from ISPC, 35.29x speedup from task ISPC
观察到单核 ISPC 的 Speedup
上升,但多核基本不变,因为原本随机数据下每个核期望时间就相同
同理,交错构造,让一个 ALU 跑满,其它 ALU 闲置
1 2 3 for (unsigned int i = 0 ; i < N; i++) { values[i] = (i & 7 ) ? 1.0f : 2.999f ; }
0.92x speedup from ISPC, 7.53x speedup from task ISPC
Extra Credit: 自己通过 AVX2 指令集手动实现 sqrt
目前大多数比较新的处理器应该是支持 AVX2 的,所以本地也能做。查看 CPU
指令集:cat /proc/cpuinfo
也没啥好说的,RTFM,照着写就行
但是我是读手册苦手,找半天找不到有用的指令,最后大部分是 Copilot
帮我找的
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 31 32 33 34 35 void sqrtAvx2 (int N, float initialGuess, float values[], float output[]) { static const __m256 kThreshold = _mm256_set1_ps(0.00001f ); for (int i = 0 ; i < N; i += 8 ) { if (i + 8 > N) { sqrtSerial (N - i, initialGuess, values + i, output + i); break ; } __m256 x = _mm256_loadu_ps(values + i); __m256 guess = _mm256_set1_ps(initialGuess); while (true ) { __m256 error = _mm256_sub_ps( _mm256_mul_ps(_mm256_mul_ps(guess, guess), x), _mm256_set1_ps(1.f )); error = _mm256_andnot_ps(_mm256_set1_ps(-0.f ), error); __m256 cmp = _mm256_cmp_ps(error, kThreshold, _CMP_GT_OQ); if (_mm256_testz_ps(cmp, cmp)) { break ; } __m256 newGuess = _mm256_mul_ps( _mm256_mul_ps( _mm256_sub_ps(_mm256_set1_ps(3.f ), _mm256_mul_ps(x, _mm256_mul_ps(guess, guess))), guess), _mm256_set1_ps(0.5f )); guess = _mm256_blendv_ps(guess, newGuess, cmp); } _mm256_storeu_ps(output + i, _mm256_mul_ps(x, guess)); } }
里面几个点:对于凑不齐 8
个的避免麻烦直接线性处理了,不影响性能;求绝对值的时候直接位运算操作符号位;使用各种指令时注意是否有要求内存对齐,否则会出些难以调试的问题
最后在各种 case 下都快于 ISPC,下面是随机数据的测试结果:
1 2 3 4 5 6 7 [sqrt serial]: [1202.493] ms [sqrt ispc]: [244.737] ms [sqrt task ispc]: [36.260] ms [sqrt avx2]: [221.520] ms (4.91x speedup from ISPC) (33.16x speedup from task ISPC) (5.43x speedup from avx2)
Program 5: BLAS saxpy
测试结果:0.91x speedup from use of tasks
发现多核没有性能提升,原因是程序主要瓶颈为内存读写,计算只是一条简单语句,因此多核没有效果
为什么程序中计算内存带宽是 4N
个 float
不是 3N
个 float
?
考虑 Cache 的工作机制,写入到 result
时会先将内存读入到
Cache 中,修改后再写回内存,因此是 4N 个 float
只能说我实在想不出来内存瓶颈怎么解决,摸了
Program 6: Making
K-Means
Faster
没数据。。如果实在想做的话大概得自己造了
大概内容就是自己找 K-Means
的瓶颈然后优化,大概也是和上面的一样,就跳过了