【问题标题】:Quadratic runtime for Delphi XML (MSXML) routinesDelphi XML (MSXML) 例程的二次运行时
【发布时间】:2025-12-14 07:25:01
【问题描述】:

在工作中,我们使用的是 XML 日志文件。每个日志消息都是一个带有<date><time> 子节点的<message> 块,有<submessage> 块、<table> 构造等等,并且可以使用一些 Delphi 处理和 XSLT 将日志文件转换为本地化的 HTML稍后。

对于中等大小的日志文件(大约 2 MB),我们遇到了性能问题(加载 XML 和进行一些基本操作最多需要一分钟),我可以将它们简化为这样的测试项目(编辑:更新代码并添加测量值):

procedure TForm1.PrepareTest(MessageCount : integer);
var
  XML : IXMLDocument;
  i : integer;
begin
  XML := NewXMLDocument;
  XML.DocumentElement := XML.CreateNode('root');
  for i := 1 to MessageCount do
  begin
    XML.DocumentElement.AddChild('message').Text := 'Test Text';
  end;
  XML.SaveToFile(XML_NAME);
end;

procedure TForm1.XMLTest;
var
  StartTime : Cardinal;
  XML : IXMLDocument;
begin
  StartTime := GetTickCount();
  XML := NewXMLDocument;
  XML.LoadFromFile(XML_NAME);
  Memo1.Lines.Add('Node count: ' + IntToStr(XML.DocumentElement.ChildNodes.Count));
  Memo1.Lines.Add('Time: ' + FloatToStr((GetTickCount() - StartTime) / 1000) + ' seconds');
end;

这会产生以下时间测量结果(节点计数每列增加 25%,所有时间都以毫秒为单位):

Node count      8000    10000   12500   15625   19531   24413   30516   38145   47681
Base test time  484     781     1140    1875    2890    4421    6734    10672   16812
Variation 1                             32      47      62      78      78      141
Variation 2     2656    3157    3906    5015    6532    8922    12140   17391   24985
  (delta Base)   2172    2376    2766    3140    3642    4501    5406    6719    8173

注意这两种变体,第一种是 LoadFromFile,第二种是像 PrepareTest 那样在 XML 的开头(!)另外添加 10000 个节点,这是最坏的情况,但查看增量基础测试,即使这也没有显示二次效应。另请注意,计算节点可以用任何其他操作替换,因此看起来涉及的 XML 文件的初始化/验证存在一些延迟,这会导致问题,并且之后的任何操作都会显示预期的行为。

内存使用不高,最后一个测试用例(47681 个节点)的内存使用峰值为 39 MB,其 XML 文件大小为 1.3 MB。

加载 XML 后做的第一件事(例如读取或写入一些节点或访问节点计数)很慢,它显示 二次运行时行为,因此任何超过 10 MB 的日志文件都无法使用.

我们已经通过解析 100 条消息的小块解决了性能问题以及其他一些问题,而且我知道 Delphi XML 例程不适合/过度杀伤这种用例 - 使用不同的 XML 库很可能停止性能问题。所以我不是在寻求解决问题的方法(尽管如果不使用不同的 XML 库就可以解决问题会很有趣)。

我的问题是:Delphi XML 例程和 MSXML 的二次运行时行为的原因是什么?我无法想象在 XML 加载/解析/验证中会导致这种情况的事情,除了真正“愚蠢”的事情,比如管理链表而不是树中的节点,但我可能忽略了一些东西,也许是 DOM 相关的。

