#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define DUMPCOPY for(i = 0; i < 65536; i++){\
destination = source;\
}

#define SMARTCOPY memcpy(destination, source ,65536);

int main(void)
{
char source, destination;
int i, j;

for(j = 0; j < 100; j++){
SMARTCOPY
// DUMPCOPY
}

return 0;
}

在DUMPCOPY方式下运行time ./a.out
real 0m0.333s
user 0m0.047s
sys 0m0.001s

在SMARTCOPY方式下运信time ./a.out
real 0m0.018s
user 0m0.010s
sys 0m0.001s
C专家编程中的解释:
之所以出现性能的下降,因为destination和source的大小正好是 cache容量的整数倍,且 cache行的填充使用的是一种优化
的算法:填充于同一 cache行的主存地址恰好是该 cache行大小的整数倍,所以只有地址的高位才会放入 cache行的标签中,
这样destiantion和source便遍不可能同时出现在 cache中,于是导致了性能的下降。
在DUMPCOPY方式下:destination 和source在使用同一 cache行的情况下,会导致每次对内存的引用都没发命中 cache,
使cpu的利用率大大下降,因为它不 得不等待常规的内存操作完成。
而在SMARTCOPY方式下:memcpy()函数经过特别优化来提高性能,它把先读取一个 cache行再对它进行写入这个循环分分解
开来,避免了上述问题。

上面的解释我看的不太明白,请大家帮我 分析一下性能下降的原因。谢谢!建议了解一下 cache 的结构,记忆中 cache 是n-set/n-way 结构吧,1个set 好像是 64 Byte吧
cache fill 行为至少需加载 1set 吧,memcpy的实现大概是这样的(用的当然是汇编代码):

for(i=0;i<65536;i+=8){
int temp; //temp其实是寄存器。
temp=source;
temp=source;
temp=source;
temp=source;
temp=source;
temp=source;
temp=source;
temp=source;
destination[--i]=temp;
destination[--i]=temp;
destination[--i]=temp;
destination[--i]=temp;
destination[--i]=temp;
destination[--i]=temp;
destination[--i]=temp;
destination[--i]=temp;
}
原帖由 xiaozhu2007 于 2007-10-20 22:11 发表http://bbs.chinaunix.net/images/common/back.gif
#include
#include
#include

#define DUMPCOPY for(i = 0; i < 65536; i++){\
destination = source;\
}

#define SMARTCOPY memcpy(destination, sourc ...
这个问题很难两三句话就讲清楚,但可以肯定的告诉lz,你看到的性能降低不是因为书上说的原因,除非你使用了书上提到的sparcCPU。这里简单介绍一下http://www.xmenwolverine.com:

首先抛开复杂的 cache结构,lz可以把 cache想象成一个大 数组数组的每个元素为一个 cache line,我们假定一个 cacheline为64 bytes。要把内存中的内容缓存到 cache这个 数组中,就需要一个索引,确定它存到哪一个 cacheline。CPU是用地址作为索引的,当然不是整个地址,而是地址的一部分。下图中,书上提到的SPARC使用了地址的高几位作为索引,而大部分架构使用地址的中间几位作为索引。现在就可以看出为什么sparc上这种方式性能低下了。

当程序中使用到 数组时,由于 数组地址是连续的,那么 数组每个元素地址的高几位都是相同的,它们通通对应 cache中同一个 cacheline。我们假设地址的高4位作为索引(再假设对于source这个 数组,它地址的高4位是0011)。那么当程序顺序扫描 数组时,cpu先读入64bytes,放到第3个 cache line中,go on。当读到第65个字节时,CPU再次读入64 bytes,然后把第3个 cacheline清空,把新的数据放进去……反复如此。这样,整个source 数组在同一时刻就只有64bytes数据在 cache中,每读入新的64bytes数据,就要把旧的清空,新的放入。这样反复的访存效率是很低下的, cache也没充分利用。并且这里dest 数组是和source 数组紧临的,也就是说source和dest 数组地址的高几位都相同,它们都对应同一个 cacheline!

如果用地址的中间几位作为索引,由于是低位地址, 数组的不同部分它们通常是不同的。这样同一个 数组的的不同部分就可以对应到 cache中不同的 cacheline去,那么source 数组就可以被整个放到 cache中去,而不是读64 byte新数据就把64bytes老数据冲掉了。减少了 cache的“颠簸”。

(当然,这里举的例子可能不恰当,如果仅仅用4 bit做索引,而 cache line又只有64bytes的话, cache显然太小了。而且4位做索引,中间几位通常也都是相同的。但如果用中间10bit做索引, 数组不同部分显然索引就不一样了。这里为了方便讲解,用了4bit。具体的规范,应该查所用cpu的 cache规范。)

lz这里看到的性能不同,是因为memcpy用了movsb这条指令做拷贝,性能当然比笨循环的方式高。当然还有库函数针对 cacheline对齐做的一些优化等等

备注一下:实际上index索引的是组,组里有很多 cache line,index确定内存对应哪个组后,由偏移寻址对应的 cacheline。

