两条相似行的CPU时间差异

时间:2011-07-07 19:53:08

标签: c++ c assembly profiling cpu-usage

我的程序中有一个while循环,其中IterZNextIterZ是指向列表中节点的指针。列表中的节点是struct类型,带有一个名为“Index”的字段。

double xx = 20.0;
double yy = 10000.0;
double zz;      
while (IterZNext!=NULL && NextIndex<=NewIndex)
{
    IterZ=IterZNext;
    IterZNext = IterZ->Next;
    if (IterZNext!=NULL)
    {
        zz = xx + yy;
                NextIndex1 = IterZNext->Index; // line (*)
        NextIndex = IterZNext->Index;  // line (**)
        IterZNext->Index;
    }
}

当我分析我的程序时,我找到了行(*)

NextIndex1 = IterZNext->Index;

消耗大部分CPU时间(2.193s),而行(**)

NextIndex = IterZNext->Index;

与行(*)大致相同仅使用0.093s。我使用英特尔VTune放大器来查看这两行的组合,如下所示:

Address Line    Assembly                   CPU Time Instructions Retired
Line (*):
0x1666  561 mov eax, dword ptr [ebp-0x44]   0.015s  50,000,000
0x1669  561 mov ecx, dword ptr [eax+0x8]        
0x166c  561 mov dword ptr [ebp-0x68], ecx   2.178s  1,614,000,000

Line (**):
0x166f  562 mov byte ptr [ebp-0x155], 0x1   0.039s  80,000,000
0x1676  562 mov eax, dword ptr [ebp-0x44]   0.027s  44,000,000
0x1679  562 mov ecx, dword ptr [eax+0x8]        
0x167c  562 mov dword ptr [ebp-0x5c], ecx   0.026s  94,000,000

如果我更改了行()和行( *)的顺序,那么程序将更改为

double xx = 20.0;
double yy = 10000.0;
double zz;      
while (IterZNext!=NULL && NextIndex<=NewIndex)
{
    IterZ=IterZNext;
    IterZNext = IterZ->Next;
    if (IterZNext!=NULL)
    {
        zz = xx + yy;
                NextIndex = IterZNext->Index;  // line (**)
                NextIndex1 = IterZNext->Index; // line (*)
        IterZNext->Index;
    }
}

并且程序集的结果更改为

Address Line    Assembly    CPU Time    Instructions Retired
Line (**):
0x1666  560 mov byte ptr [ebp-0x155], 0x1   0.044s  84,000,000
0x166d  560 mov eax, dword ptr [ebp-0x44]   0.006s  2,000,000
0x1670  560 mov ecx, dword ptr [eax+0x8]    0.001s  4,000,000
0x1673  560 mov dword ptr [ebp-0x5c], ecx   1.193s  1,536,000,000

Line (*):
0x1676  561 mov eax, dword ptr [ebp-0x44]   0.052s  128,000,000
0x1679  561 mov ecx, dword ptr [eax+0x8]        
0x167c  561 mov dword ptr [ebp-0x68], ecx   0.034s  112,000,000

在这种情况下,line(* )使用大部分CPU时间(1.245s),而line()仅使用0.086s。

有人能告诉我: (1)为什么第一次作业需要这么长时间?请注意,行zz = xx + yy仅使用0.058s。这与缓存未命中有关吗?因为列表中的所有节点都是动态生成的。 (2)为什么这两行之间的CPU时间差异很大?

谢谢!

3 个答案:

答案 0 :(得分:6)

所有现代CPU都是超级计算机&amp;乱序 - 这意味着指令实际上并没有按照程序集的顺序执行,并且实际上并没有当前的PC - 飞行中有大量的指令并且一次执行。

因此CPU报告的任何采样信息只是CPU执行的一个粗略区域 - 它正在执行采样中断关闭时指示的指令;但它也在执行所有其他飞行中的那些!

然而,人们已经习惯(并期望)分析工具告诉他们正好 CPU当前正在运行哪条指令 - 所以当采样中断触发时,CPU基本上选择了许多活动中的一个说明是“当前”的。

答案 1 :(得分:5)

CPU line caching可能就是原因。访问[ebp-0x5c]也会进入缓存[ebp-0x68],然后将更快地获取(对于第二种情况,反之亦然)。

