分享

CPU cache学习

 醉雨情 2014-09-29

在2004年写的一篇文章x86汇编语言学习手记(1)中,曾经涉及到gcc编译的代码默认16字节栈对齐的问题。之所以这样做,主要是性能优化方面的考虑。

  大多数现代cpu都one-die了l1和l2cache。对于l1 cache,大多是write though的;l2 cache则是write back的,不会立即写回memory,这就会导致cache和memory的内容的不一致;另外,对于mp(multi processors)的环境,由于cache是cpu私有的,不同cpu的cache的内容也存在不一致的问题,因此很多mp的的计算架构,不论是ccnuma还是smp都实现了cache coherence的机制,即不同cpu的cache一致性机制。

  cache coherence的一种实现是通过cache-snooping协议,每个cpu通过对bus的snoop实现对其它cpu读写cache的监控:

  首先,cache line是cache和memory之间数据传输的最小单元。

  1. 当cpu1要写cache时,其它cpu就会检查自己cache中对应的cache line,如果是dirty的,就write back到memory,并且会将cpu1的相关cache line刷新;如果不是dirty的,就invalidate该cache line.

  2. 当cpu1要读cache时,其它cpu就会将自己cache中对应的cache line中标记为dirty的部分write back到memory,并且会将cpu1的相关cache line刷新。

  所以,提高cpu的cache hit rate,减少cache和memory之间的数据传输,将会提高系统的性能。

  因此,在程序和二进制对象的内存分配中保持cache line aligned就十分重要,如果不保证cache line对齐,出现多个cpu中并行运行的进程或者线程同时读写同一个cache line的情况的概率就会很大。这时cpu的cache和memory之间会反复出现write back和refresh情况,这种情形就叫做cache thrashing。

  为了有效的避免cache thrashing,通常有以下两种途径:

  1. 对于heap的分配,很多系统在malloc调用中实现了强制的alignment.
  2. 对于stack的分配,很多编译器提供了stack aligned的选项。

  当然,如果在编译器指定了stack aligned,程序的尺寸将会变大,会占用更多的内存。因此,这中间的取舍需要仔细考虑,下面是我在google上搜索到的一段讨论:

one of our customers complained about the additional code generated to
maintain the stack aligned to 16-byte boundaries, and suggested us to
default to the minimum alignment when optimizing for code size. this
has the caveat that, when you link code optimized for size with code
optimized for speed, if a function optimized for size calls a
performance-critical function with the stack misaligned, the
performance-critical function may perform poorly.



http://blog./space.php?uid=20586993&do=blog&id=130624


计算机中cache的概念无处不在,buffer大多是指软件缓存,而cache多指硬件缓存,我把buffer和cache理解为同一个概念。

经过几天的学习,对cpu cache有点了解,把学习过程记录下来。

 

cache位于cpu和存储器之间

CPU -fast-> CACHE -slow-> MEMORY

每当cpu执行的指令需要访问存储器时,给出物理地址在地址总线上,cache控制器会根据这个地址判断是否访问的地址中的内容已经在cache中(方式如下),如果存在那么直接把内容传递给cpu,否则从memory中读取CACHE_LINE_SIZE字节大小的数据,这个读取比较慢,具体读写可以优化,选择一个cache中的line放置读取的内容,具体替换的算法不说了。写存储器原理是一样的。
 
那么怎么根据地址判断是否在cache中呢?以我的机器为例子,奔腾3.0G,memory为512M,也就是0x20000000(2^29),cache的大小为1M(/proc/cpuinfo),CACHE_LINE_SIZE为128字节(1<<7)。
所以cache line的个数为2^20/2^7 == 2^13 == 8096。
而memory又有多少个cacheline大小的块呢?2^29/2^7 == 2^22
 
很显然,并不是所有的内容都能放到cache中,1/512 的内容在cache中。 好了,可以说说为怎么判断是否在cache中了(cache命中)举例说明:

