Cache架构优化和AMD μProf的简单使用


本次实验将探究Cache优化、超标量优化两种对程序的优化方法以及带来的性能提升,从实验结果中探究并行程序设计的优越之处。

关键字: 并行程序设计,Cache优化,超标量优化,CPU Profilling

实验平台: x86平台,Windows 10 64位操作系统,CPU型号AMD
R7-5800h,8核16线程,主频3.2GHz,单核拥有32KB一级数据、32KB一级指令缓存、512KB二级缓存、共享16MB三级缓存;DDR416G内存,TDM-GCC 编译器+Code::Blocks集成开发环境,CPU Profilling工具:AMD uProf

编译选项: Have g++ follow the C++17 GNU C++ language standard
,Target x86_64(64bit),Optimize even more(for speed)

代码链接:样例代码

实验一:n*n矩阵与向量内积

算法设计

逐列访问元素的平凡算法

按照矩阵的乘法运算法则进行算法设计,对于一个1*n的向量 B 1 ∗ n B_{1*n} B1n和一个n*n的方阵 A n n A_{nn} Ann,采用逐列访问矩阵元素的方法,依次将 A n ∗ n A_{n*n} Ann的每一列与 B 1 n B_{1n} B1n相乘并累加,每次求出内积的一个结果。

cache优化算法

由于按列访问方阵的元素容易造成更多的Cache缺失,使得运行效率降低,于是考虑从Cache的角度对算法进行优化。采用逐行访问矩阵元素的方法,依次将方阵A的第i行与B的第i个值相乘,每次得到最终向量结果的一个累加因子,在最后累加得到内积结果。

逐行访问矩阵元素时, A i j A_{ij} Aij A i ( j + 1 ) A_{i(j+1)} Ai(j+1)常常同时在Cache当中,当对 A i j A_{ij} Aij执行操作后能快速方便的从Cache中取出 A i ( j + 1 ) A_{i(j+1)} Ai(j+1)执行下一步操作,省去了从内存中读取数据的步骤,更加快速。

编程实现

本次实验采取的数据规模是一个1*5000的行向量B,一个5000*5000方阵A,二者的向量内积是一个1*5000的向量sum,具体定义与赋值可见代码

  • 逐列访问的平凡算法

        int i,j;
        for(i=0;i<MAX;i++)
            for(j=0;j<MAX;j++)
                sum[i]+=B[j]*A[j][i];   //平凡算法,逐列访问
    
  • Cache优化算法

        int i,j;
        for(i=0;i<MAX;i++)
            for(j=0;j<MAX;j++)
                sum[j]+=A[i][j]*B[i];      //Cache优化,逐行访问
    

性能测试

用计算程序执行时间来衡量程序的性能,经过多次运行程序测试后得到程序的执行时间结果如下表:

12345678910平均执行时间
平凡算法0.2420.2580.2680.2670.2480.2810.2530.2670.2790.2780.2641
Cache优化0.1460.1430.1410.1460.1530.1230.1480.1450.1490.1290.1423

可见,Cache优化后的算法的程序执行速度是平凡算法的1.86倍,平均执行时间是平凡算法的的53.88%,性能提升近乎一倍。

更改程序数据规模后得到以下结果:

数据规模Cache优化平均执行时间平凡算法平均执行时间加速比
5000.0250.0261.04
10000.0300.0321.06
20000.0380.0511.34

在这里插入图片描述

综上可知,进行Cache优化后的程序性能优于平凡算法,而且随着问题规模的增大这样的优势愈发明显,在5000的规模时优化倍率达到1.86,性能几乎翻倍,这与Cache优化算法的访存模式具有很好空间局部性,令cache 的作用得以发挥,减少Cache缺失的次数,提高工作效率。

Profilling

本次实验平台为AMD R7-5800h处理器,采用AMD Zen3架构,可以使用AMD官方CPU
Profill工具AMD
uProf
来对执行程序时CPU的活动进行分析,分析当输入规模为5000时平凡算法与Cache优化后各自的L1与L2级的Cache
Miss等,得到结果如下:

算法CPI%L1_DC_MISSESL1_DC_MISS_RATE
平凡算法1.0933.770.11
Cache优化0.604.360.01

由表3可知,优化后CPI大大减少,程序性能获得较大提升;同时,L1级缓存的缺失率(L1_DC_MISS_RATE)也由原先0.11降至0.01,提升明显;L1级的Cache Miss的比例(%L1_DC_MISS)也由原先33.77%降至4.36%,提升也十分明显。

