解决【Windows+Delphi+多线程+String】效率低的问题

(一)问题现象

某Delphi程序用10个线程,分别读取10个不同的文本文件,逐行读取。
发现整体速度和仅用1个线程顺序读完10个文件的速度差不多,整体速度很慢。
观察CPU占用也差不多,占用率都很低,

最早没细想,以为是Pascal语言就这个速度。
但后来发现几乎同样的代码,用Lazarus(FPC)编译后,速度就快了很多,基本上接近Go语言的速度了。
这才确定是Delphi(而不是Pascal)的问题。

严格的说这是两个问题

  1. 按行读取文本慢。
  2. 多线程和单线程速度一样(多线程效率未提升)。

(1.1)按行读取文本慢

这个问题其实主要是用 ReadLn() 的方式读取 TextFile
如果你的Delphi版本提供了TStreamReader,那么用它会快得多。
仅考虑Ansi编码的话还可以参考我的:🔗《提升老版本Delphi按行读取文本文件的效率》TStreamReader还快一点点呢。
总之这个问题不是本文的重点。

(1.2)多线程和单线程速度一样

这个问题仅出现在同时满足下面4个条件的情况下:

  1. Windows
  2. Delphi (D7 - D11 现象一样)
  3. 多线程
  4. 字符串处理(String)

也就是说换成Linux,用其它语言,或者Delphi多线程网络通信,都是没问题的。

这有个以前测试的图标,大概能看出鱼丸粗面组合,啊不,上面4项组合情况下到底多慢。
主机配置差异大,所以横向对比主机没意义,主要看语言/线程的差异。
所有语言都是最基础的编写方式,比如都是用String读写,没做任何优化。

看到Go时,我能听到Delphier心碎的声音……
PS:后来又看到单线程Python更受打击……
在这里插入图片描述

(二)原因分析

省略中间过程……

总之最后经过求助,通过论坛上各位热心同学讨论和帮助,以及实际程序验证。
发现是Delphi使用的内存管理在多线程下效率有问题,无法充分利用CPU多核心。

(三)解决办法

最简单就是替换默认内存管理

(3.1)FastMM5

主页:🔗https://github.com/pleriche/FastMM5

使用FastMM5,在项目最前面加入FastMM5引用就可以了。

program MyAPP;

uses
  FastMM5,
  SysUtils,
  ......

FastMM5是双协议,
你可以选择在GPL v3许可证的限制下免费使用它,
或者付费进行商业软件(非开源)的开发。

这有个对比

PS:我测试中未发现不同的模式对速度/内存占用的影响。

自己实测多线程速度有少量的提升


(3.2)FastMM4

主页:🔗https://github.com/pleriche/FastMM4
据说Delphi新版就是用的FastMM4呢。

使用FastMM4,在项目最前面加入FastMM4引用就可以了。
但是如果不进行任何设置,则没有任何效果,需要修改FastMM4Options.inc文件中的配置。

就是打开NeverSleepOnThreadContention,打开的方式如下,简单说如果前面有个.就去掉。

......
{Enable this option to not call Sleep when a thread contention occurs. This
 option will improve performance if the ratio of the number of active threads
 to the number of CPU cores is low (typically < 2). With this option set a
 thread will usually enter a "busy waiting" loop instead of relinquishing its
 timeslice when a thread contention occurs, unless UseSwitchToThread is
 also defined (see below) in which case it will call SwitchToThread instead of
 Sleep.}
{$define NeverSleepOnThreadContention}
......

关于这个选项的一些讨论

1)这个选项默认关闭是有原因的,只在特定的情况下有效。
2)应该只在线程数低于内核数(真实内核,而不是超线程内核)时使用它。

自己实测多线程速度有少量的提升


(3.3)ScaleMM2

主页:🔗https://github.com/andremussche/scalemm

使用ScaleMM2,在项目最前面加入ScaleMM2引用就可以了。

自己实测多线程速度有很大的提升,接近Go的速度,追平Lazarus (FPC)的效果了。
但是程序 内存消耗 增加了1/3到1/4……!!!
所以小心,对于有些内存吃紧的情况,由于物理内存用完而用到虚拟内存时,是会大幅降低程序速度的。


(3.4)TCMalloc

由Google发布的Thread-Caching Malloc 线程缓存型内存分配机制。
它为每一个线程都缓存一些可分配内存,因此在多线程场景下,TCMalloc能够尽可能规避多个线程同时分配/释放内存时的锁争用问题,这使得TCMalloc相较于其它内存分配机制,内存分配和回收速度更快。

💡不是Pascal而是C++实现,可用于Linux(吧?)。

【Google Performance Tools】仓库:🔗https://github.com/gperftools/gperftools
可以用Visual Studio自己编译出libtcmalloc.dll(我喜欢自己都试一下)——注意区分64或32位。