当访问256M+1K处的地址时,物理地址值为0x100,00400,看看低20位(由1Mcache计算出来的,所以是20不是21或18)为0000,0000,0100,0000,0000,这下好了,cache控制器知道,如果它在,那么只能在cache中的哪一块呢,01000(低7位不影响,只是偏移),所以硬件电路直接访问那一块,那么如果这个cache块有效,它是对应,0x100,00400(256M+1K),还是0x10,00400(16M+1K),还是其它的呢?对了,这下缓冲管理有个目录,里面,记录了一个称为tag的12bit的值,对于上述例子,如果这个值为0x100那么就表明这个块缓冲着256M+1K的内容了。否则(比如0x010)不是(cache缺失)。

显然,缺失了就要把这个块用真正的内容替换掉,如果必要把内容写回到memory 16M+1K,再读进256M+1K的 128字节到这个块中。接下来的传输到cpu和命中是一样的。这样如果接下来又要访问16M+1K的内容呢,再替换呗,再256M+1K呢,再换,再16M+1K,再换,这就是颠簸啦,可是谁会访问的地址高位(20以上)会变化如此的频繁呢,是的不会,这就是局部性原理,和颠簸是相反的概念。只要前面12位不变(在同一个M内),那么就不会颠簸了。

但是也有可能会有地址变动很大的可能,比如把一块数据从16M copy 到256M,偶尔还是很有可能的把,所以就有了N-WAY,就是说中间13为相同并不一定只是映射到同一个cache line中,可以映射到N个块中,比如N == 2,那么当映射完16M+1K时要映射256M+1K,那么会发现,还有个槽可以映射,那么就不会颠簸了,同理,增大N可以减小更多的颠簸,比如需要检查16M+1k和15M+1K(同样的中间13位),是否相等,再决定是否要copy到256M+1K中。 这下N == 2,还是会颠簸。但是N=4一般就足够了,偶尔不够的可能性就非常小了,不是吗,局部性原理。当然对于1M的cache,N>1之后,2*13次方个函数值就会减少到1/2^(N-1)了。不过,还是因为局部性原理,有什么关系呢。

上述并不一定正确,而且很多细节没有说到,但是网上很多说cache的文章(中文的不多),找个再看看,这里主要描述的主要是我对cache的地址怎么映射的理解,也没有说多级映射,我没太了解。看看下面的例子吧,跑一跑,我自己想出来的,^^,可以根据自己的处理器快慢设置times,时间差大概为50-100倍,因为fine==0时一直在颠簸。通过下面的例子,我相信应该能写出来一个程序,在处理器主频差不多但是cache不一样的机器上跑快慢相差50倍,为什么呢,让程序在小cache的机器上颠簸而大的不颠簸。比如操作的数据总和介于大小cache之间

 

 

#include <stdlib.h>
#define TIMES 50
char buf1[128][40960];
char buf2[128][40960];
int main(int argc,char *argv[])
{
    int i,j;
    int times = 0;
    int fine = 1;
    if (argc == 2)
        fine = atoi(argv[1]);
    if(fine) {
        while(times++ < TIMES)
            for(i=0;i<sizeof(buf1)/sizeof(buf1[0]);++i)
                for(j=0;j<sizeof(buf1[0]);++j) {
                    buf1[i][j] = buf2[i][j];
                }
    } else {
        while(times++ < TIMES)
            for(j=0;j<sizeof(buf1[0]);++j)
                for(i=0;i<sizeof(buf1)/sizeof(buf1[0]);++i) {
                    buf1[i][j] = buf2[i][j];
                }
    }
    return 0;
}


http://www./embed/embed_63834.html



文档内容简介:
  这篇文章主要是探讨现在的 CPU 的 cache 和内存系统之间的关系。

目录:
  [Part 1]
  [Part 2]

文档内容:

[Part 1]
  CPU 速度的进展,一直比内存的速度进展要来得快。在 IBM PC XT 的时代,CPU 和内存的速度是差不多的。不过,后来 CPU 的速度就愈来愈快。再加上 DRAM 需要 refresh 才能保存数据的特性,DRAM 很快就跟不上 CPU 的速度了。现在的 CPU 都利用了 pipeline 的方式,可以每个 cycle 都 issue 一个(甚至多个)指令,再加上现在的 CPU 频率也比内存的频率高,内存的速度可说是远远落在 CPU 之后了。

  为了避免内存成为 CPU 速度的瓶颈,现在的 CPU 都有 cache 的设计,甚至还有多层的 cache。Cache 的原理,主要是利用到大部分的程序,在处理数据时,都有一定程度的区域性。所以,我们可以用一小块快速的内存,来暂存目前需要的数据。

  例如,几乎所有的程序,大部分的执行时间是花在一些循环中。这些循环通常都不大,可能只占整个程序空间的百分之一。如果一个程序经常要执行这段程序数千、甚至数万次,那就可以把这一小段程序放在 cache 中,CPU 就不需要每次都到很慢的主存储器中读取这段程序了。很多一般用途的程序,在存取数据时,也有类似的特性。因此,cache 的帮助非常大。如果没有 cache 的话,我们就不需要这么快的 CPU 了,因为系统的速度会卡在内存的速度上面。

  现在的 CPU 往往也有多层的 cache。例如,Intel 的 Pentium III 500Mhz CPU,有 32KB 的 L1 cache,和 512KB 的 L2 cache。其中,L1 cache 内建在 CPU 内部,速度非常快,而且它是 Harvard 式,即指令用的空间和数据用的空间是分开的。Pentium III 500Mhz CPU 的 L1 cache 是分成 16KB 的 I-cache 和 16KB 的 D-cache。而 L2 cache 则是在 CPU 外面,以 250Mhz 的速度运作。另外,它和 CPU 之间的 bus 也只有 64 bits 宽。L2 cache 通常就不会区分指令和数据的空间,也就是 unified cache。

  Cache 对速度有什么影响呢?这可以由 latency 来表示。CPU 在从内存中读取数据(或程序)时,会需要等待一段时间,这段时间就是 latency,通常用 cycle 数表示。例如,一般来说,如果数据已经在 L1 cache 中,则 CPU 在读取数据时(这种情形称为 L1 cache hit),CPU 是不需要多等的。但是,如果数据不在 L1 cache 中(这种情形称为 L1 cache miss),则 CPU 就得到 L2 cache 去读取数据了。这种情形下,CPU 就需要等待一段时间。如果需要的数据也不在 L2 cache 中,也就是 L2 cache miss,那么 CPU 就得到主存储器中读取数据了(假设没有 L3 cache)。这时候,CPU 就得等待更长的时间。

  另外,cache 存取数据时,通常是分成很多小单位,称为 cache line。例如,Pentium III 的 cache line 长度是 32 bytes。也就是说,如果 CPU 要读取内存地址 0x00123456 的一个 32 bits word(即 4 bytes),且 cache 中没有这个资料,则 cache 会将 0x00123440 ~ 0x0012345F 之间的 32 bytes 数据(即一整个 cache line 长度)都读入 cache 中。所以,当 CPU 读取连续的内存地址时,数据都已经读到 cache 中了。

  我写了一个小程序,用来测试 cache 的行为。这个程序会连续读取一块内存地址,并量测平均读取时间。这个程序的执行结果如下:

测试平台:

  • Pentium III 500Mhz, PC100 SDRAM, 440BX chipset
  • Celeron 466Mhz, PC100 SDRAM, VIA Apollo Pro 133 chipset

Latency result

