缓存的 Effect

翻译还待校正。。。

cache 是一种快速而小的缓存设备,cache存储了最近访问的memory数据。 这种描述是相当准确的,但是如果了解 cache 工作的细节将对改善程序的性能有很大的帮助。

?在这篇博客,我将通过代码示例来说明缓存是如何工作的 和 cache是如何影响程序性能的。

?例子用 C# 描述,这并不影响分析和结论。 原文地址Gallery of Processor Cache Effects

一、内存访问和性能

如下一段代码,相比于 Loop 1,?你觉得 Loop 2 运行能有多快?

int[] arr = new int[64 * 1024 * 1024];

// Loop 1
for (int i = 0; i < arr.Length; i++)
    arr[i] *= 3;

// Loop 2
for (int i = 0; i < arr.Length; i += 16)
    arr[i] *= 3;

很显然,Loop 2 的迭代次数大概是 Loop 1 迭代次数的 6% 。但是实验结果显示,两个for循环花费的时间分别︰ 80 和 78 ms。

?这种现象的原因是什么?两个循环的运行时间,主要在于对数组的memory访问,而不是整数乘法。 总之, IO 是费时的。

实际上,Loop 1 和 Loop 2 执行了相同的memory访问。

二、缓存行的影响

如下一段代码,我们改变for循环的增长步长。

for (int i = 0; i < arr.Length; i += K)
    arr[i] *= 3;

以下是不同步长值 (K) 时,循环的运行时间:

?从上图可以看出,步长在1 到 16 范围内时,for 循环的运行时间几乎没有变化。但是从 16 起,每当我们增加一倍步长,运行时间减半。

?出现上图现象的原因是,CPU 并不是一个字节一个字节的访问内存。相反,他们每次访存操作都将获取一个内存块chunk(通常为64bytes),称为 缓存行( cache line )。所以,当你读取一个特定的内存地址时,从这个内存地址开始的整个 cache line 都将从 memory 加载到 cache 。这样的话,如果下一次要访问的数据正好在该 cache line 中的话,那么将不会产生额外的访问开销(cache 命中了,就不用去memory中获取)!

?由于 16 个 int 刚好占用 64 个字节(cache line 大小),所以for循环在每 1~16 次迭代之间要处理的数据刚好位于同一个cache line中。但是一旦步长变为32,上述循环的memory访问次数就变成了 arr.Length / 32 , 之前是arr.Length / 16。

理解cache line对于优化程序有很大的帮助。例如,数据的对齐方式将会影响一次操作中的数据是否位于不同的cache line中。根据上述的示例,我们可以发现,在未对齐的情况下,该操作将慢一倍。

小结: 每次读内存,都是读取一个cache line大小,而非单个字节。

三、L1 & L2 cache sizes

?现在的计算机都有 2级或者3级 的缓存,通常被称为 L1、 L2 和 L3。如果你想要知道不同level 缓存的大小,你可以使用 CoreInfo SysInternals工具,或者使用 GetLogicalProcessorInfoWindows API 调用。这两种方法都能获得高速缓存 cache 和 cache line 的大小。

?在我的机器,CoreInfo 报告有 32 kB L1 数据缓存(data cache)、 32kb L1 指令缓存(instruction cache)和 4 MB L2 数据缓存。其中 L1 cache是每个core私有的,L2 cache是共享的︰

Logical Processor to Cache Map:
*---  Data Cache          0, Level 1,   32 KB, Assoc   8, LineSize  64
*---  Instruction Cache   0, Level 1,   32 KB, Assoc   8, LineSize  64
-*--  Data Cache          1, Level 1,   32 KB, Assoc   8, LineSize  64
-*--  Instruction Cache   1, Level 1,   32 KB, Assoc   8, LineSize  64
**--  Unified Cache       0, Level 2,    4 MB, Assoc  16, LineSize  64
--*-  Data Cache          2, Level 1,   32 KB, Assoc   8, LineSize  64
--*-  Instruction Cache   2, Level 1,   32 KB, Assoc   8, LineSize  64
---*  Data Cache          3, Level 1,   32 KB, Assoc   8, LineSize  64
---*  Instruction Cache   3, Level 1,   32 KB, Assoc   8, LineSize  64
--**  Unified Cache       1, Level 2,    4 MB, Assoc  16, LineSize  64

