ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

CSAPP perflab实验报告:rotate与smooth的CPE优化

CSAPP perflab实验报告:rotate与smooth的CPE优化 简介这份《perflab实验报告》是湖南大学计算机组成与结构课程的程序性能调优实验文档面向正在学习体系结构、计算机组成原理或缓存优化的高校学生与自学者解决图像处理函数 rotate 与 smooth 缺乏性能分析范例的问题。压缩包共 1 个 doc 文件约 444KB内容为完整的实验报告正文包含实验目的、环境说明、函数源码与多个优化版本的对比测试。报告以 Vmware 虚拟机 Ubuntu 12.04 为实验环境围绕 rotate 的行列调位旋转与 smooth 的邻域均值模糊展开前者用 4×4 分块提升空间局部性、减少缓存失效后者通过 rowsum 数组预计算行内像素和以降低 avg 函数调用频次并借助 driver 给出的 CPE 与加速比量化评估优化效果。读者可据此掌握循环展开、分块、减少函数调用、改善局部性等调优手段理解 dim 增大时 CPE 变化趋势所反映的缓存局限还能参考向量化 SIMD、缓存预取、多线程并行与编译器优化选项等进一步优化思路。目前已有 521 人学习适合作为实验复现与性能分析写作的对照材料。1. perflab实验报告解决的从来不是把函数写对这件事在 perflab-handout 目录里敲下make再跑一次./driver终端会打印出一张成绩表rotate 和 smooth 各自的 CPE以及它们和基线的比值。很多人第一次看到这张表会愣住——两个函数的逻辑明明都没写错可 rotate 的 CPE 动辄几十、smooth 也居高不下加速比还不到 1。perflab实验报告要交代的正是从这组难看数字出发怎么一步步把 CPE 压下去的过程哪些是访存模式的问题哪些是循环开销哪些是重复计算改完之后 driver 里的数字又凭什么信。它适配的是正在啃 CSAPP 性能优化章节的人、要交 Performance Lab 的学生以及想把cache 友好从口号落成代码的工程师。2. 先把perflab跑起来Makefile、driver与CPE的真实含义动手改之前得先弄清一件事perflab 的成绩不是你自己说多少就是多少是driver用周期计数器测出来的。测错对象、改错文件后面所有优化都是白干。2.1 perflab-handout里哪些文件能动、哪些一动就废解压出来的 handout 里真正跟你有关的只有kernels.c一个文件。driver.c负责调度和计时fcyc.c、clock.c提供周期测量defs.h定义pixel、RIDX这些公共设施Makefile控制编译。这些都不是给你改的尤其Makefile。常见做法是把naive_rotate和naive_smooth留着当对照然后在kernels.c里写自己的rotate与smooth最后在register_functions里注册让 driver 认得出你的版本。/* kernels.c 末尾把实现挂到 driver 的测试名单上 */ void register_functions() { /* 第一个参数是实现函数指针第二个是显示名 */ add_rotate_function(rotate, rotate_descr); add_smooth_function(smooth, smooth_descr); }改rotate_descr字符串有个实用好处driver 打印成绩时会带上这个名字你在报告里对比多个版本时一眼能分清哪个是哪个。注意一点编译期优化标志是被实验约束住的kernels 通常不开高等级优化目的就是逼你用手写方式把性能补回来。想靠-O3蒙混过去一来改不动构建二来报告里也站不住脚。2.2 make与./driverCPE数字是怎么算出来的make之后生成的driver会把每个函数在不同维度上跑很多遍用周期计数器累计耗时再除以元素个数得到 CPEcycles per element。rotate 的元素数就是dim*dimsmooth 同理。所以 CPE 的单位是处理一个像素花几个周期跟你 CPU 主频挂钩绝对值会变但它和基线的比值稳定。# 进入实验目录后 make # 编译出 driver ./driver # 跑完整测试打印 CPE 表 # rotate 默认会跑 64/128/256/512/1024 等维度smooth 类似跑完会看到类似下面的结构Mean 列是多维度的平均 CPESpeedup 是你的版本相对基线的加速倍数。字段含义报告里怎么用Dim图像维度如 64、128、256说明你覆盖了哪些规模Your CPEs你的实现每条元素耗时优化的直接证据Baseline CPEs基线naive 版耗时分母SpeedupBaseline / Your衡量提升的指标Mean各维度几何均值报告里最好只报这一列并注明算法提示CPE 受机器、频率、cache 配置影响报告里写清测环境的 CPU 型号和内存大小比只贴一个数字可信得多。2.3 读懂成绩表Baseline CPE与Your CPE的差值才是分数别只盯着自己的 CPE 低不低要看它离基线多远。基线是naive_rotate、naive_smooth在同一台机器同一轮测试里跑出来的天然帮你消掉了环境噪声。加速比接近 1说明你的优化没起作用加速比上去了但某个维度反而变差往往是那个维度超出 cache 容量、你的分块粒度开始失效。读表的时候把 Mean 和每一行的最差维度都看一遍比只看平均分更能暴露问题。理解了测量口径接下来就能对症下药地对付 rotate。3. perflab里rotate的优化从指针遍历到8×8分块rotate 的目标是把dim×dim的图像顺时针旋转 90 度逻辑上一行代码就能写对慢是因为它把访存模式搞崩了。3.1 naive_rotate到底慢在哪一次转置背后的写跨步看基线实现。/* defs.h 里给的行优先索引宏 */ #define RIDX(i, j, n) ((i) * (n) (j)) /* 基线读 src 连续写 dst 却每步跨一整行 */ void naive_rotate(int dim, pixel *src, pixel *dst) { int i, j; for (i 0; i dim; i) for (j 0; j dim; j) dst[RIDX(dim - 1 - j, i, dim)] src[RIDX(i, j, dim)]; }内层循环里j每加 1src的下标加 1连续cache 友好但dst的列号i不变、行号dim-1-j每步减 1等于每写一个像素就跳到相隔dim个像素的位置。一个 cache line 通常是 64 字节、约 16 个像素于是每写入一次就触发一次新的 cache line写带宽被彻底浪费。这才是 CPE 高的根因跟循环嵌套顺序、乘法次数都是次要的。3.2 代码移动与指针化先把循环里的乘法砍掉第一步不是分块而是把RIDX里的乘法从内层挪出去用指针步进替代。void rotate(int dim, pixel *src, pixel *dst) { int i, j; for (i 0; i dim; i) { pixel *d dst[RIDX(dim - 1, i, dim)]; /* 每行写起点固定 */ for (j 0; j dim; j) { d[0] src[RIDX(i, j, dim)]; d - dim; /* 指针下移一行免去乘法 */ } } }逻辑说明i一致时dst的行号始终从dim-1开始每步减 1所以先算出这一列的最上端地址d内层用d - dim往下走。参数上没有任何新东西纯粹把RIDX的j*n部分变成了指针自减。这一步能砍掉可观的分支和乘法开销但因为写 dst 依然是跨行跳CPE 改善有限——它只解决了计算开销没解决访存模式所以必须接着分块。3.3 分块为什么有效让src和dst同时待在cache里分块blocking的思路是不要一次处理一整行而是把图像切成小块块内 src 的一小片和 dst 的一小片能同时塞进 cache写dst时就不再反复换出 cache line。#define BLK 8 /* 块边长常用 8/16/32见 3.4 对比 */ void rotate(int dim, pixel *src, pixel *dst) { int i, j, ii, jj; for (ii 0; ii dim; ii BLK) for (jj 0; jj dim; jj BLK) for (i ii; i ii BLK; i) for (j jj; j jj BLK; j) dst[RIDX(dim - 1 - j, i, dim)] src[RIDX(i, j, dim)]; }逻辑说明外层两层把图像分成BLK×BLK的小块内层只在这个块里做原来的旋转写入。这样src块和dst块各自占用连续或接近连续的内存块大小选得当时BLK*BLK个像素能被 L1 容纳整块写完后 cache line 基本都被用满跨行写的浪费被摊薄。参数就是BLK它是唯一的自由度选大了块装不进 cache、选小了分块收益不明显。3.4 循环展开与BLK取值在分块基础上再做循环展开能进一步减少内层循环的判断和地址计算。for (i ii; i ii BLK; i) { pixel *s src[RIDX(i, jj, dim)]; pixel *d dst[RIDX(dim - 1 - jj, i, dim)]; for (j jj; j jj BLK; j 2) { /* 一次处理两个像素 */ d[0] s[0]; /* 对应 j */ d[-dim] s[1]; /* 对应 j1 */ s 2; d - 2 * dim; } }下面这张表是同一台机器上不同BLK的示意 CPE绝对值和你的机器相关重点看趋势。BLK是否整除常见维度典型 CPE 趋势备注不分块—最高跨行写cache line 用不满4是明显下降块偏小循环开销占比高8是接近最优L1 友好通用性好16是与 8 接近维度大时更稳32是依赖 cache 大小大维度可能反而退步perflab 里的维度基本是 64、128、256、512、1024 这类 2 的幂BLK取 8 或 16 都能整除不用写边界判断。如果非要通用把ii BLK改成min(ii BLK, dim)即可。选BLK时别只试一个值把 8、16、32 都跑一遍 driver用 Mean 那一列拍板。4. perflab里smooth的优化消除过程调用、合并重复求和与循环展开smooth 的朴素写法对每个像素做一次 3×3 邻域平均问题不在算法而在实现方式把大量开销藏进了函数调用里。4.1 naive_smooth的三层开销基线里的avg函数每个像素都要调一次而且邻域求和用了initialize_pixel_sum、accumulate_sum、assign_sum_to_pixel一串小函数。/* 基线每个像素一次函数调用邻域求和层层封装 */ static void accumulate_sum(pixel_sum *sum, pixel p) { sum-red (int)p.red; sum-green (int)p.green; sum-blue (int)p.blue; sum-num; }三层开销依次是过程调用本身压栈、返回、pixel_sum结构体的反复初始化与传参、以及每像素都要重新计算出邻域边界。dim一大这些固定开销乘上dim*dim次CPE 就下不来。改造方向很直接把求和展开到内层循环里用局部整型变量替代结构体把边界判断提到外层的行指针。4.2 展开avg指针算术取代RIDX与过程调用把每像素一次的函数调用换成直接用行指针取邻域。void smooth(int dim, pixel *src, pixel *dst) { int i, j; for (i 0; i dim; i) { /* 预先算好三行的行首边界行复用自身 */ pixel *r0 src RIDX((i 0) ? 0 : i - 1, 0, dim); pixel *r1 src RIDX(i, 0, dim); pixel *r2 src RIDX((i dim - 1) ? dim - 1 : i 1, 0, dim); for (j 0; j dim; j) { int jl (j 0) ? 0 : j - 1; /* 邻域左列 */ int jr (j dim - 1) ? dim - 1 : j 1; /* 邻域右列 */ int r 0, g 0, b 0, n 0, k; for (k jl; k jr; k) { /* 只遍历 2~3 列 */ r r0[k].red r1[k].red r2[k].red; g r0[k].green r1[k].green r2[k].green; b r0[k].blue r1[k].blue r2[k].blue; n 3; } pixel *d dst[RIDX(i, j, dim)]; d-red (unsigned char)(r / n); d-green (unsigned char)(g / n); d-blue (unsigned char)(b / n); } } }逻辑说明r0/r1/r2是邻域三行的行首指针内层只对 2~3 列做加法至少省掉了每像素一次函数调用和一整套结构体操作。参数n是本次邻域实际像素数边界处是 6 或 4中间是 9用它做除数保证边界正确。这一版已经把 CPE 压下一大截但每像素对三行各取一次k循环重复读取多。4.3 分块与循环展开的组合和 rotate 一样smooth 也能分块把图像切成BLK×BLK的小块让参与求和的三行数据在 cache 中反复命中同时在内层按列展开一次算两个相邻像素。/* 内层一次推进两列复用中间那一列的邻域和 */ for (j 1; j dim - 1; j 2) { int s0 r0[j-1].red r0[j].red r0[j1].red; int s1 r1[j-1].red r1[j].red r1[j1].red; int s2 r2[j-1].red r2[j].red r2[j1].red; /* 用同样的方式处理 green/blue再写 dst[i][j] 与 dst[i][j1] */ }展开的价值在于相邻像素的邻域高度重叠把列偏移固定下来能减少地址计算。下面这张表串起整条优化路径。优化阶段主要动作预期收益基线每像素一次 avg 调用最低去函数调用求和内联、结构体换局部变量CPE 大幅下降行指针r0/r1/r2 复用去掉 RIDX 乘法继续下降分块邻域三行常驻 cache大维度收益明显循环展开一次两列复用偏移收尾优化注意smooth 的边界像素邻域只有 4 或 6 个点除数是变的别用固定的 9 去当除数否则边缘颜色会偏。把 rotate 和 smooth 都改完接下来不是收工而是要想清楚报告里怎么把数字讲明白——否则 driver 里再漂亮的 Mean 也说服不了看报告的人。5. perflab实验报告怎么写CPE对比、加速比与可复现的验证报告的核心不是堆代码而是让每个优化步骤都有可复现的数字支撑。我的习惯是每改一版就立刻跑一次 driver把结果记下来而不是攒到最后一起测。5.1 用driver对同一函数的多版本做横向对比最有说服力的做法是保留每一版实现给它们不同的descr名字一次性注册让 driver 在同一次运行里全部测完避免环境漂移。/* 同时挂上三个版本一次跑完对比 */ void register_functions() { add_rotate_function(naive_rotate, naive); add_rotate_function(rotate_block8, rotate blk8); add_rotate_function(rotate_block16, rotate blk16); add_smooth_function(smooth_ptr, smooth ptr); add_smooth_function(smooth_block, smooth blk); }逻辑说明driver 会为每个注册项单独打印 CPE 和相对基线的速度放在同一轮里跑机器状态一致横向对比才公平。参数上不需要动任何东西只是把版本号作为字符串带出来。跑完把 Mean 列抄进报告表格即可。报告里至少要有一张这样的对比表并对每个优化阶段写一句话解释为什么有效。实现rotate Mean CPESpeedup关键改动naive高1.00基线指针化略降—去掉 RIDX 乘法分块 BLK8明显下降—写 dst 局部性改善分块展开最低—减少循环开销5.2 让数字可复现测环境与维度选择最后一步是验证稳定性。同一份代码在不同维度上的 CPE 会有波动把维度覆盖全报告里注明 CPU 型号、内存容量和编译方式别人照着跑才能复现。如果某个大维度 CPE 突然回升说明块超出了 cache 容量这时把BLK调小再测一次用数据说明分块粒度和 cache 的关系比空谈cache 友好有分量得多。# 连续跑两遍观察同一版本 CPE 波动幅度 ./driver | tee run1.txt ./driver | tee run2.txt # 波动很小才说明测量可信再把 Mean 写进报告把两次运行的 Mean 对比一下波动在几个百分点以内就可以采信超过这个范围先查机器是否在跑别的负载而不是急着改代码。本文还有配套的精品资源点击获取
返回列表