程序的执行文件和原始码可在这里下载。

  由上面的结果可以看出,当测试的区块大小在 16KB 以下时,平均的 latency 都在 1 ~ 3 cycles 左右。这显示出 16KB 的 L1 D-cache 的效果。在测试区块为 1KB 和 2KB 时,因为额外的 overhead 较高,所以平均的 latency 变得较高,但是在 4KB ~ 16KB 的测试中,latency 则相当稳定。在这个范围中,由于 Pentium III 和 Celeron 有相同的 L1 cache,所以测试结果是几乎完全相同的。

  在区块超过 16KB 之后,就没办法放入 L1 D-cache 中了。但是它还是可以放在 L2 cache 中。所以,在 Pentium III 的情形下,从 32KB ~ 512KB,latency 都在 10 cycles 左右。这显示出当 L1 cache miss 而 L2 cache hit 时,所需要的 latency。而 Celeron 的 L2 cache 只有 128KB,但是 Celeron 的 L2 cache 的 latency 则明显的比 Pentium III 为低。这是因为 Celeron 的 L2 cache 是 on-die,以和 CPU 核心相同的速度运作。而 Pentium III 的 L2 cache 则是分开的,且以 CPU 核心速度的一半运作。

  在区块超过 512KB 之后,L2 cache 就不够大了(Pentium III 500Mhz 只有 512KB 的 L2 cache)。这时,显示出来的就是 L1 cache miss 且 L2 cache miss 时,所需要的 latency。在 1024KB 或更大的区块中,Pentium III 的 latency 都大约是 28 cycles 左右,而 Celeron 的 latency 则超过 70 cycles。这是 CPU 读取主存储器时,平均的 latency。而 Celeron 的 latency 较高,应该是因为其外频较低,而倍频数较高的缘故(Pentium III 500Mhz 为 5 倍频,而 Celeron 466 为 7 倍频)。另外,芯片组的差异也可能是原因之一。

  Cache 的效果十分明显。不过,有时候 cache 是派不上用场的。例如,当数据完全没有区域性,或是数据量太大的时候,都会让 cache 的效果降低。例如,在进行 MPEG 压缩时,存取的数据量很大,而且数据的重复利用率很低,所以 cache 的帮助就不大。另外,像是 3D 游戏中,如果每个 frame 的三角面个数太多,也会超过 cache 能够处理的范围。

  现在的计算机愈来愈朝向「多媒体应用」,需要处理的资料量也愈来愈大,因此,要如何善用 cache 就成了一个重要的问题。一个非常重要的方法,就是把读取主存储器的 latency 和执行运算的时间重迭,就可以把 latency「藏」起来。通常这会需要 prefetch 的功能,也就是 AMD 在 K6-2 及之后的 CPU,和 Intel 在 Pentium III 之后的 CPU 加入的新功能。在下一篇文章中,我们会讨论 prefetch 的原理和用途。


[Part 2]
  在上一篇文章中,已经简单讨论过 CPU 的 cache 和其对 latency 的影响。在这篇文章中,我们就以一个较为实际的例子,并说明 prefetch 的原理和用途。

  这里要用的「实际例子」,其实还是很理想化的。为了和 3D 绘图扯上一点关系,这里就用「4x4 的矩阵和 4 维向量相乘」做为例子。不过,一般在 3D 绘图中,都是用 single precision 的浮点数(每个数需要 32 bits),而这里为了让内存的因素更明显,我们使用 double precision 的浮点数(每个数需要 64 bits),也就是一个 4 维向量刚好需要 32 bytes。

在这个例子中,我们采取一个 3D 绘图中,相当常见的动作,也就是把一大堆 4 维向量,乘上一个固定的 4x4 矩阵。如果向量的个数非常多,超过 CPU 的 cache 所能负担,那么 CPU 的表现就会大幅下降。

为了让大家心里有个底,这里先把执行的结果列出来:

测试平台: Pentium III 500Mhz, PC100 SDRAM, 440BX chipset

Vector test result

程序集可以下载程序的原始码和执行档。

  首先,我们来看没有使用 prefetch 指令的结果。事实上,结果相当符合预测。在 L1 D-cache 的范围内(即小于 16KB 的情形),平均的运算时间相当的稳定,约在 51 ~ 52 cycles 左右。这也是 Pentium III 在计算一个 4x4 矩阵和 4 维向量相乘时(使用 double precision 浮点数),可能达到的最快速度。当然,这个程序是用 C 写成的。如果直接用手写汇编语言,可能还可以再快个 5 ~ 10 cycles。

  当数据量超过 L1 D-cache 的范围,但是还在 L2 cache 的范围之内时,所需的时间提高到约 60 cycles 左右。在 Part 1 中,我们已经知道 Pentium III 500Mhz 的 L2 cache 大约有 10 cycles 的 latency,所以这个结果也是相当合理的。

  当资料量超过 L2 cache 的范围时,所有的数据就需要从主存储器中取得了。从图上可以很容易的看到,每次运算所需的时间增加到 145 ~ 150 cycles。这有点出乎意料之外:在 Part 1 中,读取主存储器的 latency 只有 30 cycles 左右,但是在这里,latency 增加了约 100 cycles。不过,这个结果并不奇怪。因为在运算结束后,运算的结果必须要写回内存中,而写回内存的动作,需要很多时间。

  从这里可以看到,在数据量超过 L2 cache 的范围时,CPU 可说是被内存的速度限制住了。事实上,如果内存的速度不变,那即使是用两倍快的 CPU,速度的增加也会非常有限。以 3D 游戏的角度来说,1024KB 或 2048KB 这样的数据量并不算少见,因为一个 single precision 浮点数的 4 维向量,就需要 16 bytes 的空间。65,536 个 4 维向量就需要 1MB 的空间了。

  事实上,内存的速度虽慢,但是要完成一个 32 bytes(一个四维向量的大小)的读写动作,也只需要 60 ~ 70 cycles 而已(以 Pentium III 500Mhz 配合 PC100 SDRAM 的情形来算)。而在不用 prefetch 的情形下,CPU 的动作类似下图所示:

