作者:Xuanwo

Databend Labs 成员,数据库研发工程师

github.com/xuanwo

Rust std fs 比 Python 慢!真的吗!?

我即将共享一个冗长的故事,从 OpenDALop.read()开端,以一个意想不到的转折完毕。这个过程对我来说十分有启发性,我期望你也能感受到。我会极力重现这个阅历,并附上我一路学到的教训。让咱们开端吧!

一切的代码片段和脚本都能够在 Xuanwo/when-i-find-rust-is-slow 中找到。

OpenDAL Python 绑定比 Python 慢?

OpenDAL 是一个数据拜访层,允许用户以统一的方式从各种存储服务中轻松高效地获取数据。咱们经过 pyo3 为 OpenDAL 供给了 python 绑定。

有一天,@beldathas 在 discord 向我陈述了一个案例,即 OpenDAL 的 python 绑定比 python 慢:

importpathlib
importtimeit
importopendal
root=pathlib.Path(__file__).parent
op=opendal.Operator("fs",root=str(root))
filename="lorem_ipsum_150mb.txt"
defread_file_with_opendal()->bytes:
withop.open(filename,"rb")asfp:
result=fp.read()
returnresult
defread_file_with_normal()->bytes:
withopen(root/filename,"rb")asfp:
result=fp.read()
returnresult
if__name__=="__main__":
print("normal:",timeit.timeit(read_file_with_normal,number=100))
print("opendal:",timeit.timeit(read_file_with_opendal,number=100))

成果显现

(venv)$pythonbenchmark.py
normal:4.470868484000675
opendal:8.993250704006641

Emmm,我对这些成果有点尴尬。以下是一些快速的假定:

  • Python 是否有内部缓存能够重复运用相同的内存?

  • Python 是否具有加速文件读取的一些技巧?

  • PyO3 是否引入了额定的开支?

我将代码重构如下:

python-fs-read

withopen("/tmp/file","rb")asfp:
result=fp.read()
assertlen(result)==64*1024*1024

python-opendal-read

importopendal
op=opendal.Operator("fs",root=str("/tmp"))
result=op.read("file")
assertlen(result)==64*1024*1024

成果显现,Python 比 OpenDAL 快得多:

Benchmark1:python-fs-read/test.py
Time(mean):15.9ms0.7ms[User:5.6ms,System:10.1ms]
Range(minmax):14.9ms…21.6ms180runs

Benchmark2:python-opendal-read/test.py
Time(mean):32.9ms1.3ms[User:6.1ms,System:26.6ms]
Range(minmax):31.4ms…42.6ms85runs

Summary
python-fs-read/test.pyran
2.070.12timesfasterthanpython-opendal-read/test.py

OpenDAL 的 Python 绑定好像比 Python 自身运转得更慢,这并不是个好消息。让咱们来探求其背面的原因。

OpenDAL Fs 服务比 Python 慢?

这个谜题涉及到许多元素,如 rust、opendal、python、pyo3 等。让咱们集中精力测验找出根本原因。

我在 rust 中经过 opendal fs 服务完成了相同的逻辑:

rust-opendal-fs-read

usestd::io::Read;
useopendal::services::Fs;
useopendal::Operator;
fnmain(){
letmutcfg=Fs::default();
cfg.root("/tmp");
letop=Operator::new(cfg).unwrap().finish().blocking();
letmutbs=vec![0;64*1024*1024];
letmutf=op.reader("file").unwrap();
letmutts=0;
loop{
letbuf=&mutbs[ts..];
letn=f.read(buf).unwrap();
letn=nasusize;
ifn==0{
break
}
ts =n;
}
assert_eq!(ts,64*1024*1024);
}

可是,成果显现即使 opendal 是用 rust 完成的,它的速度依然比 python 慢:

Benchmark1:rust-opendal-fs-read/target/release/test
Time(mean):23.8ms2.0ms[User:0.4ms,System:23.4ms]
Range(min…max):21.8ms…34.6ms121runs

Benchmark2:python-fs-read/test.py
Time(mean):15.6ms0.8ms[User:5.5ms,System:10.0ms]
Range(min…max):14.4ms…20.8ms166runs