让我们通过实验来验证这些数字。如下一段代码,我们从下标0开始,然后每隔16个位置访问数组元素。当下标增长到最后一个值时,循环又回到了起点。通过改变数组的大小,我们会发现当数组大小超过了cache size时,性能开始下降。

int steps = 64 * 1024 * 1024; // Arbitrary number of steps
int lengthMod = arr.Length - 1;
for (int i = 0; i < steps; i++)
{
    arr[(i * 16) & lengthMod]++; // (x & lengthMod) is equal to (x % arr.Length)
}

结果如下:

从上图可以看出,在数组大小达到 32kb 和 4MB之后,性能开始下降。 —— 32kb 对应L1 data cache的大小 , 4MB对应L2 cache的大小。

小结:L1 和 L2 的容量是有限的,当L1 被“填满时”,再有想填入L1的 cache line,就得考虑 evcit 其他已经在L1中的 cache line。

四、指令级并行

现在,让我们看看一些不同的东西。以下两个循环,你希望哪一个更快?

int steps = 256 * 1024 * 1024;

int[] a = new int[2];

// Loop 1
for (int i=0; i<steps; i++)
{
    a[0]++;
    a[0]++;
}

// Loop 2
for (int i=0; i<steps; i++)
{
    a[0]++;
    a[1]++;
}

结果显示,Loop2比Loop1快2倍,至少在我所使用的机器上是这样。为什么呢?我们来看看它的执行过程:

在Loop1中,整个执行过程如下:

在Loop2中,整个执行过程如下:

?

这是为什么?

随着现代处理器的发展,CPU的计算能力有了很大的改观,特别是在并行度方面。现在的单个 CPU core,它可以在同一时刻,访问两个不同内存位置的 L1 cache 或 执行两个简单的算术运算 ( 指令 )。在Loop 1中,由于访问相同的内存位置,CPU 不能利用指令级并行性,但在第二个循环中,它可以。

批注:

指令级并行,是指单个 CPU core 能同时执行多条指令。

一般而言,如果程序中相邻的一组指令是相互独立的,即不竞争同一个功能部件、不相互等待对方的运算结果不访问同一个存储单元,那么它们就可以在处理器内部并行地执行。总之一句话就是,互不干扰 的指令才能并行执行。

指令级并行依赖的CPU技术有 流水线 、多指令发射、超标量、乱序执行 和 超长指令字。

附议: Reddit上有很多人都好奇编译器优化的问题。对于{a[0] + +; a[0] + +;},是否会优化为{[0] = 2;}。而实际上,C#编译器和CLR JIT并不会做这种优化。

五、Cache associativity

思考一下: memory 里的任何一个 chunk 是否都可以映射到任何一个 cache slot 中,还是只有一部分可映射。 memory chunk 和 cache slot 大小一样,都是 64 bytes(与CPU架构相关,目前大部分处理器都是64bytes)。

cache slot 与 memory chunk 是如何建立映射关系的?有以下三种可能的方案:

  • 1. Direct mapped cache

    每一个 memory chunk 只能映射到一个特定的 cache slot。

    具体做法,通过 chunk_index % cache_slots 求出每一个 memory chunk 对应的 cache slot。显然,这是一种“多对一”的关系。而隐含的问题是,隐射到同一个cache slot的两个memory chunk是无法同时加载到同一cache中的(你要进来,它就得出去)。

    -> 每个 memory chunk 在 cache 中只有一个候选位置。

  • 2. N-way set associative cache

    每个memory chunk 可以映射到N个特定cache slot中的任何一个(简称N-way cache)。比如说,一个16-way cache,表示每个memory chunk可以映射到16个不同的cache slot。

    -> 每个memory chunk在cache中有多个候选位置。

  • 3. Fully associative cache

    每个memory chunk可以映射到任何一个cache slot中。cache的管理可以像hash table一样高效。

Direct mapped cache 方案很容易产生冲突 —— 当多个value竞争同一cache slot时,将因为冲突而持续的互相evict,从而导致cache命中率下降。Fully associative cache 方案是复杂的,而且实现起来非常昂贵。N-way set associative caches 方案是典型的cache管理方案,这种方案在简单的实现和命中率之间做了 trade off。

例如,我机器上的 4MB L2 cache 是16-way。所有的 64-byte memory chunk 可划分成多个set,位于同一个set的memory chunk将会竞争16个候选cache slot。