Vector computation diagram 1

在上图中,CPU 的运算单元(即图中的 Execution Units)大部分的时间都在等待数据输入。而 Load/Store Unit 也有不少时间是不动作的。这显然不是最好的方法,因为 CPU 的两个单元都不是全速运作。

如果我们在 CPU 的运算单元进行计算工作时,就把下一个要计算的数据先加载到 CPU 的 cache 中,那么,CPU 的动作就会变成类似下图所示:

Vector computation diagram 2

现在,Load/Store Unit 变成全速运作了。Execution Units 还是没有全速运作,但是这是没办法的。这种情形,就表示出瓶颈是在 Load/Store Unit,也就是在主存储器的速度。已经没有任何方法可以加快执行的速度了(除非加快内存的速度)。

要注意的一点是,上面的情形是很少发生的真实世界中的。实际的程序,通常瓶颈都是在运算单元。不过,我们的例子则刚好不是这样(因为矩阵和向量相乘是很简单的运算),而是类似图中的情形。

要怎么告诉 CPU,在计算的同时将下一个数据加载到 cache 中呢?这时就要用到 prefetch 的指令了。在我们的程序中,执行向量运算的程序如下:

  • for(i = 0; i < buf_size; i += 4) {
    • double r1, r2, r3, r4;
    •  
    • // 执行矩阵乘法
    • r1 = m[0] * v[i] + m[1] * v[i+1] + m[2] * v[i+2] + m[3] * v[i+3];
    • r2 = m[4] * v[i] + m[5] * v[i+1] + m[6] * v[i+2] + m[7] * v[i+3];
    • r3 = m[8] * v[i] + m[9] * v[i+1] + m[10] * v[i+2] + m[11] * v[i+3];
    • r4 = m[12] * v[i] + m[13] * v[i+1] + m[14] * v[i+2] + m[15] * v[i+3];
    •  
    • // 写回计算结果
    • v[i] = r1;
    • v[i+1] = r2;
    • v[i+2] = r3;
    • v[i+3] = r4;
  • }

现在,我们在矩阵乘法的前面插入一个 prefetch 指令,变成:

  • for(i = 0; i < buf_size; i += 4) {
    • double r1, r2, r3, r4;
    •  
    • // 执行矩阵乘法
    • r1 = m[0] * v[i] + m[1] * v[i+1] + m[2] * v[i+2] + m[3] * v[i+3];
    • // 前一行执行完后,整个 4 维向量已经加载到 cache 中。
    • // 所以,现在用 prefetch 指令加载下一个 4 维向量。
    • prefetch(v + i + 4);
    • // 继续进行计算
    • r2 = m[4] * v[i] + m[5] * v[i+1] + m[6] * v[i+2] + m[7] * v[i+3];
    • r3 = m[8] * v[i] + m[9] * v[i+1] + m[10] * v[i+2] + m[11] * v[i+3];
    • r4 = m[12] * v[i] + m[13] * v[i+1] + m[14] * v[i+2] + m[15] * v[i+3];
    •  
    • // 写回计算结果
    • v[i] = r1;
    • v[i+1] = r2;
    • v[i+2] = r3;
    • v[i+3] = r4;
  • }

这段程序中的 prefetch 函式,里面执行的是 SSE 的 prefetchnta 指令。Pentium III 和 Athlon 都支持这个指令(AMD 的 K6-2 中另外有一个 prefetch 指令,是 3DNow! 指令的一部分)。这个指令会将指定的数据加载到离 CPU 最近的 cache 中(在 Pentium III 即为 L1 cache)。

