首页 > 编程语言 >Rust std fs 比 Python 慢!真的吗!?

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

时间:2023-11-30 15:04:30浏览次数:56  
标签:std fs Python +- python read ms test

作者:Xuanwo

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

https://github.com/xuanwo

我即将分享一个冗长的故事,从 OpenDALop.read()开始,以一个意想不到的转折结束。这个过程对我来说非常有启发性,我希望你也能感受到。我会尽力重现这个经历,并附上我一路学到的教训。让我们开始吧!

所有的代码片段和脚本都可以在 Xuanwo/when-i-find-rust-is-slow 中找到。

OpenDAL Python 绑定比 Python 慢?

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

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

import pathlib
import timeit

import opendal

root = pathlib.Path(__file__).parent
op = opendal.Operator("fs", root=str(root))
filename = "lorem_ipsum_150mb.txt"

def read_file_with_opendal() -> bytes:
    with op.open(filename, "rb") as fp:
        result = fp.read()
    return result

def read_file_with_normal() -> bytes:
    with open(root / filename, "rb") as fp:
        result = fp.read()
    return result

if __name__ == "__main__":
    print("normal: ", timeit.timeit(read_file_with_normal, number=100))
    print("opendal: ", timeit.timeit(read_file_with_opendal, number=100))

结果显示

(venv) $ python benchmark.py
normal:  4.470868484000675
opendal:  8.993250704006641
    

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

  • Python 是否有内部缓存可以重复使用相同的内存?

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

  • PyO3 是否引入了额外的开销?

我将代码重构如下:

python-fs-read

with open("/tmp/file", "rb") as fp:
    result = fp.read()
assert len(result) == 64 * 1024 * 1024

python-opendal-read

import opendal

op = opendal.Operator("fs", root=str("/tmp"))

result = op.read("file")
assert len(result) == 64 * 1024 * 1024

结果显示,Python 比 OpenDAL 快得多:

Benchmark 1: python-fs-read/test.py
  Time (mean ± σ):      15.9 ms ±   0.7 ms    [User: 5.6 ms, System: 10.1 ms]
  Range (min … max):    14.9 ms …  21.6 ms    180 runs
  
Benchmark 2: python-opendal-read/test.py
  Time (mean ± σ):      32.9 ms ±   1.3 ms    [User: 6.1 ms, System: 26.6 ms]
  Range (min … max):    31.4 ms …  42.6 ms    85 runs
  
Summary
  python-fs-read/test.py ran
    2.07 ± 0.12 times faster than python-opendal-read/test.py

OpenDAL 的 Python 绑定似乎比 Python 本身运行得更慢,这并不是个好消息。让我们来探究其背后的原因。

OpenDAL Fs 服务比 Python 慢?

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

我在 rust 中通过 opendal fs 服务实现了相同的逻辑:

rust-opendal-fs-read

use std::io::Read;
use opendal::services::Fs;
use opendal::Operator;

fn main() {
    let mut cfg = Fs::default();
    cfg.root("/tmp");
    let op = Operator::new(cfg).unwrap().finish().blocking();

    let mut bs = vec![0; 64 * 1024 * 1024];

    let mut f = op.reader("file").unwrap();
    let mut ts = 0;
    loop {
        let buf = &mut bs[ts..];
        let n = f.read(buf).unwrap();
        let n = n as usize;
        if n == 0 {
            break
        }
        ts += n;
    }

    assert_eq!(ts, 64 * 1024 * 1024);
}

然而,结果显示即使 opendal 是用 rust 实现的,它的速度仍然比 python 慢:

Benchmark 1: rust-opendal-fs-read/target/release/test
  Time (mean ± σ):      23.8 ms ±   2.0 ms    [User: 0.4 ms, System: 23.4 ms]
  Range (min … max):    21.8 ms …  34.6 ms    121 runs
 
Benchmark 2: python-fs-read/test.py
  Time (mean ± σ):      15.6 ms ±   0.8 ms    [User: 5.5 ms, System: 10.0 ms]
  Range (min … max):    14.4 ms …  20.8 ms    166 runs
 
Summary
  python-fs-read/test.py ran
    1.52 ± 0.15 times faster than rust-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

use std::io::Read;
use std::fs::OpenOptions;

fn main() {
    let mut bs = vec![0; 64 * 1024 * 1024];
    let mut f = OpenOptions::new().read(true).open("/tmp/file").unwrap();
    let mut ts = 0;
    loop {
        let buf = &mut bs[ts..];
        let n = f.read(buf).unwrap();
        let n = n as usize;
        if n == 0 {
            break
        }
        ts += n;
    }

    assert_eq!(ts, 64 * 1024 * 1024);
}

但是:

Benchmark 1: rust-std-fs-read/target/release/test
  Time (mean ± σ):      23.1 ms ±   2.5 ms    [User: 0.3 ms, System: 22.8 ms]
  Range (min … max):    21.0 ms …  37.6 ms    124 runs
 
