【问题标题】:difference in CPU time for two similar lines两条相似线路的 CPU 时间差异
【发布时间】:2011-09-30 18:32:43
【问题描述】:

我的程序中有一个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.093 秒。我用Intel VTune Amplifier看这两条线的组装,如下:

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

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

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

谢谢!

【问题讨论】:

  • 您对每个源代码行的相对 CPU 时间的陈述基于什么 - 分析器和结果输出是什么?

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


【解决方案1】:

这肯定是由于缓存未命中。然后处理器将引入更大的未命中然后更多的性能损失。实际上,在现代世界中,CPU 的执行速度要比内存快得多。如果现在的处理器可以有大约 4GHz 的时钟频率,那么内存仍然以 ~0.3GHz 的频率运行。巨大的性能差距仍在继续扩大。缓存的引入是出于隐藏这一差距的愿望。如果没有缓存,使用现代处理器将花费大量时间等待内存中的数据并且当时什么都不做。除了性能差距之外,每次内存访问都会产生额外的延迟,这些延迟与内存总线上与其他 CPU 和 DMA 设备的可能并发相关,以及内存访问请求处理和处理器内存管理逻辑侧路由所需的时间(检查缓存所有级别的虚拟到物理地址转换,可能涉及 TLB 未命中以及对内存的额外访问,请求推送到内存总线等)和内存控制器(请求从 CPU 控制器路由到控制器内存总线,可能的等待用于存储库刷新周期完成等)。因此,总而言之,与 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 改进技术,包括流水线、超标量、乱序执行、分支预测等,这使得详细的指令级性能分析变得更加困难和珍贵。但是像 VTune 这样的工具是在开发时考虑到了该处理器的特性,我相信他们不会那么愚蠢地开发和提供没有任何意义的工具或特性。此外,英特尔的开发人员似乎无法完全了解所有处理器特性的详细信息,并且在分析器设计和开发过程中没有其他人可以考虑到这些细节。

【讨论】:

    【解决方案2】:

    所有现代 CPU 都是超大规模和乱序的 - 这意味着指令实际上并没有按照汇编的顺序执行,并且实际上没有像当前 PC 这样的东西 - 有很多 10 条指令飞行并立即执行。

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

    然而,人们已经习惯(并期望)分析工具告诉他们准确 CPU 当前正在运行哪条指令 - 因此,当采样中断触发时,CPU 基本上会选择许多活动中的一条成为“当前”的指令。

    【讨论】:

    • 也许如果您基于芯片的实际性能计数器进行分析(例如 valgrind 和 oprofile 似乎能够做到),您至少会得到“事实”数据...@987654321 @。这么多,实际上,如果您不是英特尔员工,那么基于它进行分析可能是无用的:)(您仍然必须手动组合所有这些输入的原始数据,这很难在不知道芯片上的确切架构的情况下进行操作)
    • @sehe - 确实如此 - 但与其说是缺乏信息,不如说是在任何时候都没有一台 PC。如果您可以在给定周期获得“真实”的 CPU 分析信息,它将说明当时每个不同的 CPU 单元中碰巧有哪些指令的哪些微操作 - 远离单个 PC ;)
    【解决方案3】:

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

    【讨论】:

    • CPU 缓存将成为其中的一部分;但如果只是这种效果,那么您会期望看到每次订购的时间完全相同;正好相反 - 但是我们得到的是 A 的 {2.19s,0.09s} 和 B 的 {1.24s,0.08s}。
    • @Dave - 还涉及内存延迟、总线和分析器影响,所有这些都有影响。
    • 是的,但这里的根本问题是,现代无序 CPU 没有一个“当前 PC”,所有数据都是统计数据。当然,在直线代码中仅相隔几条指令的两个 IP 并不能真正区分。
    • @Dave - 根本问题是每个装配线的分析是没有意义的。
    • 是的;这就是我要说的;)
    猜你喜欢
    • 1970-01-01
    • 2014-12-22
    • 2015-01-10
    • 1970-01-01
    • 2012-08-19
    • 2013-08-28
    • 1970-01-01
    • 1970-01-01
    • 2018-12-16
    相关资源
    最近更新 更多