文章目录
(一)问题现象
某Delphi程序用10个线程,分别读取10个不同的文本文件,逐行读取。
发现整体速度和仅用1个线程顺序读完10个文件的速度差不多,整体速度很慢。
观察CPU占用也差不多,占用率都很低,
最早没细想,以为是Pascal语言就这个速度。
但后来发现几乎同样的代码,用Lazarus(FPC)编译后,速度就快了很多,基本上接近Go语言的速度了。
这才确定是Delphi(而不是Pascal)的问题。
严格的说这是两个问题
- 按行读取文本慢。
- 多线程和单线程速度一样(多线程效率未提升)。
(1.1)按行读取文本慢
这个问题其实主要是用 ReadLn()
的方式读取 TextFile
。
如果你的Delphi版本提供了TStreamReader
,那么用它会快得多。
仅考虑Ansi编码的话还可以参考我的:🔗《提升老版本Delphi按行读取文本文件的效率》 比TStreamReader
还快一点点呢。
总之这个问题不是本文的重点。
(1.2)多线程和单线程速度一样
这个问题仅出现在同时满足下面4个条件的情况下:
- Windows
- Delphi (D7 - D11 现象一样)
- 多线程
- 字符串处理(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):
- 在项目最前面加入
TCMalloc
单元引用。 - 并将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.youkuaiyun.com/download/ddrfan/86723070
使用TBBMalloc(libtbbmalloc.dll):
- 在项目最前面加入
TBBMalloc
单元引用。 - 并将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 (单线程) | 16 | 14.3 | 秒 |
ReadLn | 15.3 | 6.3 | 秒 |
TStreamReader(单线程) | 5.5 | 8.4 | 秒 |
TStreamReader | 5.8 | 3.5 | 秒 |
TStreamReader + FastMM5 | 4.1 | 不适用 | 秒 |
TStreamReader + FastMM4(开选项) | 4.2 | 不适用 | 秒 |
TStreamReader + ScaleMM2 | 2.5 | 不适用 | 秒 |
TStreamReader + TCMalloc + DLL | 2.8 | X | 秒 |
TStreamReader + TBBMalloc + DLL | 2.7 | 3.5 | 秒 |
BlockRead() + array of pchar | 0.4 | 0.6 | 秒 |
同样数据,同样处理,用Lazarua(FPC)多线程作为对比:
Lazarus(FPC)读取类型 | 处理时间(Win_x64) | 处理时间(Linux_x64) | 单位 |
---|---|---|---|
ReadLn() | 4.2 | 2.5 | 秒 |
TStreamReader | 1.9 | 1.9 | 秒 |
TStreamReader + TCMalloc + DLL | 1.7 | X | 秒 |
TStreamReader + TBBMalloc + DLL | 1.8 | 1.9 | 秒 |
(4.2)内存占用
某些耗内存较多的程序,内存使用量的变化也很重要。
由于耗几十GB内存的程序处理时间太长,所以只对比了下面这个耗几个GB级别的。
虽然是对比内存,但处理速度也基本符合上面4.1测试的梯队。
最终结果内存消耗差异相当大……不得不慎重。
Delphi读取类型 | 内存占用(Win_x86) | 内存占用(Win_x64) | 内存占用(Linux_x64) | 单位 |
---|---|---|---|---|
普通 | 2714 | 4211 | 4280 | MB |
FastMM5 | 2687 | 3455 | 不适用 | MB |
FastMM4(开选项) | 2694 | 4256 | 不适用 | MB |
ScaleMM2 | 3494 | 6471 | 不适用 | MB |
TCMalloc + DLL | 2714 | 3490 | X | MB |
TBBMalloc + DLL/LIB | 2350 | 3500 | 3520 | MB |