Benchmark 2: python-fs-read/test.py
  Time (mean ± σ):      15.2 ms ±   1.1 ms    [User: 5.4 ms, System: 9.7 ms]
  Range (min … max):    14.3 ms …  21.4 ms    178 runs

Summary
  python-fs-read/test.py ran
    1.52 ± 0.20 times faster than rust-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, "\tP\201A\225\366>\260\270R\365\313\220{E\372\274\6\35\"\353\204\220s\2|7C\205\265\6\263"..., 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)        = -1 ENOTTY (Inappropriate ioctl for device)
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, "\tP\201A\225\366>\260\270R\365\313\220{E\372\274\6\35\"\353\204\220s\2|7C\205\265\6\263"..., 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 更快呢?

标签:std,fs,Python,+-,python,read,ms,test
From: https://www.cnblogs.com/databend/p/17867358.html

相关文章

  • python图像中如何 绘制矩形,编辑文案,保存结果图片等操作
    python版opencv函数学习笔记-cv.rectangle()全参数理解cv2.rectangle(img,pt1,pt2,color,thickness=None,lineType=None,shift=None)以下来自官方文档和自己的理解img:指定一张图片,在这张图片的基础上进行绘制;pt1:矩形的一个顶点;pt2:与pt1在对角线上相对的矩形的顶点;......
  • 【5.0】Python面向对象之组合
    【一】什么是组合在一个类中以另外一个类的对象作为数据属性,称为类的组合。【二】组合的使用组合与继承都是用来解决代码的重用性问题。不同的是:继承是一种“是”的关系,比如老师是人、学生是人,当类之间有很多相同的之处,应该使用继承;而组合则是一种“有”的关系,比如老......
  • 【8.0】Python面向对象之反射
    【一】反射【1】什么是反射反射是一种程序可以访问、检测和修改其本身状态或行为的能力。在Python中,反射主要指通过字符串的形式操作对象的属性。【2】Python中的反射通过字符串的形式操作对象相关的属性。python中的一切事物都是对象(都可以使用反射)【二】反射方法......
  • 【7.0】Python面向对象之绑定方法与非绑定方法
    【一】绑定方法与非绑定方法介绍【1】绑定方法绑定给谁,谁来调用就自动将它本身当作第一个参数传入(1)绑定到类的方法用classmethod装饰器装饰的方法。为类量身定制类.boud_method(),自动将类当作第一个参数传入(其实对象也可调用,但仍将类当作第一个参数传入)(2)绑定......
  • 【补】Python中关于OOP的常用术语
    【一】抽象与实现【1】抽象抽象是一种概念或思维工具,用于简化复杂的问题并将其分解为易于管理的部分。抽象可以帮助我们理解事物的本质和行为,同时也可以帮助我们在设计软件时更好地组织代码和数据结构。【2】实现实现则是对抽象的一种具体表达。它是对抽象的概念或模型进......
  • [python] 基于Tablib库处理表格数据
    Tablib是一个用于处理电子表格(如Excel,CSV,JSON)的Python库。它提供了一种简单而强大的方式来操作和处理数据。利用Tablib,我们可以轻松地读取、写入、过滤和转换各种类型的电子表格数据。Tablib具有一致且易于使用的API,以在不同的数据格式之间进行无缝转换。比如,Tablib可以将数据......
  • Python爬取某电商平台商品数据及评论!
    前言随着互联网的发展,电商平台的出现让我们的消费更加便利,消费者可以在家里轻松地购买到各种商品。但有时候我们需要大量的商品数据进行分析,或者需要了解其他消费者的评价,这时候我们可以通过爬虫来获取数据。本文将介绍如何使用Python爬取某电商平台的商品数据及评论,并且用到代理ip......
  • Python学习之十二_tkinter的学习与使用
    Python学习之十二_tkinter的学习与使用摘要本来想说会用QT5进行界面编程但是发现比较繁琐还是先学习使用tkinter的方式进行界面化的编写和学习了基础知识tkinter是一个源码开放的图形用户接口开发工具,具备跨平台的特性Python默认的GUI开发模块是tkinter(在Python3以前的版本中......
  • Python中导入包和模块
    一、模块含义在前面的几个章节中我们基本上是用python解释器来编程,如果你从Python解释器退出再进入,那么你定义的所有的方法和变量就都消失了。为此Python提供了一个办法,把这些定义存放在文件中,为一些脚本或者交互式的解释器实例使用,这个文件被称为模块。模块是一个包含所......
  • [python] 基于Tablib库处理表格数据
    Tablib是一个用于处理电子表格(如Excel,CSV,JSON)的Python库。它提供了一种简单而强大的方式来操作和处理数据。利用Tablib,我们可以轻松地读取、写入、过滤和转换各种类型的电子表格数据。Tablib具有一致且易于使用的API,以在不同的数据格式之间进行无缝转换。比如,Tablib可以将数据......