【问题标题】:Reduce CPU time spent in VCL减少在 VCL 中花费的 CPU 时间
【发布时间】:2012-01-27 13:22:56
【问题描述】:

对于 MIDI 播放器来说,尽可能精确地播放音符很重要。我从来没有成功过,总是责怪计时器(参见之前的帖子:How to prevent hints interrupting a timer)。最近我收购了 ProDelphi 并开始测量究竟是什么消耗了这么多时间。结果非常令人惊讶,请参见下面的示例代码。

procedure TClip_View.doMove (Sender: TObject; note, time, dure, max_time: Int32);
var x: Int32;
begin
{$IFDEF PROFILE}Profint.ProfStop; Try; Profint.ProfEnter(@self,1572 or $58B20000); {$ENDIF}
   Image.Picture.Bitmap.Canvas.Pen.Mode := pmNot;
   Image.Picture.Bitmap.Canvas.MoveTo (FPPos, 0);
   Image.Picture.Bitmap.Canvas.LineTo (FPPos, Image.Height);
   x := time * GPF.PpM div MIDI_Resolution;
   Image.Picture.Bitmap.Canvas.Pen.Mode := pmNot;
   Image.Picture.Bitmap.Canvas.MoveTo (x, 0);
   Image.Picture.Bitmap.Canvas.LineTo (x, Image.Height);
   FPPos := x;
//   Bevel.Left := time * GPF.PpM div MIDI_Resolution;
{$IFDEF PROFILE}finally; Profint.ProfExit(1572); end;{$ENDIF}
end; // doMove //

测量结果是(在 Intel i7-920、2.7Ghz 上没有调试代码):

  1. 如图所示的代码需要 95 微秒
  2. 5.609 毫秒,除了现在注释掉的语句 (Bevel.Left :=) 之外的所有内容都被注释掉了
  3. 0.056 微秒,当所有代码被x := time * GPF.PpM div MIDI_Resolution; 替换时

仅在 Bevel 上移动所消耗的 CPU 是在 Canvas 上绘图的 60 倍。这让我很惊讶。测量 1 的结果非常好听(还有更多的事情发生),但 2 和 3 不是。我需要某种形式的反馈给用户,因为玩家现在正在处理什么,钢琴卷上的某种线条是公认的方式。在我对减少定时事件循环中 CPU 周期的永无止境的追求中,我有一些问题:

  • 为什么绕斜角移动要花费这么多时间?
  • 有没有办法比在位图上绘图减少更多的 CPU 周期?
  • 有没有办法减少绘图时的闪烁?

【问题讨论】:

  • Windows 不是一个实时操作系统,但您真的不应该将实时方面的行为(如您的 MIDI 排序)与您的 UI 绘画联系起来。
  • @WarrenP MIDI 不期望真正的“实时”。它本身具有几毫秒的延迟(特别是对于 DIN 外部外围设备 - 它是 32000 波特 AFAIR 的串行链路)。今天的 Windows 反应灵敏,足以处理这个问题。
  • 但如果您在代码中插入随机暂停则不会,因为它是单线程且低效的。 :-)

标签: delphi bitmap vcl


【解决方案1】:

您将无法改变世界、VCL 或 Windows。我怀疑你对那些人要求太多了……

恕我直言,你最好改变一点你的架构:

  • 声音处理应在一个(或多个)单独的线程中进行,并且根本不应链接到 UI(例如,不要从它发送 GDI 消息);
  • 应使用分辨率为 500 毫秒的计时器进行 UI 刷新(半秒刷新听起来足够反应),而不是每次发生变化时。

也就是说,sequencer 不会刷新 UI,但 UI 会定期询问 sequencer 的当前状态。恕我直言,这将更加顺畅。

回答您的确切问题:

  • “Moving a bevel”实际上是发送多个 GDI 消息,渲染将由 GDI 堆栈 (gdi32.dll) 使用临时位图进行;
  • 尝试使用较小的位图,或尝试使用 Direct X 缓冲区映射;
  • 在您的 TForm.OnCreate 事件上尝试 DoubleBuffered := true,或使用具有全局位图的专用组件 (TPaintBox) 来显示整个组件内容,并为消息 WM_ERASEBKGND 使用类似的东西。

一些代码:

   procedure TMyPaintBox.WMEraseBkgnd(var Message: TWmEraseBkgnd);
   begin
     Message.Result := 1; // no erasing is necessary after this method call
   end;