Summary
python-fs-read/test.pyran
1.520.15timesfasterthanrust-opendal-fs-read/target/release/test

虽然 rust-opendal-fs-read的表现略优于 python-opendal-read,这暗示了在绑定和 pyo3 中有改善的空间,但这些并非中心问题。咱们需求进一步深化探求。

啊,opendal fs 服务比 python 慢。

Rust std fs 比 Python 慢?

OpenDAL 经过 std::fs 完成文件体系服务。OpenDAL 自身会产生额定的开支吗?

我运用 std::fs 在 Rust 中完成了相同逻辑:

rust-std-fs-read

usestd::io::Read;
usestd::fs::OpenOptions;
fnmain(){
letmutbs=vec![0;64*1024*1024];
letmutf=OpenOptions::new().read(true).open("/tmp/file").unwrap();
letmutts=0;
loop{
letbuf=&mutbs[ts..];
letn=f.read(buf).unwrap();
letn=nasusize;
ifn==0{
break
}
ts =n;
}
assert_eq!(ts,64*1024*1024);
}

可是:

Benchmark1:rust-std-fs-read/target/release/test
Time(mean):23.1ms2.5ms[User:0.3ms,System:22.8ms]
Range(min…max):21.0ms…37.6ms124runs

Benchmark2:python-fs-read/test.py
Time(mean):15.2ms1.1ms[User:5.4ms,System:9.7ms]
Range(min…max):14.3ms…21.4ms178runs
Summary
python-fs-read/test.pyran
1.520.20timesfasterthanrust-std-fs-read/target/release/test

哇,Rust 的 std fs 比 Python 还慢?这怎么或许呢?无意冒犯,可是这怎么或许呢?

Rust std fs 比 Python 还慢?真的吗!?

我无法相信这个成果:Rust std fs 的速度居然比 Python 还要慢。

我测验学会了怎么运用 strace进行体系调用剖析。strace是一个 Linux 体系调用追踪器,它让咱们能够监控体系调用并理解其过程。

strace 将包括程序发出的一切体系调用。咱们应该重视与/tmp/file相关的方面。每一行 strace 输出都以体系调用称号开端,后跟输入参数和输出。

比方:

openat(AT_FDCWD,"/tmp/file",O_RDONLY|O_CLOEXEC)=3

这意味着咱们运用参数 AT_FDCWD"/tmp/file"O_RDONLY|O_CLOEXEC调用 openat体系调用。这将回来输出 3 ,这是在后续的体系调用中引证的文件描述符。

好了,咱们现已把握了 strace。让咱们开端运用它吧!

rust-std-fs-read 的 strace:

>strace./rust-std-fs-read/target/release/test
...
mmap(NULL,67112960,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0)=0x7f290dd40000
openat(AT_FDCWD,"/tmp/file",O_RDONLY|O_CLOEXEC)=3
read(3,"tP201A225366>260270R365313220{E372274635"353204220s2|7C2052656263"...,67108864)=67108864
read(3,"",0)=0
close(3)=0
munmap(0x7f290dd40000,67112960)=0
...

python-fs-read 的 strace:

>strace./python-fs-read/test.py
...
openat(AT_FDCWD,"/tmp/file",O_RDONLY|O_CLOEXEC)=3
newfstatat(3,"",{st_mode=S_IFREG|0644,st_size=67108864,...},AT_EMPTY_PATH)=0
ioctl(3,TCGETS,0x7ffe9f844ac0)=-1ENOTTY(Inappropriateioctlfordevice)
lseek(3,0,SEEK_CUR)=0
lseek(3,0,SEEK_CUR)=0
newfstatat(3,"",{st_mode=S_IFREG|0644,st_size=67108864,...},AT_EMPTY_PATH)=0
mmap(NULL,67112960,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0)=0x7f13277ff000
read(3,"tP201A225366>260270R365313220{E372274635"353204220s2|7C2052656263"...,67108865)=67108864
read(3,"",1)=0
close(3)=0
rt_sigaction(SIGINT,{sa_handler=SIG_DFL,sa_mask=[],sa_flags=SA_RESTORER|SA_ONSTACK,sa_restorer=0x7f132be5c710},{sa_handler=0x7f132c17ac36,sa_mask=[],sa_flags=SA_RESTORER|SA_ONSTACK,sa_restorer=0x7f132be5c710},8)=0
munmap(0x7f13277ff000,67112960)=0
...

从剖析strace来看,很明显 python-fs-read 的体系调用比 rust-std-fs-read 多,两者都利用了mmap。那为什么 Python 要比 Rust 更快呢?

咱们这儿为什么用了 mmap

我最初以为mmap仅用于将文件映射到内存,然后经过内存拜访文件。可是,mmap还有其他用途。它通常被用来为应用程序分配大块的内存区域。

这能够在 strace 的成果中看到:

mmap(NULL,67112960,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0)=0x7f13277ff000

这个体系调用的意义是

  • NULL:第一个参数表示要映射的内存区域的起始地址。NULL将让操作体系为咱们选择一个适宜的地址。

  • 67112960:要映射的内存区域的巨细。咱们在这儿分配 64MiB 4KiB 内存,额定的页面用于存储此内存区域的元数据。

  • PROT_READ|PROT_WRITE:该内存区域可读写。

  • MAP_PRIVATE|MAP_ANONYMOUS:

  • MAP_PRIVATE意味着对此内存区域进行更改不会对其他映射相同区域的进程可见,而且不会传递到底层文件(假如有)。

  • MAP_ANONYMOUS意味着咱们正在分配与文件无关联匿名内存.

  • -1: 要的映射文件描述符. -1 表示咱们没有映射文件。

  • 0: 文件中要从哪个偏移量开端映射. 咱们并没有映射文件,所以运用 0

可是咱们代码里没有调用 mmap 啊?

mmap体系调用由glibc分配。咱们运用malloc向体系恳求内存,作为回应, glibc采用了 brkmmap 体系调用来依据咱们的恳求巨细分配内存。假如恳求的巨细足够大,那么 glibc 会选择运用 mmap, 这有助于缓解内存碎片问题。

默许情况下,一切以目标 x86_64-unknown-linux-gnu 编译的 Rust 程序都运用由 glibc 供给的 malloc 完成。

Python 和 Rust 是否运用相同的内存分配器?

默许情况下,Python 运用pymalloc,这是一个针对小型分配进行优化的内存分配器。Python 具有三个内存域,每个代表不同的分配战略,并针对各种目的进行了优化。

pymalloc 有如下行为:

Python has a pymalloc allocator optimized for small objects (smaller or equal to 512 bytes) with a short lifetime. It uses memory mappings called “arenas” with a fixed size of either 256 KiB on 32-bit platforms or 1 MiB on 64-bit platforms. It falls back to PyMem_RawMalloc() and PyMem_RawRealloc() for allocations larger than 512 bytes.

Rust 默许的内存分配器比 Python 慢吗?

我怀疑mmap是导致这个问题的原因。假如我切换到jemalloc,会发生什么情况?

rust-std-fs-read-with-jemalloc

usestd::io::Read;
usestd::fs::OpenOptions;
#[global_allocator]
staticGLOBAL:jemallocator::Jemalloc=jemallocator::Jemalloc;
fnmain(){
letmutbs=vec![0;64*1024*1024];
letmutf=OpenOptions::new().read(true).open("/tmp/file").unwrap();
letmutts=0;
loop{
letbuf=&mutbs[ts..];
letn=f.read(buf).unwrap();
letn=nasusize;
ifn==0{
break
}
ts =n;
}
assert_eq!(ts,64*1024*1024);
}

Wooooooooooooooow?!

Benchmark1:rust-std-fs-read-with-jemalloc/target/release/test
Time(mean):9.7ms0.6ms[User:0.3ms,System:9.4ms]
Range(min…max):9.0ms…12.4ms259runs

Benchmark2:python-fs-read/test.py
Time(mean):15.8ms0.9ms[User:5.9ms,System:9.8ms]
Range(min…max):15.0ms…21.8ms169runs
Summary
rust-std-fs-read-with-jemalloc/target/release/testran
1.640.14timesfasterthanpython-fs-read/test.py

什么?!我知道 jemalloc是一个高效的内存分配器,但它为啥会这么优异呢?

只要在我的电脑上,Rust 运转速度比 Python 慢!

随着更多的朋友参加评论,咱们发现只要在我的机器上,Rust运转速度比Python慢。

我的 CPU:

>lscpu
Architecture:x86_64
CPUop-mode(s):32-bit,64-bit
Addresssizes:48bitsphysical,48bitsvirtual
ByteOrder:LittleEndian
CPU(s):32
On-lineCPU(s)list:0-31
VendorID:AuthenticAMD
Modelname:AMDRyzen95950X16-CoreProcessor
CPUfamily:25
Model:33
Thread(s)percore:2
Core(s)persocket:16
Socket(s):1
Stepping:0
Frequencyboost:enabled
CPU(s)scalingMHz:53%
CPUmaxMHz:5083.3979
CPUminMHz:2200.0000
BogoMIPS:6787.49
Flags:fpuvmedepsetscmsrpaemcecx8apicsepmtrrpgemcacmovpatpse36clflushmmxfxsrssesse2htsyscallnxmmxextfxsr_optpdpe1gbrdtscplmcon
stant_tscrep_goodnoplnonstop_tsccpuidextd_apicidaperfmperfraplpnipclmulqdqmonitorssse3fmacx16sse4_1sse4_2movbepopcntaesxsaveavxf
16crdrandlahf_lmcmp_legacysvmextapiccr8_legacyabmsse4amisalignsse3dnowprefetchosvwibsskinitwdttcetopoextperfctr_coreperfctr_nbbpex
tperfctr_llcmwaitxcpbcat_l3cdp_l3hw_pstatessbdmbaibrsibpbstibpvmmcallfsgsbasebmi1avx2smepbmi2ermsinvpcidcqmrdt_ardseedadxsmap
clflushoptclwbsha_nixsaveoptxsavecxgetbv1xsavescqm_llccqm_occup_llccqm_mbm_totalcqm_mbm_localuser_shstkclzeroirperfxsaveerptrrdpruwb
noinvdaratnptlbrvsvm_locknrip_savetsc_scalevmcb_cleanflushbyasiddecodeassistspausefilterpfthresholdavicv_vmsave_vmloadvgifv_spec_ctrl
umippkuospkevaesvpclmulqdqrdpidoverflow_recovsuccorsmcafsrmdebug_swap
Virtualizationfeatures:
Virtualization:AMD-V
Caches(sumofall):
L1d:512KiB(16instances)
L1i:512KiB(16instances)
L2:8MiB(16instances)
L3:64MiB(2instances)
NUMA:
NUMAnode(s):1
NUMAnode0CPU(s):0-31
Vulnerabilities:
Gatherdatasampling:Notaffected
Itlbmultihit:Notaffected
L1tf:Notaffected
Mds:Notaffected
Meltdown:Notaffected
Mmiostaledata:Notaffected
Retbleed:Notaffected
Specrstackoverflow:Vulnerable
Specstorebypass:Vulnerable
Spectrev1:Vulnerable:__userpointersanitizationandusercopybarriersonly;noswapgsbarriers
Spectrev2:Vulnerable,IBPB:disabled,STIBP:disabled,PBRSB-eIBRS:Notaffected
Srbds:Notaffected
Tsxasyncabort:Notaffected

我的内存:

>sudodmidecode--typememory
#dmidecode3.5
GettingSMBIOSdatafromsysfs.
SMBIOS3.3.0present.
Handle0x0014,DMItype16,23bytes
PhysicalMemoryArray
Location:SystemBoardOrMotherboard
Use:SystemMemory
ErrorCorrectionType:None
MaximumCapacity:64GB
ErrorInformationHandle:0x0013
NumberOfDevices:4
Handle0x001C,DMItype17,92bytes
MemoryDevice
ArrayHandle:0x0014
ErrorInformationHandle:0x001B
TotalWidth:64bits
DataWidth:64bits
Size:16GB
FormFactor:DIMM
Set:None
Locator:DIMM0
BankLocator:P0CHANNELA
Type:DDR4
TypeDetail:SynchronousUnbuffered(Unregistered)
Speed:3200MT/s
Manufacturer:Unknown
SerialNumber:04904740
AssetTag:NotSpecified
PartNumber:LMKUFG68AHFHD-32A
Rank:2
ConfiguredMemorySpeed:3200MT/s
MinimumVoltage:1.2V
MaximumVoltage:1.2V
ConfiguredVoltage:1.2V
MemoryTechnology:DRAM
MemoryOperatingModeCapability:Volatilememory
FirmwareVersion:Unknown
ModuleManufacturerID:Bank9,Hex0xC8
ModuleProductID:Unknown
MemorySubsystemControllerManufacturerID:Unknown
MemorySubsystemControllerProductID:Unknown
Non-VolatileSize:None
VolatileSize:16GB
CacheSize:None
LogicalSize:None

所以我测验了以下事情:

‍ 开启 Mitigations

CPU 具有许多或许将私有数据露出给攻击者的缝隙,其间Spectre是最闻名的之一。Linux 内核现已开发了各种缓解这些缝隙的措施,而且默许启用它们。可是,这些缓解措施或许会添加额定的体系本钱。因此,Linux 内核也为期望禁用它们的用户供给了一个mitigations开关。

我曩昔禁用了一切的 mitigations:

titleArchLinux
linux/vmlinuz-linux-zen
initrd/amd-ucode.img
initrd/initramfs-linux-zen.img
optionsroot="PARTUUID=206e7750-2b89-419d-978e-db0068c79c52"rwmitigations=off

启用它并不能改动成果

‍ 调整透明大页

透明大页能够显著影响功能。大多数现代发行版默许启用它。

>cat/sys/kernel/mm/transparent_hugepage/enabled
[always]madvisenever

切换到 madvisenever会改动肯定成果,但相对份额保持一致。

‍ Tune CPU 中心亲和度

@Manjusaka 猜想这或许与 CPU中心距离 有关。我企图运用 core_affinity将进程绑定到特定的CPU,但成果依然相同。

运用 eBPF 准确测量 syscall 推迟

@Manjusaka 也为我创建了 一个eBPF程序,以便我衡量读取体系调用的推迟。研讨成果表明,Rust在体系调用级别上就比Python慢。

@Manjusaka 写一篇文章来共享关于这个 eBPF 程序的故事!

   #pythonfsread
   Process57555readfile8134049ns
   Process57555readfile942ns
   #ruststdfsread
   Process57634readfile24636975ns
   Process57634readfile1052ns

调查:在我的电脑上,Rust 运转速度比 Python 慢,而且这好像与软件无关。

C 比 Python 慢?

当用户想要进行大数据剖析时,心里所期望的基本是:

我感到相当困惑,无法准确指出差异。我怀疑这或许与 CPU 有关,但我不确定是哪个方面:缓存?频率?核距离?核亲和性?架构?

依据 Telegram 群组 Rust 众 的主张,我开发了一个C版本:

c-fs-read

#include<stdio.h>
#include<stdlib.h>
#defineFILE_SIZE64*1024*1024//64MiB
intmain(){
FILE*file;
char*buffer;
size_tresult;
file=fopen("/tmp/file","rb");
if(file==NULL){
fputs("Erroropeningfile",stderr);
return1;
}
buffer=(char*)malloc(sizeof(char)*FILE_SIZE);
if(buffer==NULL){
fputs("Memoryerror",stderr);
fclose(file);
return2;
}
result=fread(buffer,1,FILE_SIZE,file);
if(result!=FILE_SIZE){
fputs("Readingerror",stderr);
fclose(file);
free(buffer);
return3;
}
fclose(file);
free(buffer);
return0;
}

可是……

Benchmark1:c-fs-read/test
Time(mean):23.8ms0.9ms[User:0.3ms,System:23.6ms]
Range(minmax):23.0ms…27.1ms120runs
Benchmark2:python-fs-read/test.py
Time(mean):19.1ms0.3ms[User:8.6ms,System:10.4ms]
Range(minmax):18.6ms…20.6ms146runs
Summary
python-fs-read/test.pyran
1.250.05timesfasterthanc-fs-read/test

C 版本也比 Python 慢!Python 有魔法吗?

在指定的偏移量下,C 言语比 Python 慢!

当用户想要进行大数据剖析时,心里所期望的基本是:

在这个时分,@lilydjwg 参加了评论,并注意到 C 和 Python 之间的内存区域偏移存在差异。

strace -e raw=read,mmap ./program被用来打印体系调用的未解码参数:指针地址。

`c-fs-read` 的 strace:
    >strace-eraw=read,mmap./c-fs-read/test
    ...
    mmap(0,0x4001000,0x3,0x22,0xffffffff,0)=0x7f96d1a18000
    read(0x3,0x7f96d1a18010,0x4000000)=0x4000000
    close(3)=0
    python-fs-read的strace
`python-fs-read` 的 strace
    >strace-eraw=read,mmap./python-fs-read/test.py
    ...
    mmap(0,0x4001000,0x3,0x22,0xffffffff,0)=0x7f27dcfbe000
    read(0x3,0x7f27dcfbe030,0x4000001)=0x4000000
    read(0x3,0x7f27e0fbe030,0x1)=0
    close(3)=0

c-fs-read 中,mmap回来 0x7f96d1a18000,可是 read 体系调用运用 0x7f96d1a18010作为起始地址,偏移量是 0x10。在 python-fs-read中, mmap 回来 0x7f27dcfbe000, 而且 read 体系调用运用 0x7f27dcfbe030 作为起始地址, 偏移量是 0x30.

所以 @lilydjwg测验用相同的偏移量来调用 ‘read’。

    :)./benchc-fs-readc-fs-read-with-offsetpython-fs-read
    ['hyperfine','c-fs-read/test','c-fs-read-with-offset/test','python-fs-read/test.py']
    Benchmark1:c-fs-read/test
    Time(mean):23.7ms0.8ms[User:0.2ms,System:23.6ms]
    Range(min…max):23.0ms…25.5ms119runs
    Warning:Statisticaloutliersweredetected.Considerre-runningthisbenchmarkonaquietsystemwithoutanyinterferencesfromotherprograms.Itmighthelptousethe'--warmup'or'--prepare'options.
    Benchmark2:c-fs-read-with-offset/test
    Time(mean):8.9ms0.4ms[User:0.2ms,System:8.8ms]
    Range(min…max):8.3ms…10.6ms283runs
    Benchmark3:python-fs-read/test.py
    Time(mean):19.1ms0.3ms[User:8.6ms,System:10.4ms]
    Range(min…max):18.6ms…20.0ms147runs
    Summary
    c-fs-read-with-offset/testran
    2.150.11timesfasterthanpython-fs-read/test.py
    2.680.16timesfasterthanc-fs-read/test

!!!

c-fs-read中对buffer应用偏移量能够进步其速度,超越 Python!此外,咱们现已验证了这个问题在 AMD Ryzen 9 5900XAMD Ryzen 7 5700X 上都能复现。

新的信息让我找到了关于类似问题的其他陈述,Std::fs::read slow?。在这篇帖子中,@ambiso 发现体系调用功能与内存区域的偏移量有关。他指出当从每页的前 0x10 字节写入时,这款 CPU 会变慢。

    offsetmilliseconds
    ...
    14130
    15130
    1646<-----0x10!
    1748
    ...

在指定的偏移量下,AMD Ryzen 9 5900X 很慢!

咱们已确认这个问题与CPU有关。可是,咱们依然不确定其或许的原因。@Manjusaka已邀请内核开发者 @ryncsn 参加评论。

他能够在 AMD Ryzen 9 5900HX 上运用咱们的 c-fs-readc-fs-read-with-offset 重现相同的成果。他还测验运用 perf 对两个程序进行功能剖析。

没有 offset:

perfstat-d-d-d--repeat20./a.out
Performancecounterstatsfor'./a.out'(20runs):
30.89msectask-clock#0.968CPUsutilized( -1.35%)
0context-switches#0.000/sec
0cpu-migrations#0.000/sec
598page-faults#19.362K/sec( -0.05%)
90,321,344cycles#2.924GHz( -1.12%)(40.76%)
599,640stalled-cycles-frontend#0.66%frontendcyclesidle( -2.19%)(42.11%)
398,016stalled-cycles-backend#0.44%backendcyclesidle( -22.41%)(41.88%)
43,349,705instructions#0.48insnpercycle
#0.01stalledcyclesperinsn( -1.32%)(41.91%)
7,526,819branches#243.701M/sec( -5.01%)(41.22%)
37,541branch-misses#0.50%ofallbranches( -4.62%)(41.12%)
127,845,213L1-dcache-loads#4.139G/sec( -1.14%)(39.84%)
3,172,628L1-dcache-load-misses#2.48%ofallL1-dcacheaccesses( -1.34%)(38.46%)
<notsupported>LLC-loads
<notsupported>LLC-load-misses
654,651L1-icache-loads#21.196M/sec( -1.71%)(38.72%)
2,828L1-icache-load-misses#0.43%ofallL1-icacheaccesses( -2.35%)(38.67%)
15,615dTLB-loads#505.578K/sec( -1.28%)(38.82%)
12,825dTLB-load-misses#82.13%ofalldTLBcacheaccesses( -1.15%)(38.88%)
16iTLB-loads#518.043/sec( -27.06%)(38.82%)
2,202iTLB-load-misses#13762.50%ofalliTLBcacheaccesses( -23.62%)(39.38%)
1,843,493L1-dcache-prefetches#59.688M/sec( -3.36%)(39.40%)
<notsupported>L1-dcache-prefetch-misses
0.031915 -0.000419secondstimeelapsed( -1.31%)

有 offset:

perfstat-d-d-d--repeat20./a.out
Performancecounterstatsfor'./a.out'(20runs):
15.39msectask-clock#0.937CPUsutilized( -3.24%)
1context-switches#64.972/sec( -17.62%)
0cpu-migrations#0.000/sec
598page-faults#38.854K/sec( -0.06%)
41,239,117cycles#2.679GHz( -1.95%)(40.68%)
547,465stalled-cycles-frontend#1.33%frontendcyclesidle( -3.43%)(40.60%)
413,657stalled-cycles-backend#1.00%backendcyclesidle( -20.37%)(40.50%)
37,009,429instructions#0.90insnpercycle
#0.01stalledcyclesperinsn( -3.13%)(40.43%)
5,410,381branches#351.526M/sec( -3.24%)(39.80%)
34,649branch-misses#0.64%ofallbranches( -4.04%)(39.94%)
13,965,813L1-dcache-loads#907.393M/sec( -3.37%)(39.44%)
3,623,350L1-dcache-load-misses#25.94%ofallL1-dcacheaccesses( -3.56%)(39.52%)
<notsupported>LLC-loads
<notsupported>LLC-load-misses
590,613L1-icache-loads#38.374M/sec( -3.39%)(39.67%)
1,995L1-icache-load-misses#0.34%ofallL1-icacheaccesses( -4.18%)(39.67%)
16,046dTLB-loads#1.043M/sec( -3.28%)(39.78%)
14,040dTLB-load-misses#87.50%ofalldTLBcacheaccesses( -3.24%)(39.78%)
11iTLB-loads#714.697/sec( -29.56%)(39.77%)
3,657iTLB-load-misses#33245.45%ofalliTLBcacheaccesses( -14.61%)(40.30%)
395,578L1-dcache-prefetches#25.702M/sec( -3.34%)(40.10%)
<notsupported>L1-dcache-prefetch-misses
0.016429 -0.000521secondstimeelapsed( -3.17%)

他发现L1-dcache-prefetchesL1-dcache-loads的值差异很大。

  • L1-dcache-prefetches是 CPU L1 数据缓存的预取。

  • L1-dcache-loads是 CPU L1 数据缓存的加载。

假如没有指定偏移量,CPU 将执行更多的加载和预取操作,导致体系调用时刻添加。

他对热门 ASM 进行了进一步研讨:

Samples:15Kofevent'cycles:P',Eventcount(approx.):6078132137
ChildrenSelfCommandSharedObjectSymbol
-94.11%0.00%a.out[kernel.vmlinux][k]entry_SYSCALL_64_after_hwframe
-entry_SYSCALL_64_after_hwframe
-94.10%do_syscall_64
-86.66%__x64_sys_read
ksys_read
-vfs_read
-85.94%shmem_file_read_iter
-77.17%copy_page_to_iter
-75.80%_copy_to_iter
 19.41%asm_exc_page_fault
0.71%__might_fault
 4.87%shmem_get_folio_gfp
0.76%folio_mark_accessed
 4.38%__x64_sys_munmap
 1.02%0xffffffffae6f6fe8
 0.79%__x64_sys_execve
 0.58%__x64_sys_mmap

_copy_to_iter 中的 ASM:


    │copy_user_generic():
    2.19│mov%rdx,%rcx
    │mov%r12,%rsi
    92.45│repmovsb%ds:(%rsi),%es:(%rdi)
    0.49│nop
    │nop
    │nop

这儿的关键区别是rep movsb的功能。

AMD Ryzen 9 5900X 因为 FSRM 慢!

在这个时分,我的一个朋友给我发送了一个关于Terrible memcpy performance on Zen 3 when using rep movsb的链接。其间也指向了rep movsb

I’ve found this using a memcpy benchmark at github.com/ska-sa/katg… (compiled with the adjacent Makefile). To demonstrate the issue, run

./memcpy_loop -b 2113 -p 1000000 -t mmap -S 0 -D 1 0

This runs:

  • •2113-byte memory copies

  • •1,000,000 times per timing measurement

  • •in memory allocated with mmap

  • •with the source 0 bytes from the start of the page

  • •with the destination 1 byte from the start of the page

  • •on core 0.

It reports about 3.2 GB/s. Change the -b argument to 2111 and it reports over 100 GB/s. So the REP MOVSB case is about 30 slower!

FSRM,即 Fast Short REP MOV,是英特尔最初的创新,近期也被AMD采用,用以提升 rep movsbrep movsd 的速度。它旨在进步很多内存复制的功率。声明支持它的CPU将在 glibc 中默许运用 FSRM

@ryncsn 进一步研讨并发现它与 L1 预取无关。

It seems that rep movsb performance poorly when DATA IS PAGE ALIGNED, and perform better when DATA IS NOT PAGE ALIGNED, this is very funny…

总结

总的来说,这个问题并非与软件有关。因为 AMD 的一个过错,Python 在功能上超越了 C/Rust。(我终于能够好好睡觉了。)

可是,咱们的用户依然需求面临这个问题。不幸的是,像FSRM这样的功能将会被完成在ucode中,咱们别无选择只能等待 AMD 的回应。另一种或许的处理方案是不运用FSRM或许供给一个标志来禁用它。Rust 开发者或许会考虑切换到 jemallocator以进步功能 ,即使没有 AMD CPU Bug 存在,这也是一个好主意!

回忆

我花了近三天的时刻来处理这个问题,它始于 opendal: 用户的投诉,并终究引导我到CPU的微代码。这次旅程让我对straceperfeBPF有了深化的了解。这是我第一次运用 eBPF进行确诊。我还探究了各种收效甚微的途径,比方研讨 rust 的 std::fs 和 Python & CPython 的读取完成细节。起先,我期望能在更高层面上处理这个问题,但发现有必要深化挖掘。

对于一切参加寻找答案的人,我表示衷心感谢:

  • 感谢 opendal 的 Discord 上的 @beldathas 发现了这个问题。

  • 感谢 @datafuselabs 团队供给的主张。

  • 感谢咱们在 Rust 众的朋友们给出的主张和复现努力。

  • 感谢 @Manjusaka 复现问题并运用eBPF进行调查,这帮助咱们将问题定位到体系调用自身。

  • 感谢 @lilydjwg 找出根本原因:内存中0x20偏移量 -感谢 @ryncsn 他对此事进行了完全剖析。

  • •还有一位共享了关于 FSRM 有用链接的朋友。

等待咱们下次旅程!

引证

关于Databend

Databend 是一款开源、弹性、低本钱,根据对象存储也能够做实时剖析的新式数仓。等待您的重视,一同探究云原生数仓处理方案,打造新一代开源 Data Cloud。

‍‍ Databend Cloud:databend.cn

Databend 文档:databend.rs/

Wechat:Databend

✨ GitHub:github.com/datafuselab…