?为了使 Cache associativity 的影响变得明显,我需要在同样的set中重复访问超过16个元素。如下一段代码:

public static long UpdateEveryKthByte(byte[] arr, int K)
{
    Stopwatch sw = Stopwatch.StartNew();
    const int rep = 1024*1024; // Number of iterations – arbitrary
    int p = 0;
    for (int i = 0; i < rep; i++)
    {
        arr[p]++;
        p += K;
        if (p >= arr.Length)
            p = 0;
    }
    sw.Stop();
    return sw.ElapsedMilliseconds;
}

如上代码,按照步长K,依次访问数组arr[]。一旦到达数组尾,又从头开始。直到循环迭代 2^20 次之后。

通过传入不同大小的数组(以1MB为增量) 和 不同的步长,我们调用 UpdateEveryKthByte() 。下图是运行结果。

TODO

六、Cache line 假共享

在多核CPU上,cache 还会面临一个问题 —— cache 一致性。

现在大多数机器都是3级cache,其中L1 和 L2 为每个core私有的,L3是共享的。在此,为了方便说明问题,我们假设只有2级cache,其中L1私有,L2共享。

如下图,在memory中有一个 val==1 的变量,在多线程情况下,假设有2个分别运行在 core 1 和 core 2 上的线程(假设分别为 t1 和 t2),并且各自私有的 L1 cache 分别缓存了一份该变量。

如果此时,t1线程修改 val 为 2,那么很显然,t2 和 memory 中的 val 此时都将变为无效。

这就出现了 cache 不一致的问题。此时要做的就是,同步 memory 和 core 2 的 L1 cache 中的 val 值。(如何解决cache一致性问题不在此篇blog的讨论范围)。

我们通过下面一段代码,来看看 cache一致性 会造成多大的开销?

private static int[] s_counter = new int[1024];
private void UpdateCounter(int position)
{
    for (int j = 0; j < 100000000; j++)
    {
        s_counter[position] = s_counter[position] + 3;
    }
}
  1. 在一台4-core机器上做实验,如果在4个不同的线程中分别传入参数0,1,2,3,那么4个线程全部执行完将花费4.3s。
  2. 如果传入的参数分别是 16,32,48,64,那么4个线程全部执行完只需要0.28s。

    原因是什么呢?在第一个例子中,4个线程处理的数据很可能会是在同一个cache line中,那么每一次更新数组元素值的时候,就有可能会导致缓存了同样数据的 cache 出现 cache 不一致(失效)的情况。

七、硬件多样性

纵然了解了 cache 的一些特性,但是硬件之间仍然有许多差异。不同的 cpu,适应不同的优化方式。

对于某一些处理器,能够并行的访问位于不同 cache bank 中的非连续/连续cache line,和位于同一 cache bank 中的连续cache line。

如下一段代码:

private static int A, B, C, D, E, F, G;
private static void Weirdness()
{
    for (int i = 0; i < 200000000; i++)
    {
        <something>
    }
}

<something>中分别填入以下3条语句,结果如下:

<something>         Time
A++; B++; C++; D++; 719 ms
A++; C++; E++; G++; 448 ms
A++; C++;           518 ms

出现上述情况的原因是什么?很好奇,但是我也不清楚。如果有人能够清楚分析,我也很乐意学习。

总之,各大商家提供的硬件各有差异,硬件自有其复杂性。

时间: 2024-11-05 12:25:34

缓存的 Effect的相关文章

SpringMVC+mybatis+maven+Ehcache缓存实现

所谓缓存,就是将程序或系统经常要调用的对象存在内存中,以便其使用时可以快速调用,不必再去创建新的重复的实例.这样做可以减少系统开销,提高系统效率. 缓存主要可分为二大类: 一.通过文件缓存,顾名思义文件缓存是指把数据存储在磁盘上,不管你是以XML格式,序列化文件DAT格式还是其它文件格式: 二.内存缓存,也就是实现一个类中静态Map,对这个Map进行常规的增删查. 一.EhCache缓存系统简介 EhCache 是一个纯 Java 的进程内缓存框架,具有快速.精干等特点,是 Hibernate

LDAP缓存命令

启动cacao及实例: [[email protected] bin]# cd /home/ldap/iamldap/dsee6/cacao_2/cacao/bin [[email protected] bin]# ./cacaoadm start [[email protected] bin]# cd /home/ldap/iamldap/ds6/bin/ [[email protected] bin]# ./dsadm start /home/ldap/iamldap/var/dscc6/d