[ 本帖最后由 zx_wing 于 2007-10-21 22:26 编辑]此外上面举例中关于 cache的用语都是不正规的,只是为了说明问题简化而已。为了避免一些专业朋友给我纠正用语,还是先说了好。免得后面又来讨论 cache结构,麻烦又跑题:mrgreen:不错,赞一个,我认为4楼已经把 cache说得很明白了解释的不错,补充一个n-way的理解

http://blog.chinaunix.net/u1/43233/showart_339984.html回复 #4 zx_wing的帖子谢谢,讲的很好,明白 了,继续学下下呵呵回复 #4 zx_wing 的帖子还有点问题,
1。就是memcpy()函数你说的由于使用了movsb指令,所以效率比较高,根据3楼兄弟的理解,是不是因为memcpy()函数在每次循环中拷贝了8个字符,而且是通过寄存器来缓存而不是 cache,所以它的效率更高。
2。我不太熟悉 cache缓存的过程,就按上面的DUMPCOPY方式举个例子,在从source 数组拷贝一个字符给destination的过程中,是不是首先存内存中读一个字符,然后放入 cache缓存的一个 cache行中,然后当下一个从source过来的数据再次要把数据写

求教-谁能提供ntohl htonl 的函数源码

入到该 cache行中时,此时 cache把这一行原先缓存的数据发送到destination 数组中,再在该 cache行中放入新的数据,过程是不是这样的?还有 cache是怎么知道要把缓存http://www.51mingyu.com/的 cache行发送到destionation的某个位置上呢( cache知道destination的地址吗)?

C专家编程上写的, cache主要有两种类型,一种是全写法:就是发往目的地址的同时保存到 cache中;令一种是写回法,就是发往目的地址数据的时候,先不发往目的地址对应的内存,而是先写入 cache中对应的一个 cache行,当下一次内存再次往这个 cache行写数据的时候,就把原先在该行缓存的数据写入内存,然后缓存新的数据。
我想根据书上理解的,我的 cache应该是采取写回法的方式吧?!

3。呵呵,我基础不好,能给我说说,在数据复制的过程中,cpu,寄存器,内存,和 cache它们之间是怎么相互合作完成这个过程的?以及它们各自的作用是什么?

谢谢!!!!!!!!!!:)回复 #9 xiaozhu2007的帖子呵呵,lz这些问题,说实话,问的太宽了。不容易说清楚,也不容易说严谨。我只能简单说一下。

1.在x86中,memcpy确实是使用movs指令簇(这里b是个后缀,表示一次拷贝的字节数,也可能是w、d、q)。它之所以快,是因为你只需要指定source地址和dest地址,拷贝工作就由硬件为你完成了。在现代cpu中,只要涉及到了访问内存,就离不开 cache,没有寄存器直接和内存打交道的说法,都要通过 cache(只有一种例外,就是写操作的not-write-allocate)。一句话,CPU只和寄存器和 cache打交到, cache和物理内存的交互、同步都是北桥中的内存控制器做的(限于intel的CPU,amd的也有类似的机制)。在实际硬件中,cpu都以 cacheline为单位读入数据, cache line长度一般都大于8个字节,所以即使你只操作了一个字节的数据,它还是会读入一个 cacheline长度的数据,只是多于的没用到而已。所以你这里的8个字节说法不成立。当然,3楼的兄台演示的“循环展开”确实是一种优化的方法,这里就不多讨论了。

2.上面说了cpu只和 cache打交道,并不和实际的内存打交道。就用你的程序例子来解释你的第二个问题,它的流程是这样的:
从source第一字节开始,读入source(一次读一个 cache line长度),放入对应的 cache line中-------> 向dest 数组写数据,写到dest 数组对应的 cacheline中(从dest第一字节开始) -------->根据 cache的策略同步回物理内存---------->反复

这里 cache的策略就是lz提到的两种方法。一个叫write-through(直写),一个叫write-back(写回)。对于前者,cpu直接把dest中 cacheline的内容写到内存中的dest 数组去。对于后者,尽量推迟将 cache line同步内存中的时间,通常只在该行 cacheline需要被新的内容占用时才同步回内存。现代cpu通常采用write-back,但在un-cacheable的内存上(即该内存不能被 cache,通常是MMIO空间)使用write-through。地址到 cacheline的转换类似于hash表,文字很难说清楚,得画图,lz想了解应查阅相关资料。

(注意,上面举例画的流程图是以write-allocate举例的,也就是写前先写到 cache中,然后同步到内存。对于non-write-allocate,没有中间那个步骤,数据直接从source 数组对应的 cacheline同步到内存中。

3.寄存器只是cpu做运算使用的工具,通常需要先把内存中的内容搬到寄存器中,在寄存器中运算完了再写回内存(当然,它们中间就是上面的 cache)。

呵呵,lz问的这些不是基础不好,我觉得你问的深了。上面大概介绍了下过程,但实际上中间很多细节都没讲到,因为太多太多,打字累啊。我建议lz现在先对它有个感性认识就好了,需要的时候再阅读专门的资料深入学习。论坛只能让你知道个大概,不能让你知道细节。