WavePci 微型端口驱动程序的性能问题

通过遵循以下一般原则,可以显著减少音频驱动程序对系统的性能影响:

  • 尽量减少在正常作期间运行的代码。

  • 仅在必要时运行代码。

  • 考虑系统资源消耗总量(而不仅仅是 CPU 加载)。

  • 针对速度和大小优化代码。

此外,WavePci 微型端口驱动程序必须解决特定于音频设备的多个性能问题。 以下讨论主要涉及音频呈现问题,尽管一些建议的技术也适用于音频捕获。

流服务机制

在讨论性能优化之前,需要了解用于维护流的 WavePci 机制的一些背景。

处理波形呈现或捕获流时,音频设备需要微型端口驱动程序定期提供服务。 当一个流有新的映射可用时,驱动程序会将这些映射添加到该流的 DMA 队列。 驱动程序还会从队列中删除已处理的任何映射。 有关映射的信息,请参阅 WavePci 延迟

为了执行服务,小端口驱动程序提供 延迟过程调用(DPC) 或中断服务例程(ISR),这具体取决于间隔是由系统计时器设置还是由 DMA 驱动的中断设置。 在后一种情况下,DMA 硬件通常会在每次完成传输一定数量的流数据时触发中断。

每次执行 DPC 或 ISR 时,它都会确定需要维护的流。 DPC 或 ISR 通过调用 IPortWavePci::Notify 方法为流提供服务。 此方法采用流的服务组的调用参数,该参数是 IServiceGroup 类型的对象。 Notify 方法调用服务组的 RequestService 方法(请参阅 IServiceSink::RequestService)。

服务组对象包含一组服务接收器,这些接收器是 IServiceSink 类型的对象。 IServiceGroup 派生自 IServiceSink,两个接口都具有 RequestService 方法。 当 Notify 方法调用服务组的 RequestService 方法时,服务组将通过对组中每个服务接收器调用 RequestService 方法进行响应。

流的服务组包含至少一个服务接收器,端口驱动程序会在创建流后立即添加到服务组。 端口驱动程序调用微型端口驱动程序的 IMiniportWavePci::NewStream 方法以获取指向服务组的指针。 服务接收器的 RequestService 方法是端口驱动程序的特定于流的服务例程。 此例程执行以下作:

KS 事件中所述,当流到达特定位置或时钟到达特定时间戳时,客户端可以注册通知。 NewStream 方法可以选择不提供服务组,在这种情况下,端口驱动程序会设置自己的计时器来标记对服务例程的调用间隔。