【问题讨论】:

  • 这并不是真正的 Delphi 问题。有问题的 XML 例程只是 Windows 标准 MSXML 库的接口。
  • XML 不适合记录。您最好的选择是打开文件,找到最后一个标签(希望通过已知大小自动附加您的新节点,然后再次附加最后一个标签。否则您将失败。
  • @mj2008:按照您的描述添加新消息,日志文件仅在转换为 HTML 时作为一个整体加载,因此该问题不会影响主日志记录过程。尽管如此,如前所述,XML 在此处具有优势,例如XSLT 非常有用,工作流中使用的所有内容都“准备好 UTF”。
  • @teran:几个月前我已经完成了测量,明天我可能会在查看它们后编辑问题,但是在没有进一步 XML 修改的情况下,单个 LoadFromFile() 调用非常快,而且只会变慢添加修改时(并且只有第一次修改很慢),所以它似乎使用延迟初始化/验证。
  • @schnaader 基于 DOM 的 API 总是很容易使用,但速度很慢。另一方面,基于 SAX 的 API 使用起来相当棘手,但速度很快。

标签: xml performance delphi


【解决方案1】:

我同意 mj2008 的观点,即 XML 不适合记录日志。 也就是说,这个问题和一般的大型 XML 文件,可以通过使用 SAX 更快地处理,它在解析传入的 XML 数据流时引发事件,这使您可以在从磁盘读取项目时对其进行处理,实际上缓解了在将其交给 XSLT 之前将其全部加载到内存中的指数级。

我很遗憾我还没有在 Delphi 中完成 SAX,但我怀疑最困难的部分是实现所需的 SAX 接口(例如 ISAXContentHandler),但 Delphi 有 TInterfacedObject 和 TAutoObject 等。

【讨论】:

  • 这个答案让我更加困惑。您说 SAX 提高了我完全理解的性能,但是为什么加载时它仍然具有指数运行时间?为什么解析 X 节点的后半部分花费的时间是前半部分的两倍?对于最坏情况的 XML 树(X 个嵌套节点),我可能会理解它,但对于日志文件的简单结构(X 个单独的消息节点),非线性运行时似乎是错误的。
  • SAX 解析将随输入文件大小线性缩放
  • 我现在只相信大卫并 +1 并接受你的回答,因为 SAX 将是我接下来尝试的事情之一。如果它也有指数行为,我会发布一个新问题或编辑这个问题,但我对此表示怀疑。谢谢!
【解决方案2】:

您的问题简而言之:为什么二进制库 MSXML 这么慢?谁知道。谁在乎。你要拆机吗?闯入微软抢其源代码?这里不是 Delphi,这是微软代码。

虽然 XML 不适合记录日志,但 OmniXML 可能是比 MSXML 更好的选择。

但是,更好的选择是“打开文本文件以进行追加、写入行、关闭文本文件”。请注意固有的可扩展性,并且不需要解析。

【讨论】:

  • +1 因为你是绝对正确的,我为解决这些问题所做的工作朝着这些方向发展。如果这里有人知道答案,我只是好奇。顺便说一句,我非常接近检查/拆卸或至少确实对该主题进行更多研究,因为我对他们为什么/如何将 MSXML 搞得如此糟糕感兴趣。
【解决方案3】:

与其他人的 cmets 不同,我认为 XML 是一种出色的日志记录格式。用于 XML 的 Delphi VCL 包装器对核心内存非常贪婪,因此这可能解释了纯 TXMLDocument 大规模处理性能不佳的原因。

我建议使用简单的 XSLT 转换将其发布到您的 XML 日志中。我尚未大规模测量此解决方案的性能,但我相信它会比您目前报告的内容有很大的改进。

样式表。

例如,假设我们的日志看起来像这样......

<log>
  <message>This is the first message<message/> 
</log>  

这个简单的 XSLT 1.0 样式表,带有参数 addend-message ...

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output indent="yes" omit-xml-declaration="yes" />
<xsl:strip-space elements="*" />  
<xsl:param name="addend-message" select="''" />

<xsl:template match="@*|node()">
  <xsl:copy>
    <xsl:apply-templates select="@*|node()" />
  </xsl:copy>
</xsl:template>  

<xsl:template match="log">
  <xsl:copy>
    <xsl:apply-templates select="@*|node()" />
    <message><xsl:value-of select="$addend-message" /></message>
  </xsl:copy>
</xsl:template>  

... 将在日志中附加一条消息。

语言绑定

要在 Delphi 中实现这一点,请使用以下声明...

ITransform = interface
  procedure AddParameter( const sParamBaseName, sParamValue, sNamespaceURI: string);
  procedure Transform;
  property  InputDocumentFileName : string;
  property  OutputDocumentFileName: string;
end;

function MS_Transform( const sStylesheet: string): ITransform;

创建一个 ITransform,将样式表作为字符串传递。将两个文件名属性都设置为日志的文件名。每次需要向日志中添加消息时,调用AddParameter(),然后调用Transform()

解决方案实现细节

上述语言绑定的一种可能实现可能是...

uses XMLIntf, msxml, msxmldom, sysutils;

type
  ITransform = interface
    ['{1004AE9A-D4AE-40E1-956D-AD98801AF7C1}']
      procedure SetInputDocumentFileName ( const sValue: string);
      procedure SetOutputDocumentFileName( const sValue: string);
      procedure AddParameter( const sParamBaseName, sParamValue, sNamespaceURI: string);
      procedure Transform;

      property InputDocumentFileName : string    write SetInputDocumentFileName;
      property OutputDocumentFileName: string    write SetInputDocumentFileName;
    end;

    TMS_XSLT = class( TInterfacedObject, ITransform)
    private
      FStylesheet: IXSLTemplate;
      FStylesheetAsDoc: IXMLDOMDocument2;
      FInputFN, FOutputFN: string;
      FProcessor: IXSLProcessor;;

      procedure SetInputDocumentFileName ( const sValue: string);
      procedure SetOutputDocumentFileName( const sValue: string);
      procedure MakeProcessor;

    publc
      constructor Create( const sStylesheet: string);
      procedure AddParameter( const sParamBaseName, sParamValue, sNamespaceURI: string);
      procedure Transform;

      property InputDocumentFileName : string    write SetInputDocumentFileName;
      property OutputDocumentFileName: string    write SetInputDocumentFileName;
    end;

  function MS_Transform( const sStylesheet: string): ITransform


function MS_Transform( const sStylesheet: string): ITransform
begin
result := TMS_XSLT.Create( sStylesheet)
end;

constructor TMS_XSLT.Create( const sStylesheet: string);
begin
  FStyleSheet := msxml.CoXSLTemplate60.Create;
  FStylesheetAsDoc := msxml.CoFreeThreadedDOMDocument60.Create;
  FStylesheetAsDoc.loadXML( sStyleSheetContent);
  FStylesheet.stylesheet := FStylesheetAsDoc  
end;

procedure TMS_XSLT.MakeProcessor;
begin
if not assigned( FProcessor) then
  FProcessor := FStylesheet.createProcessor
end;

procedure TMS_XSLT.SetInputDocumentFileName( const sValue: string);
begin
FInputDoc := sValue
end;

procedure TMS_XSLT.SetOutputDocumentFileName( const sValue: string);
begin
FOutputDoc := sValue
end;

procedure TMS_XSLT.AddParameter( const sParamBaseName, sParamValue, sNamespaceURI: string);
begin
MakeProcessor;
FProcessor.addParameter( sParamBaseName, sParamValue, sNamespaceURI)
end;

procedure TMS_XSLT.Transform;
var
  Doc: TXMLDocument;
  DocIntf: IXMLDocument;
  oXMLDOMNode: IXMLDOMNodeRef;
  sOutput: string;
begin
MakeProcessor;
try
  Doc  := TXMLDocument.Create( nil);
  Doc.Options := [doNodeAutoCreate, doNodeAutoIndent, doAttrNull, doAutoPrefix, doNamespaceDecl];
  Doc.DOMVendor := GetDOMVendor( 'MSXML');
  DocIntf := Doc;
  DocIntf.LoadFromFile( FInputFN);
  DocIntf.Active := True;
  if Supports( DocIntf.Node.DOMNode, IXMLDOMNodeRef, XMLDOMNode) then
    FProcessor.input := XMLDOMNode.GetXMLDOMNode;
  FProcessor.transform;
  while oProcessor.readyState <> 4 do sleep(1);
  sOutput := FProcessor.output;
  if sOutput = '' then exit;
  WriteToFile( sFOutputFN, sOutput);
  // Alternate way..
  //  Doc  := TXMLDocument.Create( nil);
  //  Doc.Options := [doNodeAutoCreate, doNodeAutoIndent, doAttrNull, doAutoPrefix, doNamespaceDecl];
  //  Doc.DOMVendor := GetDOMVendor( 'MSXML');
 //   DocIntf := Doc;
  //  DocIntf.LoadFromXML( sOutput);
  //  DocIntf.Active := True;
  //  DocIntf.SaveToFile( FOutputFN)
finally
  FProcessor := nil
  end
end;

这绑定到Microsoft's MS XML library 和 XSLT 引擎。遗憾的是,我不知道有什么方便的方法可以将Saxon's XSLT processor 绑定到 Delphi 代码。

替代实现

my answer here 给出了利用 MS 的 XSLT 引擎的替代实现。这种方法的缺点是参数化不是原生的。要参数化样式表,您必须自己滚动,通过在转换之前对样式表进行字符串替换。

性能考虑

如果您要快速进行大量日志记录,那么将要记录的消息缓存到内存中可能是一个不错的策略,然后定期但不太频繁地使用单个 XSLT 转换清除缓存以写入您的所有消息。

【讨论】:

  • +1,稍微偏离主题,因为日志记录本身不是我的问题,但这是完成主要日志记录工作的好方法。
  • +1 表示疯狂的想法。我仍然认为这太复杂了。文本文件更好。
【解决方案4】:

您是在数学意义上使用“指数”这个术语,还是只是在通俗意义上使用?例如,知道它是否真的是二次方的,或者它是否是某种函数,其性能是相当线性的,直到你达到某个阈值(内存大小),然后它突然降级,这会很有趣。

如果处理 2Mb 需要一分钟,那么确实是非常严重的错误。我不太了解您的环境,无法开始猜测,但这最多需要一秒钟。您需要深入了解时间的去向。首先确定它是花时间解析 XML,还是在解析完成后处理 XML。

【讨论】:

  • 我会尽快用完整的代码、运行时和内存使用情况更新问题。我在数学意义上使用“指数”,例如对于大小为 10 KB 的“基本”XML,处理 20 KB 大约需要 4 倍的时间,40 KB 大约需要 16 倍的时间,依此类推。但是您的问题仍然有效,因为我上次只检查了 3-5 个值,还没有进行完整的运行时分析。
  • 那将是二次缩放。您是否有机会使用冒泡排序?
  • @DavidHeffernan:我没有做任何可以解释二次行为的事情,这就是为什么我对 MSXML 在这里做错的原因如此感兴趣的原因。我什至可以将问题简化为两个 API 调用,加载一个 XML 并计算它的节点。
  • 其中哪一部分是二次的?加载还是计数?
  • 计数,但它不取决于我是否计算节点或对 XML 执行任何其他操作,似乎有一些延迟的初始化/验证正在进行,之后的操作显示预期“正常”的行为。
最近更新 更多