答案 2 :(得分:0)

这绝对是由于缓存未命中。然后更大的错过,处理器将引入更多的性能损失。实际上在现代世界中CPU的运行速度比内存快得多。如果现在处理器可以具有大约4GHz的时钟频率,则存储器仍然以~0.3GHz的频率运行。这是一个巨大的性能差距,仍然在继续增长。缓存引入是由隐藏这种差距的愿望驱动的。没有缓存使用现代处理器将花费大量时间等待来自内存的数据并且当时无所事事。除了性能差距之外,每个内存访问都会在内存总线上产生与其他CPU和DMA设备的可靠并发相关的额外延迟,以及处理器内存管理逻辑一侧的内存访问请求处理和路由所需的时间(检查缓存)所有级别的虚拟到物理地址转换,可能涉及TLB未命中,具有对存储器的额外访问,请求推送到存储器总线等)和存储器控制器(请求从CPU控制器路由到控制器存储器总线,可能等待用于存储库刷新周期complition等)。因此,总而言之,与L1缓存命中或寄存器访问相比,原始访问内存的成本确实很高。成本差异与内存和二级存储(HDD)中访问数据的成本差异相当。

此外,内存访问的成本将随着从处理器移动到内存而增长。 L2访问将提供更大的惩罚,然后L1或CPU寄存器访问,L3访问将提供比L2访问更大的惩罚,并且最后,内存访问将提供比内存访问更大的惩罚。例如,您可以在下表(从http://www.anandtech.com/show/4955/the-bulldozer-review-amd-fx8150-tested/6中捕获)中比较不同级别的内存层次结构上的数据访问成本

缓存/内存延迟比较

-----------------------------------------------------------
|                                |L1| L2| L3| Main Memory |
-----------------------------------------------------------
|AMD FX-8150 (3.6GHz)            | 4| 21| 65| 195         |
-----------------------------------------------------------
|AMD Phenom II X4 975 BE (3.6GHz)| 3| 15| 59| 182         |
-----------------------------------------------------------
|AMD Phenom II X6 1100T (3.3GHz) | 3| 14| 55| 157         |
-----------------------------------------------------------
|Intel Core i5 2500K (3.3GHz)    | 4| 11| 25| 148         |
-----------------------------------------------------------

关于你的具体案例:

0x1669  561 mov ecx, dword ptr [eax+0x8]        
0x166c  561 mov dword ptr [ebp-0x68], ecx   2.178s  1,614,000,000


0x1670  560 mov ecx, dword ptr [eax+0x8]    0.001s  4,000,000 /* confusing and looks like wrong report for me*/ 
0x1673  560 mov dword ptr [ebp-0x5c], ecx   1.193s  1,536,000,000

您在代码行中取消引用索引值时会受到惩罚。

mov ecx, dword ptr [eax+0x8]

请注意,它首先访问列表的每个后续节点中的数据,直到此刻您只能通过节点地址进行操作,但该地址的数据由于此而无法访问内存。 你说,你使用动态列表,这是从缓存命中率点的坏。此外,我认为你有足够大的列表意味着你将通过先前访问的数据(在先前的迭代中访问的列表节点)来缓存,并且在访问索引期间几乎总是会在L3缓存上具有缓存未命中或缓存命中在每个新的迭代。但请注意,在每次涉及缓存时,首次访问索引时,每次从内存返回的新迭代数据都会丢失数据,并将其存储在L1缓存中。当您在同一周期迭代期间第二次访问Index时,您将获得低成本的L1缓存命中!

所以我希望我能就你们的问题向你提供详细的答案。

关于VTune报告正确性的正确性。我想提倡英特尔VTune开发人员。当然,现代处理器是非常复杂的设备,在板上有许多ILP改进技术,包括流水线操作,超标量化,乱序执行,分支预测等,以及cource,这使得详细的指令级性能分析更难,更珍贵。但像VTune这样的工具是在脑海中开发出的处理器功能,我相信他们并不是那么愚蠢地开发和提供没有任何意义的工具或功能。此外,看起来英特尔的开发人员无法完全理解所有处理器功能的详细信息,因为在分析器设计和开发过程中没有其他人能够将这些细节考虑在内。