Delphi 百万级配置保存性能优化:从 20 秒到 2 秒的实战

背景

最近在项目中遇到一个性能问题,某工业软件的配置模块,一个配置保存操作,需要保存 100 MB+ 的复杂结构配置文件时,参数保存操作耗时高达 20 秒。该文件包含:

  • 各类型自定义结构体,需要自行对参数进行序列化和反序列化
  • 多层级的配置树
  • 多级安全校验规则

问题分析

通过对代码分析,发现主要性能瓶颈在于:

  • I/O 风暴:每个节点单独写入,导致频繁的磁盘 I/O 操作
  • 内存碎片:字符串拼接不合理,产生过多的临时对象,导致频繁的内存分配和回收
  • 编码冗余:多次进行UTF8编码和解码操作

问题示例:

// 典型递归写入模式
procedure SaveNode(Node: TConfigNode; Writer: TStreamWriter);
begin
  Writer.WriteLine(Node.Header);  // 触发磁盘I/O
  foreach ChildNode in Node.Children do
    SaveNode(ChildNode, Writer);  // 递归调用
end;

优化方案

核心方案:三级缓冲

20250213172901_32073.png

  1. 使用 TStringBuilder 缓冲内容:将所有节点的内容先缓存到 TStringBuilder 中,减少字符串拼接产生的临时对象,减少内存分配和复制的开销

    // 原代码
    nodeStr = format('%sobject %s : %s', [copy(SPACES, 1, Level*2), ParamName, ParamClassName]);
    OutputWriter.WriteLine(nodeStr);
    
    // 修改后的代码
    sb.AppendLine(format('%sobject %s : %s', [copy(SPACES, 1, Level*2), ParamName, ParamClassName]));
    
  2. 预分配缓冲: 预先分配关键缓冲区的内存,减少内存分配和回收

    // 根据历史数据动态调整缓冲区
    BufferSize := Trunc(LastFileSize * 1.2);
    Builder := TStringBuilder.Create(BufferSize);
    
  3. 减少 I/O 操作:将所有节点的内容先缓存到内存中,最后一次性写入磁盘,减少频繁的磁盘 I/O 操作

    // 原代码
    procedure TParamWriter.WriteValue(Writer: TStreamWriter);
    begin
        Writer.WriteLine(Format('Value=%s', [Value])); // 直接I/O
    end;
    
    // 优化后
    procedure TParamWriter.BuildString(var Buffer: TStringBuilder);
    begin
        Buffer.AppendLine(Format('Value=%s', [Value])); // 内存操作
    end;
    
    procedure SaveToFile(FileName: string);
    var
        Buffer: TStringBuilder;
        Writer: TStreamWriter;
    begin
        Buffer := TStringBuilder.Create(1024*1024); // 预分配1MB
        try
            BuildConfigString(Buffer);  // 递归生成内容
            Writer := TStreamWriter.Create(FileName);
            Writer.Write(Buffer.ToString); // 单次I/O
        finally
        Buffer.Free;
        Writer.Free; 
        end;
    end;
    
  4. 优化树形遍历

    procedure TraverseNode(Node: TTreeNode; var Buffer: TStringBuilder);
    begin
        Buffer.Append(Node.Header); // 避免递归中的反复内存分配
        foreach Child in Node.Children do
            TraverseNode(Child, Buffer); // 传递缓冲引用
    end;
    

优化效果

指标优化前优化后测试环境
100M 配置保存时间20.4s2.1s7200RPM HDD
CPU 峰值使用率92%18%i5-6500
磁盘写入次数10w+n/

总结

总结以下本次优化过程给我带来的启示:

  1. 资源的创建和释放次数 : 特别是树状数据结构,存在递归操作时
  2. 字符串拼接优化:使用 TStringBuilder 替代 + 操作
  3. 缓冲机制的重要性:减少磁盘 I/O 次数
  4. 性能测试的必要性:进行多次性能测试,验证优化效果