【tcmalloc】仓库:🔗https://github.com/google/tcmalloc
这怎么肥四?声明:这不是一个谷歌官方支持的产品……

懒得折腾直接下载现成的:
比如这里:🔗https://github.com/obones/tcmalloc-delphi 有32/64位的Windows下的DLL,以及Delphi的接口单元。

接口单元实现不只一个,可以找别人的,也可以自己写(有现成的干嘛要自己写?)

使用TCMalloc(libtcmalloc.dll):

  1. 在项目最前面加入TCMalloc单元引用。
  2. 并将DLL文件放入程序所在目录,或操作系统目录(x64放system32,x86放syswow64目录)。

自己实测多线程速度有较大的提升


(3.5)TBBMalloc

由Intel发布的Threading Building Blocks Malloc 属于Intel oneAPI 线程构建模块。
由灵活的 C++ 库简化了应用程序添加并行性工作的复杂性。

💡不是Pascal而是C++实现,可用于Linux。

【官方介绍】页面:🔗 https://www.intel.cn/content/www/cn/zh/developer/tools/oneapi/onetbb.html (不是中文)
【oneTBB】仓库:🔗https://github.com/oneapi-src/oneTBB

呃,怎么都找不到现成的接口单元项目呢……
只好自己上传了一个 🔗https://download.csdn.net/download/ddrfan/86723070

使用TBBMalloc(libtbbmalloc.dll):

  1. 在项目最前面加入TBBMalloc单元引用。
  2. 并将DLL文件放入程序所在目录,或操作系统目录(x64放system32,x86放syswow64目录)。

自己实测多线程速度有较大的提升


(3.6)避免使用字符串类型

手动修改代码,避免使用String。

比如ReadLN -> BlockRead
用固定大小的缓冲区+字符串指针pchar代替String
用固定长度的字符指针数组代替StringList
尽可能传递指针(地址/引用)而不是复制数据,等等…… 总之就是自行优化。

类似把C++程序改为纯C实现……

道理都懂,但是方便性下降得厉害,俺是快速开发工具啊。


(3.7)改用 Free Pascal

之前居然忘了这一段最重要的,赶紧补上。
使用 Lazarus + Free Pascal Compiler 完全没效率问题(参考下方对比表格)
主页:🔗https://www.lazarus-ide.org/

也可以用 CodeTyphon(相当于加上丰富的组件大礼包)。
主页:🔗https://www.pilotlogic.com/sitejoom/

都是Pascal语言,程序移植简单(如果没有用到大量第三方组件的话 😄 )


(3.8)换开发语言

换种开发语言,比如golang 📖 不用任何技巧,快得要死!
主页:🔗https://golang.google.cn/


(四)总结和实测

似乎没有Delphi下完美的解决方案。
全面不用String在实际项目中难以做到。
速度/内存/复杂度不能兼得,用动态库得考虑部署。

(4.1)性能

下面是个测试例子,除了最基本用了单线程。
内存管理均为4个线程同时读取4个文本文件,
读出每一行用Tab符号拆分为列表(文件大概750MB)

Delphi读取类型处理时间(Win_x64)处理时间(Linux_x64)单位
ReadLn (单线程)1614.3
ReadLn15.36.3
TStreamReader(单线程)5.58.4
TStreamReader5.83.5
TStreamReader + FastMM54.1不适用
TStreamReader + FastMM4(开选项)4.2不适用
TStreamReader + ScaleMM22.5不适用
TStreamReader + TCMalloc + DLL2.8X
TStreamReader + TBBMalloc + DLL2.73.5
BlockRead() + array of pchar0.40.6

同样数据,同样处理,用Lazarua(FPC)多线程作为对比:

Lazarus(FPC)读取类型处理时间(Win_x64)处理时间(Linux_x64)单位
ReadLn()4.22.5
TStreamReader1.91.9
TStreamReader + TCMalloc + DLL1.7X
TStreamReader + TBBMalloc + DLL1.81.9

(4.2)内存占用

某些耗内存较多的程序,内存使用量的变化也很重要。
由于耗几十GB内存的程序处理时间太长,所以只对比了下面这个耗几个GB级别的。

虽然是对比内存,但处理速度也基本符合上面4.1测试的梯队。

最终结果内存消耗差异相当大……不得不慎重。

Delphi读取类型内存占用(Win_x86)内存占用(Win_x64)内存占用(Linux_x64)单位
普通271442114280MB
FastMM526873455不适用MB
FastMM4(开选项)26944256不适用MB
ScaleMM234946471不适用MB
TCMalloc + DLL27143490XMB
TBBMalloc + DLL/LIB235035003520MB
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值