【问题标题】:openpyxl performance in read-only mode只读模式下的openpyxl性能
【发布时间】:2022-06-30 20:00:53
【问题描述】:

我对openpyxl在读取文件时的性能有疑问。

我正在尝试使用 ProcessPoolExecutor 读取相同的 xlsx 文件,单个文件可能有 500,000 到 800,000 行。

在只读模式下调用sheet.iter_rows(),不使用ProcessPoolExecutor时,读取整个工作表,处理10000行数据大约需要1s。但是当我用 ProcessPoolExecutor 设置 max_row 和 min_row 参数时,就不一样了。

totalRows: 200,000
1 ~ 10000 take 1.03s
10001 ~ 20000 take 1.73s
20001 ~ 30000 take 2.41s
30001 ~ 40000 take 3.27s
40001 ~ 50000 take 4.06s
50001 ~ 60000 take 4.85s
60001 ~ 70000 take 5.93s
70001 ~ 80000 take 6.64s
80001 ~ 90000 take 7.72s
90001 ~ 100000 take 8.18s
100001 ~ 110000 take 9.42s
110001 ~ 120000 take 10.04s
120001 ~ 130000 take 10.61s
130001 ~ 140000 take 11.17s
140001 ~ 150000 take 11.52s
150001 ~ 160000 take 12.48s
160001 ~ 170000 take 12.52s
170001 ~ 180000 take 13.01s
180001 ~ 190000 take 13.25s
190001 ~ 200000 take 13.46s
total: take 33.54s

显然,单看每个过程的结果,消耗的时间确实少了。 但是整体时间消耗增加了。 范围越靠后,每个进程消耗的时间就越多。 单个进程读取 200,000 行仅需 20 秒左右。

我对迭代器不是很清楚,也没有仔细研究过openpyxl的源代码。 从耗时来看,即使设置了范围,迭代器仍然需要从第1行开始处理,不知道是不是这样。

我不是专业程序员,如果你恰好有相关经验,请尽量简单

代码在这里!!!

import openpyxl
from time import perf_counter
from concurrent.futures import ProcessPoolExecutor

def read(file, minRow, maxRow):
    start = perf_counter()
    book = openpyxl.load_workbook(filename=file, read_only=True, keep_vba=False, data_only=True, keep_links=False)
    sheet = book.worksheets[0]
    val = [[cell.value for cell in row] for row in sheet.iter_rows(min_row=minRow, max_row=maxRow)]
    book.close()
    end = perf_counter()
    print(f'{minRow} ~ {maxRow}', 'take {0:.2f}s'.format(end-start))
    return val


def parallel(file: str, rowRanges: list[tuple]):
    futures = []
    with ProcessPoolExecutor(max_workers=6) as pool:
        for minRow, maxRow in rowRanges:
            futures.append(pool.submit(read, file, minRow, maxRow))
    return futures

if __name__ == '__main__':
    file = '200000.xlsx'
    start = perf_counter()
    tasks = getRowRanges(file)
    parallel(file, tasks)
    end = perf_counter()
    print('total: take {0:.2f}s'.format(end-start))

【问题讨论】:

标签: python parallel-processing iterator openpyxl


【解决方案1】:

问:
"...关于性能的问题..."
...请尽量简单.. .

答:
拥有 6 辆法拉利跑车 (~max_workers = 6)
不提供移动 6 位车手的保证 (~The Workload)
从头到尾
在 1 / 6 的时间内。

那行不通,
即使我们有一条 6 车道宽的赛道(我们没有),正如您已经报告的那样,也存在瓶颈(只有 1 车道宽的桥,在从比赛开始到结束的方式)。