HDFS缓存机制

前言 缓存,英文单词译为Cache,缓存可以帮助我们干很多事,当然最直接的体会就是可以减少不必要的数据请求和操作.同样在HDFS中,也存在着一套完整的缓存机制,但可能使用了解此机制的人并不多,因为这个配置项平时大家比较少用而且HDFS中默认是关闭此功能的.至于是哪个配置项呢,在后面的描述中将会给出详细的分析. HDFS缓存疑问点 为什么在这里会抛出这样一个问题呢,因为本人在了解完HDFS的Cache整体机理之后,确实感觉到其中的逻辑有点绕,直接分析不见得会起到很好的效果,所以先采取提问的形式来做

缓存系列之三:redis安装及基本数据类型命令使用

一:Redis是一个开源的key-value存储系统.与Memcached类似,Redis将大部分数据存储在内存中,支持的数据类型包括:字符串.哈希表.链表.集合.有序集合以及基于这些数据类型的相关操作.Redis使用C语言开发,在大多数像Linux.BSD和Solaris等POSIX系统上无需任何外部依赖就可以使用.Redis支持的客户端语言也非常丰富,常用的计算机语言如C.C#.C++.Object-C.PHP.Python. Java.Perl.Lua.Erlang等均有可用的客户端来访问

缓存、队列(Memcached、redis、RabbitMQ)

Memcached 简介.安装.使用 Python 操作 Memcached 天生支持集群 redis 简介.安装.使用.实例 Python 操作 Redis String.Hash.List.Set.Sort Set 操作 管道 发布订阅 RabbitMQ 简介.安装.使用 使用 API 操作 RabbitMQ 消息不丢失 发布订阅 关键字发送 模糊匹配 一.Memcached 1.简介.安装.使用 Memcached 是一个高性能的分布式内存对象缓存系统,用于动态 Web 应用以减轻数据库负

Memcached缓存瓶颈分析

Memcached缓存瓶颈分析 获取Memcached的统计信息 Shell: # echo "stats" | nc 127.0.0.1 11211 PHP: $mc = new Memcached(); $mc->addServer('127.0.0.1',11211); $stats = $mc->getStats(); Memcached缓存瓶颈分析的一些指标 Posted in Memcached, Performance analysis & tunin

10.1.2 使用记忆化缓存结果

记忆化(Memoization),可以描述为缓存函数调用的结果,听起来可能有点复杂,但是,技术非常简单.正如我们前面提到的那样,在函数式编程中,大多数函数是没有副作用的,因此,如果我们用相同的参数值,两次调用同一个函数,得到的结果相同. 如果我们要得到与上一次相同的结果,为什么还要麻烦去再一次执行函数呢?相反,我们可以缓存这个结果.如果我们把第一次调用的结果,以字典方式保存起来,第二次调用时,就不必重新计算值了:可以从字典中读取结果,马上返回.清单 10.3 显示了一个函数,计算两个整数的和.

squid 代理缓存服务器

Squid cache(简称为Squid)是一个流行的自由软件,它符合GNU通用公共许可证.Squid作为网页服务器的前置cache服务器,可以代理用户向web服务器请求数据并进行缓存,也可以用在局域网中,使局域网用户通过代理上网.Squid主要设计用于在Linux一类系统运行 代理服务器原理 代理服务器接受到请求后,首先与访问控制列表中的访问规则相对照,如果满足规则,则在缓存中查找是否存在需要的信息. 对于Web用户来说,Squid是一个高性能的代理缓存服务器,可以加快内部网浏览Interne

HDFS中心缓存管理

前言 众所周知,HDFS作为一个分布式文件系统.存储着海量的数据,每天的IO读写操作次数当然是非常高的.所以在之前的文章中,我们提到了用HDFS的异构存储来做冷热数据的分类存储,但比较好的一点是,他们还是隶属于同一个集群.那么问题来了,是否我还可以做进一步的改进,优化呢,因为有的数据文件访问在某个时间段是大家公用的,访问频率甚至比一般的热点文件还要高很多.但是过了那个时间点,就又会变为普通的文件.本文就来分享HDFS对于这一需求点的解决方案,HDFS中心缓存管理.这一方面的功能属性,可能也被很多