NewStream 方法一样,微型端口驱动程序的 IMiniportWavePci::Init 方法还会输出指向服务组的指针。 在 Init 调用之后,端口驱动程序将其服务接收器添加到服务组中。 此特定服务接收器包含整个筛选器的服务例程。 (上一段描述了与筛选器上的引脚关联的流的服务接收器。此服务例程调用微型端口驱动程序的 IMiniportWavePci::Service 方法。 每次 DPC 或 ISR 使用筛选器的服务组调用 Notify 时,服务例程都会执行。 Init 方法可以选择不提供服务组,在这种情况下,端口驱动程序永远不会调用其筛选器服务例程。

硬件中断

某些微型端口驱动程序生成过多或没有足够的硬件中断。 在某些具有 DirectSound 硬件加速的 WavePci 渲染设备中,仅当映射的供应即将耗尽且呈现引擎面临饥饿风险时,才会发生硬件中断。 在其他硬件加速的 WavePci 设备中,每次映射完成时或在其他相对较小的间隔内,都会发生硬件中断。 在这种情况下,ISR 经常发现它没有什么可做,但每个中断仍然消耗系统资源来进行寄存器切换和缓存重新加载。 提高驱动程序性能的第一步是尽可能减少中断数,而不会造成饥饿风险。 消除不必要的中断后,可以通过设计 ISR 更高效地执行来实现额外的性能提升。

在某些驱动程序中,ISR 每次发生硬件中断时都会调用流的 Notify 方法来浪费时间,而不管流是否实际正在运行。 如果没有流处于 RUN 状态,则 DMA 处于非活动状态,并且尝试获取映射、发布映射或检查任何流中的新事件所花费的时间将浪费。 在高效的驱动程序中,ISR 会在调用流的 Notify 方法之前验证流是否正在运行。

但是,具有此类 ISR 的驱动程序需要确保在数据流退出 RUN 状态时,触发该流上的任何挂起事件。 否则,事件可能会延迟或丢失。 此问题仅在操作系统早于 Microsoft Windows XP 的 RUN 到 PAUSE 转换期间出现。 在 Windows XP 及更高版本中,当流的状态从运行更改为暂停时,端口驱动程序会自动发出所有未决的位置事件信号。 但是,在较旧的操作系统中,微型端口驱动程序负责在流暂停后立即调用 通知 来触发任何未完成的事件。 有关详细信息,请参阅下面的 PAUSE/ACQUIRE 优化。

典型的 WavePci 微型端口驱动程序从 KMixer 系统驱动程序管理单个播放流。 KMixer 的当前实现至少使用三个映射 IRP 来缓冲播放流。 每个 IRP 都包含足够的缓冲存储,可以支持大约 10 毫秒的音频。 如果微型端口驱动程序每次 DMA 控制器完成 IRP 的最终映射时触发硬件中断,那么中断应以 10 毫秒为间隔定期发生,这种频率足以防止 DMA 队列饿死。

计时器 DPC

如果驱动程序管理任何硬件加速的 DirectSound 流,它应使用计时器 DPC(请参阅 计时器对象和 DPC),而不是 DMA 驱动的硬件中断。 同样,带有板载计时器的 PCI 卡上的 WavePci 设备,可以使用基于计时器的硬件中断,而不是使用 DPC。

对于 DirectSound 缓冲区,可将整个缓冲区附加到单个 IRP。 如果缓冲区很大,并且微型端口驱动程序仅在到达缓冲区末尾时触发硬件中断,则连续中断之间可能会间隔很远,导致 DMA 队列出现饥饿现象。 此外,如果驱动程序管理大量硬件加速的 DirectSound 流,并且每个流都会生成自己的中断,则所有中断的累积影响可能会降低系统性能。 在这些情况下,微型端口驱动程序应避免使用硬件中断来计划单个流的服务。 相反,它应在一个按定期计时器生成的时间间隔安排运行的单一 DPC 中处理所有流。

通过将计时器间隔设置为 10 毫秒,连续执行的 DPC 之间的时间间隔与之前在单个 KMixer 播放流的硬件中断中描述的间隔相似。 因此,除了硬件加速的 DirectSound 流外,DPC 还可以处理 KMixer 播放流。

当最后一个流退出 RUN 状态时,微型端口驱动程序应禁用计时器 DPC 以避免浪费系统 CPU 周期。 禁用 DPC 后立即,驱动程序应确保所有以前正在运行的流上挂起的时钟或位置事件都被清理掉。 在 Windows 98/Me 和 Windows 2000 中,驱动程序应调用 Notify 来触发任何处于暂停状态的流上的挂起事件。 在 Windows XP 及更高版本中,操作系统会在流退出 RUN 状态时,自动触发任何挂起的事件,而不需要微型端口驱动程序进行干预。

PAUSE/ACQUIRE 优化

在 Windows 98/Me 和 Windows 2000 中,WavePci 端口驱动程序的流服务例程( RequestService 方法)始终生成对微型端口驱动程序的 IMiniportWavePciStream::Service 方法的调用,而不管流是否处于 RUN 状态。 在这些操作系统中,Service 方法应在花费时间进行实际工作之前检查流是否正在运行。 (但是,如果微型端口驱动程序的 DPC 或 ISR 已经进行了优化,以便仅针对正在运行的流调用 Notify ,则将此检查添加到 服务 方法可能是冗余的。

在 Windows XP 及更高版本中,此优化是不必要的,因为 Notify 方法仅在正在运行的流上调用 服务 方法。

使用 IPreFetchOffset 接口

DirectSound 用户熟悉播放游标和写入游标的双重概念。 播放指针指示从设备发出的数据流中的位置(驱动程序对当前在 DAC 上样本位置的最佳估计)。 写入位置是客户端写入其他数据的下一个安全位置流中的位置。 对于 WavePci,默认假设是写入游标位于请求的最后一个映射的末尾。 如果微型端口驱动程序获取了大量未完成的映射,那么播放游标和写入游标之间的偏移量可能会非常大,以至于导致某些 WHQL 音频位置测试失败。 在 Windows XP 及更高版本中, IPreFetchOffset 接口解决了这些问题。

微型端口驱动程序使用 IPreFetchOffset 指定总线主硬件的预提取特征,这在很大程度上取决于硬件 FIFO 大小。 音频子系统使用此数据设置播放游标和写入游标之间的常量偏移量。 此常量偏移量可以明显小于默认偏移量,它利用这样一个事实,即即使在将映射交给硬件之后,也可以将数据写入映射,只要播放光标距离写入数据的位置足够远。 此语句假定驱动程序不会复制或以其他方式操作映射中的数据。典型的偏移量可能大约为64个样本,具体取决于引擎设计。 由于偏移量很小,WavePci 驱动程序可以完全响应且功能正常,同时仍请求大量映射。

请注意,DirectSound 当前将硬件加速引脚的写入游标填充 10 毫秒。

有关详细信息,请参阅 预提取偏移量。

处理映射中的数据

如果可能,请避免硬件驱动程序在映射中触摸数据。 映射中包含的数据处理的任何软件部分都应被拆分为独立于硬件驱动程序的软件过滤器。 让硬件驱动程序执行此类处理可降低其效率并造成延迟问题。

硬件驱动程序应努力对其实际硬件功能保持透明。 驱动程序绝不应声称为实际上由软件执行的数据转换提供硬件支持。

同步基元

如果驱动程序的代码旨在避免尽可能避免被阻止,则驱动程序现在和将来不太可能出现死锁或性能问题。 具体而言,驱动程序的执行线程应努力运行到完成状态,而不会在等待另一个线程或资源时停止。 例如,驱动程序线程可以使用 InterlockedXxx 函数(例如,请参阅 InterlockedIncrement)协调对某些共享资源的访问,而不会受到阻止的风险。

尽管这些技术非常强大,但可能无法从执行路径安全地删除所有旋转锁、互斥体和其他阻塞同步基元。 明智地使用 InterlockedXxx 函数,了解无限期等待可能会导致数据饥饿。

最重要的是,不要创建自定义同步基元。 内置的 Windows 基元(如互斥体、旋转锁等)可能会根据需要进行修改以支持将来的新调度程序功能,而使用自定义构造的驱动程序几乎可以肯定将来无法正常工作。

IPinCount 接口

在 Windows XP 及更高版本中, IPinCount 接口为微型端口驱动程序提供了一种方法,以便更准确地考虑分配引脚消耗的硬件资源。 通过调用微型端口驱动程序的 IPinCount::PinCount 方法,端口驱动程序执行以下操作:

  • 向微型端口驱动程序公开筛选器的当前引脚计数(由端口驱动程序维护)。

  • 使微型端口驱动程序有机会修改引脚计数,以动态反映硬件资源的当前可用性。

对于某些音频设备,具有不同属性(3-D、立体声/单声道等)的波形流在消耗的硬件资源方面也可能有不同的“权重”。 打开或关闭“轻量级”流时,驱动程序会将可用引脚的计数增加或减少一个。 但是,打开“重量级”流时,迷你端口驱动程序可能需要将可用引脚计数减少两个,而不是一个,以便更准确地指示可以用剩余资源创建的引脚数。

关闭重载流时,此过程将逆转。 可用的引脚数量可能会增加多个,以反映出可以从新释放的资源创建两个或多个轻量级流的事实。