表4中可以看见每千次指令中由L1 Cache Miss引起的L2级缓存的访问(L2_ACCESS_FROM_L1_MISS(PTI))从优化前的126.85次降至13.94,也代表着L1级缓存缺失情况的减少;

同时L2级缓存缺失(L2_MISS_FROM_L1_MISS(PTI))也大大降低,从每千次47.22降至0,L2级的缓存命中率( L 2 _ H I T _ F R O M _ L 1 _ M I S S L 2 _ A C C E S S _ F R O M _ L 1 _ M I S S \frac{L2\_HIT\_FROM\_L1\_MISS}{L2\_ACCESS\_FROM\_L1\_MISS} L2_ACCESS_FROM_L1_MISSL2_HIT_FROM_L1_MISS)也由原先的60.37%上升至98.42%,可见从Cache角度优化算法后性能得到了很大的提升。

结果分析

从程序的角度分析,当采用平凡算法时逐列访问矩阵元素, A i j A_{ij} Aij A ( i + 1 ) j A_{(i+1)j} A(i+1)j的内存位置并不相邻,并且随着数据规模的变大相隔更远,不会同时在L1级Cache中,导致访问下一个元素时无法快速从Cache中取出下一个操作对象,发生Cache缺失,又要花费更多时间从内存中去读数,增加了时间开销,效率降低。

而当优化算法后采取逐行访问矩阵元素的方式, A i j A_{ij} Aij A i ( j + 1 ) A_{i(j+1)} Ai(j+1)绝大部分情况下都同时在Cache当中,此时对 A i j A_{ij} Aij执行操作后能快速方便的从Cache中取出 A i ( j + 1 ) A_{i(j+1)} Ai(j+1)执行下一步操作,不会发生Cache缺失,省去了从内存中读取数据的步骤,更加快速。

从实验结果来讲,无论从运行时间还是L1、L2级缓存的表现均能判断出Cache优化后的算法更好,且随着数据规模变大,这样的优势愈发明显,程序运行的CPI和Cache
Miss次数等等指标都得到明显改善,说明此次实验采用的算法是合理且有效的。

实验二 超标量优化:n个数求和

算法设计

平凡算法

即简单的顺序相加,用一个遍历待累加数组A[MAX]的的循环依次累加A[i],最终得到结果。

超标量优化算法

  • 两路链式算法

    尝试将循环展开,设置两个中间结果sum1与sum2,分别为数组的奇数项和与偶数项和,两个中间变量的累加可以在同一个循环中执行,因此只用执行 M A X 2 \frac{MAX}{2} 2MAX次循环,最后将sum1与sum2相加即可得到结果。通过同时执行两步运算,这样的算法节省了程序执行时间,体现了超标量的思想。

编程实现

为了让实验现象明显,本次实验的待累加数组长度设置为1e8,并且循环50次求和操作。

平凡算法

        int t=50;                 //循环次数
        while(t--){
         for(int i=0;i<MAXN;i++)
                 sum+=a[i];
           }

超标量优化

  • 两路链式算法

         while(t--){
         for(int i=0;i<MAXN;i+=2)
            {
                sum1+=a[i];
                sum2+=a[i+1];
            }
        }
        sum=sum1+sum2;
    

性能测试

用计算程序执行时间来衡量程序的性能,经过多次运行程序测试后得到程序的执行时间结果如下表:

12345678910平均时间
平凡算法1.5471.5541.5651.5861.5591.5631.5641.5651.5581.5671.5628
两路链式算法1.2301.2141.2131.2171.2161.2321.2091.2111.2341.2151.2191
性能测试结果 (单位:s)

可见进行了超标量优化后,程序性能变为原先的1.28倍,而且可以预计随着数据规模的提升这样的提升会更加明显,更改数据规模后得到结果如下:

数据规模平凡算法两路链式算法优化倍率
1e50.0140.0131.07
1e60.0260.0221.18
1e70.1690.1351.25
不同数据规模下两种算法性能对比

可见,在进行超标量优化后,程序性能得到提升,而且随着数据规模的扩大,优化带来的性能提升越来越明显。

在这里插入图片描述

Profilling

运用AMD的CPU Profilling工具AMD uProf分析优化前后程序后得到结果如下:

CYCLES_NOT_IN_HALTRETIRED_INSTCPI
平凡算法524775000058487500000.50
两路链式算法215875000056297500000.38
1e8*50规模输入下CPU Profilling结果

CYCLES_NOT_IN_HALTRETIRED_INSTCPI
平凡算法2335000004767500000.42
两路链式算法1780000004190000000.37
1e7*50规模输入下CPU Profilling结果

CYCLES_NOT_IN_HALTRETIRED_INSTCPI
平凡算法37500000802500000.53
两路链式算法32750000615000000.47
1e6*50规模输入下CPU Profilling结果

CYCLES_NOT_IN_HALTRETIRED_INSTCPI
平凡算法850000137500000.55
两路链式算法675000115000000.49
1e5*50规模输入下CPU Profilling结果

可见在不同规模下的优化前后程序的总指令数(RETIRED_INST)几乎相等,由于AMD的Profilling工具会将一些系统内核程序的指令数等一同统计,因此会有微小差别;而在CPU的循环数(CYCLES_NOT_IN_HALT)上,可以发现随着数据输入规模增大二者差距愈发变大,当数据规模为1e8 * 50时,优化后的两路链式算法在循环数上仅有原先的41%,大大减少了循环的次数;从CPI方面来看,优化后的算法CPI表现优于平凡算法,在1e8 * 50数据规模下优化后0.38的CPI显著优于优化前的0.50,可见程序性能得以提升,超标量优化切实有效。

结果分析

从程序算法的角度来看,平凡算法依次累加数组元素,要进行MAX次循环才能得到结果,这样的串行算法仅仅利用了CPU的一条工作线,效率并不高,尤其是当数据规模很大时,执行时间也会更加漫长,没能较好地利用CPU资源;而优化后的两路链接算法一步执行两次累加操作,只用 M A X 2 \frac{MAX}{2} 2MAX次循环就能得到结果,对比普通的链式算法,两路链式算法能更好地利用
CPU超标量架构,两条求和的链可令两条流水线充分地并发运行指令。因此两路链接算法表现会更加优秀,以此类推,还可以增加至三路、四路链接算法,增加流水线数从而进一步提升性能。

而从实验结果来讲,当输入数据规模不大时二者无论是执行时间还是CPU循环数指令数等都差别不大,这是由于CPU本身具有强大的运算能力,因此面对不大的数据输入时体现出来的差别也不大;而当数据规模增大后,无论从运行时间还是Profilling后得到的CPU循环数、CPI等指标都明显表现出优化后的算法更高效,说明此次实验采用的优化算法是合理有效的。

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在PyTorch中,内存管理的主要工具是torch.cuda.empty_cache()和torch.autograd.profiler.profile()。 1. torch.cuda.empty_cache():这个函数会清除PyTorch张量缓存中的所有未被引用的缓存。这些缓存可能会导致内存占用过高,因此可以使用该函数来释放这些缓存。 2. torch.autograd.profiler.profile():这个函数可以用来分析PyTorch代码的运行时间和内存使用情况。它可以帮助您找出哪些部分的代码使用了大量内存,以便进行优化。 以下是使用这些工具来优化内存使用的示例代码: ```python import torch import torch.autograd.profiler as profiler # 定义一个简单的神经网络 class Net(torch.nn.Module): def __init__(self): super(Net, self).__init__() self.fc1 = torch.nn.Linear(10, 20) self.fc2 = torch.nn.Linear(20, 1) def forward(self, x): x = self.fc1(x) x = torch.nn.functional.relu(x) x = self.fc2(x) return x # 创建一个模型实例和一些随机输入数据 model = Net() inputs = torch.randn(1000, 10) # 使用torch.autograd.profiler.profile()来分析模型的内存使用情况 with profiler.profile(record_shapes=True) as prof: outputs = model(inputs) # 打印分析结果 print(prof.key_averages().table(sort_by="self_cpu_memory_usage", row_limit=10)) # 清除PyTorch张量缓存中的所有未被引用的缓存 torch.cuda.empty_cache() ``` 在上面的示例中,我们定义了一个简单的神经网络,并使用torch.autograd.profiler.profile()来分析其内存使用情况。然后,我们打印了使用内存最多的前10个操作的列表。最后,我们使用torch.cuda.empty_cache()来清除PyTorch张量缓存中的所有未被引用的缓存,以释放内存。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值