实际上,
还有更多破坏性能的瓶颈(The Bridge作为主要的性能阻塞器,还有一些更小、更少阻塞,但性能进一步下降 em>桥梁),有些可以避免,有些则不能:

  • 文件 I/O 在纯单人串行运行中的速度不超过 ~ 10k [rows/s]
    所以永远不要期望相同的速度会出现在相同的“交叉” (单,单通道)桥(共享文件 I/O 硬件接口),用于任何下一个并发运行的法拉利,竞争使用相同的资源,已用于第一个从文件读取的进程(real-hardware latencies 问题,a很多......魔鬼在细节)

  • 另一个可避免的降级伴随着昂贵的附加成本,每个list.append() 都需要支付。在这里,尝试选择一个不同的对象,完全避免基于list 的存储,并预先分配一个块存储(一次性支付的 RAM 分配成本),其优势在于知道结果存储的大小,并且保持动态存储数据,最好在缓存行尊重块中而不是增量(可能技术性太强,但如果要最大化性能,这些细节很重要)

  • 双迭代器 SLOC 非常适合作为工作簿示例,但如果性能是或重点,请尝试寻找另一种方法,甚至可能使用更简单的 XLS 阅读器(没有像 VBA 解释器等人那样多的机器) ,它可以将按行消耗的单元格导出为纯文本,这可以比 triplet-of-nested-iterators 中的原样代码更快地收集“语法糖化” SLOC
    [ [ ... for cell in row ] for row in sheet.iterator(...) ]

  • 最后也是过程实例化成本,它进入了修订后的阿姆达尔定律,重新制定,以便它也考虑到工作(块)的开销和原子性。有关(技术上独立的)详细信息,请参阅 thisthese - 其中交互式加速模拟器计算器通常用于测试任何此类并行化努力永远无法克服的主要上限。

  • 最后,但绝不是最不重要的 - 内存: 将您的 .xlsx 文件大小乘以 ~ 50 倍,然后乘以 6 个工作人员 ~ 预计将使用该数量的物理内存(请参阅doc“与其他库和应用程序相比,内存使用率相当高,大约是原始文件大小的 50 倍,例如 50 MB Excel 文件需要 2.5 GB” credit到@Charlie Clark )如果你的系统没有那么多物理内存,操作系统就会开始窒息,因为它真的要分配它并进入内存交换“抖动”模式(移动块-RAM 到磁盘交换区域和来回,因为在虚拟内存管理的地址空间中交错前进的 6 个工作人员在一个非常高(超过 5(!)个数量级以上的小物理 RAM 内模拟) ) 磁盘 I/O 延迟,试图跨越已经阻塞的性能瓶颈,是的 - The Bridge ...交通堵塞已经达到最大值,如6 名工作人员尝试做同样的事情——在更阻塞的瓶颈上移动更多数据)所有这些都在非常大的延迟下猛增(参见上面延迟的 URL)。 hint 可能,但不需要拯救我们,加上 thisthis 可能会减少,更好地直接防止进一步的低效率

【讨论】:

    【解决方案2】:

    我相信和OP有同样的问题。

    令人费解的是,一旦在sheet.iter_rows() 上设置了min_rowmax_row,并发执行就不再适用,就好像某种全局锁在起作用一样。

    以下代码试图从 Excel 文件中转储一张大工作表中的数据。这个想法是利用sheet.iter_rows 上的min_rowmax_row 来锁定阅读窗口,并利用ThreadPoolExecutor 进行并发执行。

    # artificially create a set of row index ranges,
    # 10,000 row per set till 1,000,000th row
    # something like [(1, 10_000), (10_001, 20_000), .....]
    def _ranges():
        _i = 1
        _n = 10_000
        while _i <= 1_000_000:
            yield _i, _i + _n - 1
            _i += _n
    
    
    def write_to_file(file, mn, mx):
        print(f'write to file {mn}-{mx}')
        wb = load_workbook(file, read_only=True
                           , data_only=True, keep_links=False, keep_vba=False)
        sheet = wb[wb.sheetnames[0]]
    
        out_file = _dst / f"{mn}-{mx}.txt"
        row_count = 1
        with out_file.open('w', encoding='utf8') as f:
    
            rows = sheet.iter_rows(values_only=True, min_row=mn, max_row=mx)
    
            for row in rows:
                print(f'section {mn}-{mx} write {row_count}')
                f.write(' '.join([str(c).replace('\n', ' ') for c in row]) + '\n')
                row_count += 1
    
    
    def main():
        fut = []
        with futures.ThreadPoolExecutor() as ex:
            for mn, mx in _ranges():
                fut.append(ex.submit(write_to_file, _file, mn, mx))
    
        futures.wait(fut)
    

    所有write_to_file() 都立即启动。

    然而,对行的迭代似乎以严格的顺序方式运行。

    稍作改动:

    def write_to_file(file, mn, mx):
        print(f'write to file {mn}-{mx}')
        wb = load_workbook(file, read_only=True
                           , data_only=True, keep_links=False, keep_vba=False)
        sheet = wb[wb.sheetnames[0]]
    
        out_file = _dst / f"{mn}-{mx}.txt"
        row_count = 1
        with out_file.open('w', encoding='utf8') as f:
    
            rows = sheet.iter_rows(values_only=True)
                                  # ^^^^^^^^^^^^^^^^^___min_row/max_row not set
    
            for row in rows:
                print(f'section {mn}-{mx} write {row_count}')
                f.write(' '.join([str(c).replace('\n', ' ') for c in row]) + '\n')
                row_count += 1
    

    第 20001-30000 节先写!

    并发执行的混乱效应发生。

    但是,如果没有min_rowmax_row,并发执行根本没有意义。

    【讨论】:

      猜你喜欢
      • 2019-01-29
      • 2020-09-26
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-04-21
      • 2014-06-13
      相关资源
      最近更新 更多