访存优化_1、带宽、缓存局域性

 通常来说,并行只能加速计算的部分,不能加速内存读写的部分

因此,对 fill 这种没有任何计算量,纯粹只有访存循环体,并行没有加速效果。称为内存瓶颈(memory-bound)。

而 sine 这种内部需要泰勒展开来计算,每次迭代计算量很大的循环体,并行才有较好的加速效果。称为计算瓶颈(cpu-bound)。

并行能减轻计算瓶颈,但不减轻内存瓶颈,故后者是优化的重点。

那我们给他每个加上1是不是就能变快了?

可以看到并不是这样的 

因为一次浮点加法的计算量访存的超高延迟相比实在太少了。

计算太简单,数据量又大,并行只带来了多线程调度的额外开销。

小彭老师的经验公式:1次浮点读写 ≈ 8次浮点加法

如果矢量化成功(SSE):1次浮点读写 ≈ 32次浮点加法

如果CPU4核且矢量化成功:1次浮点读写 ≈ 128次浮点加法

所以就要足够的计算量来隐藏操作的延迟。

什么是超线程技术:不一定是两个线程在两个核上同时运行,而是有可能两个线程同时运行在一个核上,由硬件自动来调度。这个主要是针对如果内存卡住,cpu会自动切换到另一个核上。

内存条是并行的,所以2*8>1*16

CPU中的高速缓存:

CPU的厂商早就意识到了内存延迟高,读写效率低下的问题。因此他们在CPU内部引入了一片极小的存储器——虽然小,但是读写速度却特别快。这片小而快的存储器称为缓存(cache)。

当CPU访问某个地址时,会先查找缓存中是否有对应的数据。如果没有,则从内存中读取,并存储到缓存中;如果有,则直接使用缓存中的数据。

这样一来,访问的数据量比较小时,就可以自动预先加载到这个更高效的缓存里,然后再开始做运算,从而避免从外部内存读写的超高延迟。

 缓存的分级结构:

 L1,L2缓存是只给自己核心用的,而L3比较大,可以给多个核心使用。

L1缓存分为数据缓存指令缓存

 也可以看到刚刚两个出现转折的点,也是在二级缓存和三级缓存的大小附近。

因此,数据小到装的进二级缓存,则最大带宽就取决于二级缓存的带宽。稍微大一点则只能装到三级缓存,就取决于三级缓存的带宽。三级缓存也装不下,那就取决于主内存的带宽了。

结论:要避免mem-bound,数据量尽量足够小,如果能装的进缓存就高效了。

缓存的工作机制:读:

通俗点来讲,当CPU想要读取一个地址的时候, 就会跟缓存说,我要读取这个地址,缓存就去查找,看看这个地址有没有存储过,要是存储过就直接将缓存里存的数据返回给CPU,要是没找到,就像下一级缓存下令,让它去读,如果三级缓存也读不到,三级缓存就会向主内存发送请求,就会创建一个新条目,这样下一次再寻找这个数据的时候就不用再去主内存里读取了。

在X86架构中,这个条目的大小是64字节。比如当访问 0x0048~0x0050 4 个字节时,实际会导致 0x0040~0x0080 64 字节数据整个被读取到缓存中。

 缓存的工作机制:写:

同样的,当CPU写入一个数组时,缓存会查找与该地址匹配的条目,如果找到,那就修改数据,如果没有找到,就创建一个新条目,并且标记为dirty。

当读和写创建的新条目过多,缓存快要塞不下时,他会把最不常用的那个条目移除,这个现象称为失效(invalid)。如果那个条目时刚刚读的时候创建的,那没问题可以删,但是如果那个条目是被标记为脏的,则说明是当时打算写入的数据,那就麻烦了,需要向主内存发送写入请求,等他写入成功,才能安全移除这个条目。

如有多级缓存,则一级缓存失效后会丢给二级缓存。二级再丢给三级,三级最后丢给主内存。

连续访问与跨步访问:

#include <iostream>
#include <vector>
#include <cmath>
#include <cstring>
#include <cstdlib>
#include <array>
#include <benchmark/benchmark.h>
#include <omp.h>

constexpr size_t n = 1 << 28;
std::vector<float> a(n);  

void BM_skip1(benchmark::State& bm) {
    for (auto _ : bm) {
#pragma omp parallel for
        for (size_t i = 0; i < n; i += 1) {
            a[i] = 1;
        }
        benchmark::DoNotOptimize(a);
    }
}
BENCHMARK(BM_skip1);

void BM_skip2(benchmark::State& bm) {
    for (auto _ : bm) {
#pragma omp parallel for
        for (size_t i = 0; i < n; i += 2) {
            a[i] = 1;
        }
        benchmark::DoNotOptimize(a);
    }
}
BENCHMARK(BM_skip2);

void BM_skip4(benchmark::State& bm) {
    for (auto _ : bm) {
#pragma omp parallel for
        for (size_t i = 0; i < n; i += 4) {
            a[i] = 1;
        }
        benchmark::DoNotOptimize(a);
    }
}
BENCHMARK(BM_skip4);