只不过加上这样一行程序,执行结果就有很大的不同。回到前面的测试结果,我们可以看出,prefetch 指令,在数据已经存在 cache 中的时候,会有相当程度的 overhead(在这里是大约 10 cycles)。但是,当数据不在 cache 中的时候,效率就有明显的改善。特别是在数据量为 1024 KB 时,所需时间约为 70 cycles,说明了瓶颈确实是在 Load/Store Unit。在 1024 KB 之后,所需的 cycle 的增加,则是因为在多任务系统中难以避免的 task switch 所产生的 overhead。

  由此可知,prefetch 指令对于多媒体及 3D 游戏等数据量极大的应用,是非常重要的。也可以预料,将来的程序一定会更加善用这类的功能,以达到最佳的效率。



http://hi.baidu.com/junglylee/blog/item/fc3f8bf1143983cd7931aaf5.html


1. cache是什么

cache是CPU为了弥补计算速度与内存访问速度之间的巨大差距而设计的高速缓存区。一般CPU内存访问大都是局部连续的内存块,将内存中的数据块缓存在cache中,从而将对内存的访问转换为对速度较高的cache的访问,提高数据访问速度。

在Linux系统中,如Fedora, 文件 /proc/cpuinfo 保存了CPU的信息,这里列出的cache一般为二级缓存。下面是一台机器的cpuinfo文件内容,利用cat /proc/cpuinfo查看。

processor    : 0
vendor_id    : GenuineIntel
cpu family    : 6
model        : 15
model name    : Intel(R) Core(TM)2 Quad CPU    Q6600  @ 2.40GHz
stepping    : 11
cpu MHz        : 1596.000
cache size    : 4096 KB

其中cache的大小为4096KB。要查看一级缓存,可以使用命令:more /var/log/dmesg | grep cache,一台机器的显示为

CPU: L1 I cache: 32K, L1 D cache: 32K
CPU: L2 cache: 4096K

其中一级缓存分为数据缓存(L1 D)和指令缓存(L1 I),大小分别为32K。

2. cache工作原理

cache以cache线(cache line)为单位存贮数据。每条线一般为 4或8个字,也即有32或64字节,上节中32K的一级cache拥有1024或512个cache线。内存也相应以cache线长度为单位划分。cache从内存读写数据时就以cache线长度为单位。从cache到内存之间有一套cache映射策略来计算cache线与内存块的对应关系,这里从略。

CPU进行一次内存访问,如果数据在cache中,则称为cache命中的。在实际计算中,如果连续访问的数据位于不同的内存块中,而这些内存块被映射到同一cache线中,或者其分布范围超出了cache的大小,那么每一次的连续访问都将导致cache线重新从内存中读取数据,cache的作用几乎没有发挥,产生所谓的cache冲突。这在实际程序设计中十分重要,应予尽量避免。

避免cache冲突需要考虑数据在内存的存储顺序,例如Fortran和C的数组(二维或以上)存储顺序就正好相反。访问C的二维数据,最明智的做法就是先从行开始,然后再列访问。另外一个简单的方法就是补边法,增加数组的大小以避免数组的大小为2的幂次方,或增加辅助数组使得各数组之间错开一条cache线。

3. 实际的例子

下面是用Fortran写的例子,注意Fortran数组是先从列向存储。Linux下查看程序运行的时间的命令为time。

! Code 1:
program main
parameter(N=2048)
real*8 A(N, N)
do k=1, 100
do i=1,N
do j=1,N

A(i,j)=1.0d0
enddo
enddo
enddo
stop
end

将A的大小变为A(N+1, N+1)
! Code 2
program main
parameter(N=2048)
real*8 A(N+1, N+1)
do k=1, 100
do i=1,N
do j=1,N

A(i,j)=1.0d0
enddo
enddo
enddo
stop
end

考虑到Fortran数组的存储循序,将程序重写为

! Code 3
program main
parameter(N=2048)
real*8 A(N, N)
do k=1, 100
do j=1,N
do i=1,N

A(i,j)=1.0d0
enddo
enddo
enddo
stop
end

用g77编译以上程序,Code 1, 2, 3的运行时间分别为

Code1         17.679 s
Code2           5.076 s
Code3           1.730 s

可以看出,速度的提高还是很明显的。

参考:并行计算导论, 张林波等,清华大学出版社, 2006




摘自于:http://blog.csdn.net/hintonic/article/details/7589801

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多