【讨论】:

  • 双缓冲当然会减慢速度,虽然它可能有助于消除闪烁,但它会影响性能。当然,将音频从 UI 线程中分离出来是没有实际意义的。
  • 这正在颠覆架构!这是一个有趣的想法,我会尝试,因为它将音乐制作与视觉反应脱钩。现在我把所有东西都联系在一起了。谢谢!
  • 我决定给你接受的答案,不仅是因为答案正确,还因为你的解耦建议。这极大地帮助了我改进程序。我现在有单独的线程,每个线程都有自己的音频和 VCL 计时器以及它们之间的消息队列。这在分析器中给出了很好的数字。
  • 废话:“移动斜角”实际上是发送多个 GDI 消息。如果斜角已在内部缓冲区上计算,则不必移动它...您只需在其他地方绘制缓冲区。
  • @az01 你的评论太冒犯了。你应该更好地使用源,卢克! TBevelTControl.SetBounds 中发送 GDI 消息以使控件无效,然后从操作系统发回 WM_PAINT 消息,TBevel.Paint 在新位置使用 GDI Canvas 命令绘制斜角。它不会“只是在其他地方绘制缓冲区”。它绘制了其他的斜角 - “没有在内部缓冲区上计算斜角”。
【解决方案2】:

我感觉你的位图缓冲是错误的。当您移动剪辑时,根本不需要重新绘制它。你可以试试这个剪辑组件结构:

TMidiClip = Class(TControl)
Private
 FBuffer: TBitmap;
 FGridPos: TPoint;
 FHasToRepaint: Boolean;
Public
  Procedure Paint; Override; // you only draw the bitmap on the control canvas
  Procedure Refresh; // you recompute the FBuffer.canvas
End;

当您更改某些属性(例如“剪辑刻度长度”)时,您将“FHasToRepaint”设置为 true,但在更改“FGridPos”(网格上的位置)时不会。所以大多数时候,在你的 Paint 事件中,你只有一个 FBuffer 的副本。

实际上,这在很大程度上取决于您的网格及其子项(剪辑)的设计。 我可能错了,但似乎您的设计在控件中分解得不够:主网格应该是 TControl,剪辑应该是 TControl,甚至剪辑上的事件也应该是一些 TControls...您只能定义一个通过这种方式强烈优化的位图缓冲系统(又名“双缓冲”)。

关于计时器:您应该使用能够处理每个音频样本的音乐时钟,否则您将无法获得足够好的分辨率。这样的时钟可以使用“Windows 音频驱动程序”(mmsystem.pas) 或“Asio”驱动程序来实现(您在 BASS 中有一个接口,例如 Delphi Asio Vst 项目)。

【讨论】:

  • 我使用你建议的计时器。我不使用音频样本,只是发送 MIDI 事件。平均间隔为 22 毫秒,但它应该能够处理高达 10 毫秒的间隔。至于您对位图的回答,我必须承认我的无知。我现在直接在 Image.Picture.Bitmap 中包含的钢琴卷上绘图。您是否建议保留一个单独的位图,在该位图上绘制,然后在图像位图上绘制它?
  • 关于时钟外壳检查这个:link。关于网格的设计,我承认我的答案有点混乱,但这是因为这个答案主要基于对网格/网格项的复杂系统需要进行最高级讨论的猜测。
【解决方案3】:

到目前为止,解决此问题的最佳方法是阻止 GUI 消息队列干扰您的 MIDI 播放器。将 MIDI 播放器放在后台线程上,这样它就可以在不受主线程中断的情况下完成工作。当然,这依赖于您在具有多个处理器的机器上运行,但如今将其视为理所当然并非没有道理。

从您的 cmets 看来,您的音频处理似乎被 UI 线程阻塞。不要让这种情况发生,您的音频问题就会消失。如果您使用TThread.Synchronize 之类的东西从音频线程触发 VCL 事件,那么这将在 UI 线程上阻塞。请改用异步通信方法。

您提出的加速 VCL 的替代方案实际上并不可行。您无法修改 VCL,即使可以修改,瓶颈也很可能是底层 Windows 代码。

【讨论】:

  • @DavidHeffeman,MIDI 播放器已经在后台运行,回调由计时器线程处理。一些事件处理程序必须与 VCL 通信以告知用户播放的状态。我并没有提议修改VCL,只是想寻找快速做事的方法。在位图上绘图似乎比相对于另一个组件移动组件要快。只是想知道是否还有其他我不知道的技术。
  • 也许我理解错了。我以为你的问题是音频播放不准确。是不是你画的太慢了?
  • 您正确描述了问题,也许是我理解错了:-) 计时器(来自 MMSystem)有一个在线程中运行的回调。它检查所有类型的事件处理程序,其中一些会向 VCL 报告以提供反馈。减少此报告链中每个元素的 CPU 时间是值得的。这些元素之一是上面显示的doMove 例程。我找到了一种方法来做到这一点,但只是要求更多:-)而且我想知道为什么相对于另一个组件移动 Bevel 需要这么多时间。
  • 好的,那么我认为您需要与 VCL 异步通信。使用 TThread.Queue 或其他一些异步逗号。不要阻塞在音频线程中。
  • 我说的和 Arnaud 说的一样。
猜你喜欢
  • 2020-06-05
  • 1970-01-01
  • 1970-01-01
  • 2020-09-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多