假设a1,b1,c1和d1指向堆内存,我的数字代码有以下核心循环
const int n=100000;
对于(int j=0;j<;n;j++){
a1[j]+=b1[j];
c1[j]+=d1[j];
}
此循环通过另一个外部for循环执行10000次。为了加快速度,我将代码更改为:
用于(int j=0;j<;n;j++){
a1[j]+=b1[j];
}
对于(int j=0;j<;n;j++){
c1[j]+=d1[j];
}
编译在MS Visual C++ 10上,在英特尔核心2 DUO(X64)上实现了32位的完全优化和SSE2,第一个例子需要5.5秒,双环示例只需要1.9秒。我的问题是:(请参考我在底部重新表述的问题)
PS:我不确定这是否有帮助:
第一个循环的反汇编基本上如下所示(该块在整个程序中重复大约五次):
movsd xmm0,mmword ptr[edx+18h]
addsd xmm0,百万字ptr[ecx+20h]
movsd多字ptr[ecx+20h],xmm0
movsd xmm0,毫米波ptr[esi+10h]
addsd xmm0,百万字ptr[eax+30h]
movsd mmword ptr[eax+30h],xmm0
movsd xmm0,百万字ptr[edx+20h]
addsd xmm0,百万字ptr[ecx+28h]
movsd多字ptr[ecx+28h],xmm0
movsd xmm0,mmword ptr[esi+18h]
addsd xmm0,mmword ptr[eax+38h]
双循环示例的每个循环都会生成此代码(以下代码块重复大约三次):
addsd xmm0,mmword ptr[eax+28h]
movsd多字ptr[eax+28h],xmm0
movsd xmm0,百万字ptr[ecx+20h]
addsd xmm0,百万字ptr[eax+30h]
movsd mmword ptr[eax+30h],xmm0
movsd xmm0,毫米波ptr[ecx+28小时]
addsd xmm0,mmword ptr[eax+38h]
movsd多字ptr[eax+38h],xmm0
movsd xmm0,百万字ptr[ecx+30h]
addsd xmm0,mmword ptr[eax+40h]
movsd多字ptr[eax+40h],xmm0
这个问题与此无关,因为行为严重依赖于阵列(n)和CPU缓存的大小。因此,如果有进一步的兴趣,我将重新表述这个问题:
您能否深入了解导致不同缓存行为的细节,如下图中的五个区域所示?
通过为这些CPU提供类似的图表,指出CPU/缓存体系结构之间的差异也可能是有趣的。
PPS:这是完整的代码。它使用TBBTick_Count进行更高分辨率的计时,可以通过不定义TBB_计时宏来禁用:
#包括<;iostream>;
#包括<;iomanip>;
#包括<;cmath>;
#包括<;字符串>;
//#定义TBB_定时
#ifdef TBB_定时
#包括<;tbb/勾选计数h>;
使用tbb::勾选计数;
#否则
#包括<;时间;
#恩迪夫
使用名称空间std;
//#定义预分配\u内存新建\u cont
枚举{new_cont,new_sep};
双*a1、*b1、*c1、*d1;
无效分配(内部控制,内部n)
{
开关(续){
新个案(续)
a1=新的双精度[n*4];
b1=a1+n;
c1=b1+n;
d1=c1+n;
打破
新个案(九月)
a1=新的双精度[n];
b1=新的双精度[n];
c1=新的双精度[n];
d1=新的双精度[n];
打破
}
对于(int i=0;i<;n;i++){
a1[i]=1.0;
d1[i]=1.0;
c1[i]=1.0;
b1[i]=1.0;
}
}
无效ff(内部控制)
{
开关(续){
新个案(九月)
删除[]b1;
删除[]c1;
删除[]d1;
新个案(续)
删除[]a1;
}
}
双平面(整数n,整数m,整数cont,整数循环)
{
#ifndef预分配内存
allo(续,n);
#恩迪夫
#ifdef TBB_定时
tick_count t0=tick_count::now();
#否则
时钟启动=时钟();
#恩迪夫
如果(循环==1){
对于(int i=0;i<;m;i++){
对于(int j=0;j<;n;j++){
a1[j]+=b1[j];
c1[j]+=d1[j];
}
}
}否则{
对于(int i=0;i<;m;i++){
对于(int j=0;j<;n;j++){
a1[j]+=b1[j];
}
对于(int j=0;j<;n;j++){
c1[j]+=d1[j];
}
}
}
双ret;
#ifdef TBB_定时
滴答声计数t1=滴答声计数::现在();
ret=2.0*双倍(n)*双倍(m)/(t1-t0)。秒();
#否则
clock_t end=clock();
ret=2.0*双精度(n)*双精度(m)/(双精度)(结束-开始)*双精度(时钟/秒);
#恩迪夫
#ifndef预分配内存
ff(续);
#恩迪夫
返回ret;
}
void main()
{
freopen(“C:\\test.csv”、“w”和stdout);
char*s=";;
字符串na[2]={quot;new“cont”和“new”sep};
不能;;
对于(int j=0;j<;2;j++)
对于(int i=1;i<;=2;i++)
#ifdef预分配内存
cout<;<;s<;i<;<;<;<;<;<;i<;<;<;<;<;<;<;<;<;<;<;u循环<;<;<;<;<;<;na[预先分配的内存];
#否则
cout<;s<;i<;u loops<;na[j];
#恩迪夫
库特<;<;endl;
长nmax=1000000;
#ifdef预分配内存
allo(预分配内存,nmax);
#恩迪夫
对于(长n=1L;n<;nmax;n=max(n+1,长(n*1.2)))
{
const long m=10000000/n;
库特<;<;n;
对于(int j=0;j<;2;j++)
对于(int i=1;i<;=2;i++)
cout<;<;s<;平原(n,m,j,i);
库特<;<;endl;
}
}
(它为不同的n值显示FLOP/s)
进一步分析后,我认为这(至少部分)是由四个指针的数据对齐引起的。这将导致一定程度的缓存组/路径冲突
如果我猜对了数组的分配方式,那么它们很可能与页面行对齐
这意味着每个循环中的所有访问都将落在相同的缓存方式上。然而,英特尔处理器已经有一段时间具有8路一级缓存关联性。但在现实中,表现并不完全一致。访问4路仍然比说2路慢
编辑:事实上,看起来您是在单独分配所有阵列。
通常,当请求如此大的分配时,分配器将从操作系统请求新页面。因此,较大的分配很有可能出现在与页面边界相同的偏移量处
以下是测试代码:
int main(){
常数n=100000;
#ifdef分配_分离
double*a1=(double*)malloc(n*sizeof(double));
double*b1=(double*)malloc(n*sizeof(double));
double*c1=(double*)malloc(n*sizeof(double));
double*d1=(double*)malloc(n*sizeof(double));
#否则
double*a1=(double*)malloc(n*sizeof(double)*4);
双*b1=a1+n;
双*c1=b1+n;
双*d1=c1+n;
#恩迪夫
//将数据归零以防止任何非规范化的机会。
memset(a1,0,n*sizeof(double));
memset(b1,0,n*sizeof(double));
memset(c1,0,n*sizeof(double));
memset(d1,0,n*sizeof(double));
//打印地址
法院<;<;a1<;<;结束;
库特<;<;b1<;<;endl;
法院<;<;c1<;<;结束;
法院<;<;d1<;<;endl;
时钟启动=时钟();
int c=0;
而(c++<;10000){
#如果一个循环
对于(int j=0;j<;n;j++){
a1[j]+=b1[j];
c1[j]+=d1[j];
}
#否则
对于(int j=0;j<;n;j++){
a1[j]+=b1[j];
}
对于(int j=0;j<;n;j++){
c1[j]+=d1[j];
}
#恩迪夫
}
clock_t end=clock();
cout<;<;<;seconds=<;<;<;<;<;<;<;<;<;<;<;<;<;<;<;<;<;<;<;<;<;<;<;<;<;<;
系统(“暂停”);
返回0;
}
基准测试结果:
编辑:在实际核心2架构计算机上的结果:
2 x英特尔至强X5482 [email protected] GHz:
#单独定义分配
#定义一个循环
00600020
006D0020
007A0020
00870020
秒=6.206
#分别定义分配
//#定义一个循环
005E0020
006B0020
00780020
00850020
秒=2.116
//#分别定义分配
#定义一个循环
00570020
00633520
006F6A20
007B9F20
秒=1.894
//#分别定义分配
//#定义一个循环
008C0020
00983520
00A46A20
00B09F20
秒=1.993
意见:
-
6.206秒单圈和2.116秒双圈。这将精确地复制OP的结果
-
在前两个测试中,数组是分开分配的。您会注意到它们相对于页面的对齐方式都相同
-
在后两个测试中,阵列被打包在一起以打破这种对齐。这里您会注意到两个循环都更快。此外,第二个(双)循环现在是您通常期望的较慢的循环
正如@Stephen Cannon在评论中指出的,这种对齐很可能导致加载/存储单元或缓存中出现假锯齿。我在谷歌上搜索了一下,发现Intel实际上有一个用于部分地址别名的硬件计数器
http://software.intel.com/sites/products/documentation/doclib/stdxe/2013/~AMPLANTERXE/pmw\u dp/events/partial\u address\u alias.html
5个区域-说明
区域1:
这个很简单。数据集非常小,性能由循环和分支等开销控制
区域2:
这里,随着数据大小的增加,相对开销的数量会下降,性能也会下降;饱和;。这里有两个循环比较慢