8

我使用Delphi Sampling Profiler分析了我的应用程序的一部分。像大多数人一样,我看到大部分时间都在里面度过ntdll.dll

注意:我打开了忽略 Application.Idle时间的选项,并从System.pas. 所以它不在里面ntdll,因为应用程序是空闲的:

替代文字

经过多次运行,多次,大部分时间似乎都花在了里面ntdll.dll,但奇怪的是调用者是谁:

在此处输入图像描述

调用者来自虚拟树视图:

PrepareCell(PaintInfo, Window.Left, NodeBitmap.Width);    

注意:应用程序不在里面ntdll.dll,因为应用程序是空闲的,因为调用者不是Application.Idle

让我感到困惑的是,这条线本身(即不是PrepareCell内部ntdll的东西)是. 更令人困惑的是:

  • 不仅不是里面的东西 PrepareCell()
  • 它甚至不是调用者的设置PrepareCell例如弹出堆栈变量,设置隐式异常帧等)。这些东西会在分析器中显示为beginPrepareCell 内部的热点。

VirtualTrees.pas:

procedure TBaseVirtualTree.PrepareCell(var PaintInfo: TVTPaintInfo; WindowOrgX, MaxWidth: Integer);
begin
   ...
end;

所以我想弄清楚这条线是怎么回事:

PrepareCell(PaintInfo, Window.Left, NodeBitmap.Width);    

正在打电话ntdll.dll


唯一的其他方式是三个参数:

  • PaintInfo
  • Window.Left
  • NodeBitmap.Width

也许其中之一是一个函数,或者一个属性 getter,它会调用ntdll. 所以我在行上放了一个断点,并在运行时查看CPU窗口:

替代文字

里面有一行可能是罪魁祸首:

call dword ptr [edx+$2c]

但是当我跟随那个跳跃时,它并没有结束ntdll.dll,而是TBitmap.GetWidth

替代文字

如您所见,它不会在任何地方调用;并且肯定不会进入ntdll.dll


那么这条线如何:

PrepareCell(PaintInfo, Window.Left, NodeBitmap.Width);    

呼入ntdll.dll


注意:我很清楚它并没有真正调用 ntdll.dll。因此,任何有效的答案都必须包含“Sampling Profiler 具有误导性;该行没有调用 ntdll.dll”。答案也必须要么说大部分时间没有花在 ntdll.dll 中,要么突出显示的行不是调用者。最后,任何答案都必须解释为什么 Sampling Profiler 是错误的,以及如何修复它。

更新 2

什么是 ntdll.dll?Ntdll 是 Windows NT 的原生 API 集。Win32 API 是一个包装器ntdll.dll看起来就像存在于 Windows 1/2/3/9x 中的 Windows API。为了真正进入 ntdll,你必须调用一个直接或间接使用 ntdll 的函数。

例如,当我的 Delphi 应用程序空闲时,它通过调用 user32.dll 函数等待消息:

WaitMessage;

当你真正看到它时:

USER32.WaitMessage
  mov eax,$00001226
  mov edx,$7ffe0300
  call dword ptr [edx]
  ret

调用 at 指定的函数$7ffe0300是 Windows 转换到 Ring0 的方式,调用 EAX 中指定的 FunctionID。在这种情况下,被调用的系统函数是 0x1226。在我的操作系统 Windows Vista 上,0x1226 对应系统功能NtUserWaitMessage

这就是你进入 ntdll.dll 的方法:你调用它。

当我说出最初的问题时,我拼命地试图避免挥手不回答。通过非常具体,仔细指出我所看到的现实,我试图防止人们忽视事实,并试图使用挥手的论据。


更新三

我转换了两个参数:

PrepareCell(PaintInfo, Window.Left, NodeBitmap.Width);

进入堆栈变量:

_profiler_WindowLeft := Window.Left;
_profiler_NodeBitmapWidth := NodeBitmap.Width;
PrepareCell(PaintInfo, _profiler_WindowLeft, _profiler_NodeBitmapWidth);

确认瓶颈不是调用

  • Windows.Left, 或者
  • 节点位图宽度

Profiler 仍然指示该行

PrepareCell(PaintInfo, _profiler_WindowLeft, _profiler_NodeBitmapWidth);

本身就是瓶颈;PrepareCell没有任何东西。这一定意味着它在调用准备单元的设置中,或者在 PrepareCell 的开头:

VirtualTrees.pas.15746: PrepareCell(PaintInfo, _profiler_WindowLeft, _profiler_NodeBitmapWidth);
   mov eax,[ebp-$54]
   push eax
   mov edx,esi
   mov ecx,[ebp-$50]
   mov eax,[ebp-$04]
   call TBasevirtualTree.PrepareCell

没有任何东西调用ntdll。现在 PrepareCell 本身的前导码:

VirtualTrees.pas.15746: begin
   push ebp
   mov ebp,esp
   add esp,-$44
   push ebx
   push esi
   push edi
   mov [ebp-$14],ecx
   mov [ebp-$18],edx
   mov [ebp-$1c],eax
   lea esi,[ebp-$1c]
   mov edi,[ebp-$18]

里面什么都没有调用ntdll.dll


问题仍然存在:

  • 为什么将一个变量推入堆栈,而将另外两个变量推入寄存器是瓶颈?
  • 为什么 PrepareCell 内部的任何东西都不是瓶颈?
4

2 回答 2

3

好吧,这个问题实际上是我制作自己的采样分析器的主要原因:
http ://code.google.com/p/asmprofiler/wiki/AsmProfilerSamplingMode

也许不完美,但你可以试一试。让我知道您对此有何看法。

顺便说一句,我认为这与几乎所有调用都以对内核的调用(内存请求、绘制事件等)结束这一事实有关。只有计算不需要调用内核。大多数调用以等待内核结果结束:

ntdll.dll!KiFastSystemCallRet

您可以在带有线程堆栈视图的 Process Explorer 中看到这一点,或者在 Delphi 中,或者在我的 AsmProfiler 的“实时视图”中使用 StackWalk64 API:
http ://code.google.com/p/asmprofiler/wiki/ProcessStackViewer

于 2010-04-12T18:14:12.190 回答
1

那里可能发生了两件事。

第一个是 SamplingProfiler 通过遍历堆栈来识别调用者,直到它遇到看起来像是从 Delphi 代码到 Delphi 的有效调用点。

问题是,某些程序可能会一次保留大量堆栈,而无需重新初始化它。这可能会导致误报。那么唯一的线索就是你的误报是最近被调用的。

第二件事是ntdll本地化,这是众所周知的,但是,ntdll 是您在用户空间中的等待点,并且作为 user197220,ntdll 是您在调用系统内容并等待的大部分时间中最终等待的地方为结果。

在您的情况下,除非您降低采样率,否则您正在查看 247 毫秒的 CPU 工作时间,如果这 247 个样本是在多秒的实时时间内收集的,那么这可能会被视为空闲。由于误报指向 VirtualTree 绘制准备,我敢打赌 ntdll 时间实际上是绘制时间(驱动程序或操作系统软件)。您可以尝试注释掉实际执行绘画的代码以确定。

于 2010-07-28T13:39:06.820 回答