void BM_skip8(benchmark::State& bm) {
    for (auto _ : bm) {
#pragma omp parallel for
        for (size_t i = 0; i < n; i += 8) {
            a[i] = 1;
        }
        benchmark::DoNotOptimize(a);
    }
}
BENCHMARK(BM_skip8);

void BM_skip16(benchmark::State& bm) {
    for (auto _ : bm) {
#pragma omp parallel for
        for (size_t i = 0; i < n; i += 16) {
            a[i] = 1;
        }
        benchmark::DoNotOptimize(a);
    }
}
BENCHMARK(BM_skip16);

void BM_skip32(benchmark::State& bm) {
    for (auto _ : bm) {
#pragma omp parallel for
        for (size_t i = 0; i < n; i += 32) {
            a[i] = 1;
        }
        benchmark::DoNotOptimize(a);
    }
}
BENCHMARK(BM_skip32);

void BM_skip64(benchmark::State& bm) {
    for (auto _ : bm) {
#pragma omp parallel for
        for (size_t i = 0; i < n; i += 64) {
            a[i] = 1;
        }
        benchmark::DoNotOptimize(a);
    }
}
BENCHMARK(BM_skip64);

void BM_skip128(benchmark::State& bm) {
    for (auto _ : bm) {
#pragma omp parallel for
        for (size_t i = 0; i < n; i += 128) {
            a[i] = 1;
        }
        benchmark::DoNotOptimize(a);
    }
}
BENCHMARK(BM_skip128);

BENCHMARK_MAIN();

 如果访问数组时,按一定的间距跨步访问,则效率如何?

从116都是一样快的,32开始才按2的倍率变慢,为什么?

因为CPU和内存之间隔着缓存,而缓存和内存之间传输数据的最小单位是缓存行(64字节)。16float64字节,所以小于64字节的跨步访问,都会导致数据全部被读取出来。而超过64字节的跨步,则中间的缓存行没有被读取,从而变快了。

   (上面是原作者在课堂上所教授的内容,下图是我实际操作的结果,可以看到CPU已经对跨步访问有了足够的优化,步长增加一倍,对应时间也缩小一倍)

 所以我们设计数据结构时,应该把数据存储的尽可能紧凑,不要松散排列。最好每个缓存行里要么有数据,要么没数据,避免读取缓存行时浪费一部分空间没用。

重新认识一下结构体:

MyClass在内存中所占的是12个字节

AOS:不方便连续访问x元素

SOA:

 如果每个属性都要访问到,那还是AOS比较好

这是因为使用SOA会让CPU不得不同时维护很多条预取赛道(mc_x, mc_y, mc_z),当赛道多了以后每一条赛道的长度就变短了,从而能够周转的余地时间比较少,不利于延迟隐藏。

而如果把这三条赛道合并成一条(mc),这样同样的经费(缓存容量)能铺出的赛道(预取)就更长,从而CPU有更长的周转时间来隐藏他内部计算的延迟。所以本案例中AOSSOA

结论:

如果几个属性几乎总是同时一起用的,比如位置矢量posxyz分量,可能都是同时读取同时修改的,这时用AOS,减轻预取压力。

如果几个属性有时只用到其中几个,不一定同时写入,比如posvel,通常的情况都是pos+=vel,也就是pos是读写,vel是只读,那这时候就用SOA比较好,省内存带宽。

不过“posxyz分量用AOS这个结论,是单从内存访问效率来看的,需要SIMD矢量化的话可能还是要SOAAOSOA,比如hw04那种的。而“posvel应该用SOA分开存”是没问题的。

SOA大多数情况下不亏。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
能的提高一直是计算机研究人员孜孜不倦追求的目标。随着大规模集成电 路的发展,处理器的计算能力飞速提高。计算机能提高的瓶颈由计算转变为 储。能是程序访特征和特定的储结构共同作用的结果。程序访特征 的研究一直伴随着处理器的发展,为储结构提供发展方向。 针对Cache结构不区分程序数据和主物理实现一维连续的特征,本文选取 具有典型访特征的应用程序,通过分析访特征给出合理的优化方案: 1) 在共享Cache储体系结构中,利用Simics+GEMS体系结构模拟器,分析 基于PostgreSQL数据库在线事务处理不同数据集的访特征,建立数据分类模型, 将数据集划分为放弃型、保护型和自由竞争型三类;然后提出一种软件协同的半 透明共享Cache结构区分对待三类数据集,实验结果证明Cache失效率最高下降 率为12%。 2) 针对矩阵行列访问二维连续的特征和DRAM储一维连续的特,提出一 种针对行列交替访问的优化方案——窗口访问,并证明了最优窗口原理,利用可 重构实验平台实现窗口访储控制器,实验证明矩阵行列交替访问的能 提高可达73.6%,一维FFT并行算法能可提高45.1%。 3) 基于窗口访问原理,指导CPU和GPU上矩阵数据的布优化矩阵行列 交替访问程序,实验证明CPU中矩阵行列交替访问的能最大提高58.4%, 并实现窗口访问和FFTW结合的一维FFT并行算法,与FFTW相比计算能提高 可达7%;分析CUDA编程框架和相应GPU的结构特点,将窗口原理应用到GPU 中以warp为单位的访过程上,实验表明矩阵行列交替访问的能提高了1 倍。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值