「労働者が自分の仕事をうまくやりたいなら、まず自分の道具を研ぎ澄まさなければなりません。」 - 孔子、「論語。陸霊公」
表紙 > プログラミング > `shutil.copytree` を高速化します!

`shutil.copytree` を高速化します!

2024 年 11 月 4 日に公開
ブラウズ:195

Speed up `shutil.copytree` !

shutil.copytree の高速化について議論する

ここに書いてください

これは に関するディスカッションです。https://discuss.python.org/t/speed-up-shutil-copytree/62078 を参照してください。何かアイデアがあれば、ぜひ送ってください!

背景

shutil は Python の非常に便利なモジュールです。これは github で見つけることができます: https://github.com/python/cpython/blob/master/Lib/shutil.py

shutil.copytreeは、フォルダーを別のフォルダーにコピーする関数です。

この関数では、_copytree関数を呼び出してコピーしています。

_copytree は何をしますか?

  1. 指定されたファイル/ディレクトリを無視します。
  2. 宛先ディレクトリを作成しています。
  3. シンボリック リンクの処理中にファイルまたはディレクトリをコピーします。
  4. 発生したエラーを収集し、最終的に発生させます (権限の問題など)。
  5. ソース ディレクトリのメタデータを宛先ディレクトリにレプリケートします。

問題点

_copytree の速度は、ファイル数が多い場合やファイル サイズが大きい場合にはあまり速くありません。

ここでテストします:

import os
import shutil

os.mkdir('test')
os.mkdir('test/source')

def bench_mark(func, *args):
    import time
    start = time.time()
    func(*args)
    end = time.time()
    print(f'{func.__name__} takes {end - start} seconds')
    return end - start

# write in 3000 files
def write_in_5000_files():
    for i in range(5000):
        with open(f'test/source/{i}.txt', 'w') as f:
            f.write('Hello World'   os.urandom(24).hex())
            f.close()

bench_mark(write_in_5000_files)

def copy():
    shutil.copytree('test/source', 'test/destination')

bench_mark(copy)

結果は次のようになります:

write_in_5000_files には 4.084963083267212 秒かかります
コピーには 27.12768316268921 秒かかります

私がやったこと

マルチスレッド化

コピー処理を高速化するためにマルチスレッドを使用しています。そして、関数 _copytree_single_threaded の名前を変更し、新しい関数 _copytree_multithreaded を追加します。これがcopytree_multithreadedです:

def _copytree_multithreaded(src, dst, symlinks=False, ignore=None, copy_function=shutil.copy2,
                            ignore_dangling_symlinks=False, dirs_exist_ok=False, max_workers=4):
    """Recursively copy a directory tree using multiple threads."""
    sys.audit("shutil.copytree", src, dst)

    # get the entries to copy
    entries = list(os.scandir(src))

    # make the pool
    with ThreadPoolExecutor(max_workers=max_workers) as executor:
        # submit the tasks
        futures = [
            executor.submit(_copytree_single_threaded, entries=[entry], src=src, dst=dst,
                            symlinks=symlinks, ignore=ignore, copy_function=copy_function,
                            ignore_dangling_symlinks=ignore_dangling_symlinks,
                            dirs_exist_ok=dirs_exist_ok)
            for entry in entries
        ]

        # wait for the tasks
        for future in as_completed(futures):
            try:
                future.result()
            except Exception as e:
                print(f"Failed to copy: {e}")
                raise

マルチスレッドを使用するかどうかの判断を追加します。

if len(entries) >= 100 or sum(os.path.getsize(entry.path) for entry in entries) >= 100*1024*1024:
        # multithreaded version
        return _copytree_multithreaded(src, dst, symlinks=symlinks, ignore=ignore,
                                        copy_function=copy_function,
                                        ignore_dangling_symlinks=ignore_dangling_symlinks,
                                        dirs_exist_ok=dirs_exist_ok)

else:
    # single threaded version
    return _copytree_single_threaded(entries=entries, src=src, dst=dst,
                                        symlinks=symlinks, ignore=ignore,
                                        copy_function=copy_function,
                                        ignore_dangling_symlinks=ignore_dangling_symlinks,
                                        dirs_exist_ok=dirs_exist_ok)

テスト

ソース フォルダーに 50000 個のファイルを書き込みます。ベンチマーク:

def bench_mark(func, *args):
    import time
    start = time.perf_counter()
    func(*args)
    end = time.perf_counter()
    print(f"{func.__name__} costs {end - start}s")

書き込み:

import os
os.mkdir("Test")
os.mkdir("Test/source")

# write in 50000 files
def write_in_file():
    for i in range(50000):
         with open(f"Test/source/{i}.txt", 'w') as f:
             f.write(f"{i}")
             f.close()

2 つの比較:

def copy1():
    import shutil
    shutil.copytree('test/source', 'test/destination1')

def copy2():
    import my_shutil
    my_shutil.copytree('test/source', 'test/destination2')

  • 「my_shutil」は、shutil の私が修正したバージョンです。

コピー 1 のコストは 173.04780609999943 秒
コピー 2 のコストは 155.81321870000102s

copy2 は copy1 よりもはるかに高速です。何度でも実行できます。

メリットとデメリット

マルチスレッドを使用すると、コピー処理を高速化できます。ただし、メモリ使用量が増加します。ただし、コード内のマルチスレッドを書き直す必要はありません。

非同期

「バリー・スコット」に感謝します。私は彼/彼女の提案に従います:

非同期 I/O を使用すると、オーバーヘッドを減らして同じ改善が得られる可能性があります。

次のコードを書きます:

import os
import shutil
import asyncio
from concurrent.futures import ThreadPoolExecutor
import time


# create directory
def create_target_directory(dst):
    os.makedirs(dst, exist_ok=True)

# copy 1 file
async def copy_file_async(src, dst):
    loop = asyncio.get_event_loop()
    await loop.run_in_executor(None, shutil.copy2, src, dst)

# copy directory
async def copy_directory_async(src, dst, symlinks=False, ignore=None, dirs_exist_ok=False):
    entries = os.scandir(src)
    create_target_directory(dst)

    tasks = []
    for entry in entries:
        src_path = entry.path
        dst_path = os.path.join(dst, entry.name)

        if entry.is_dir(follow_symlinks=not symlinks):
            tasks.append(copy_directory_async(src_path, dst_path, symlinks, ignore, dirs_exist_ok))
        else:
            tasks.append(copy_file_async(src_path, dst_path))

    await asyncio.gather(*tasks)
# choose copy method
def choose_copy_method(entries, src, dst, **kwargs):
    if len(entries) >= 100 or sum(os.path.getsize(entry.path) for entry in entries) >= 100 * 1024 * 1024:
        # async version
        asyncio.run(copy_directory_async(src, dst, **kwargs))
    else:
        # single thread version
        shutil.copytree(src, dst, **kwargs)
# test function
def bench_mark(func, *args):
    start = time.perf_counter()
    func(*args)
    end = time.perf_counter()
    print(f"{func.__name__} costs {end - start:.2f}s")

# write in 50000 files
def write_in_50000_files():
    for i in range(50000):
        with open(f"Test/source/{i}.txt", 'w') as f:
            f.write(f"{i}")

def main():
    os.makedirs('Test/source', exist_ok=True)
    write_in_50000_files()

    # 单线程复制
    def copy1():
        shutil.copytree('Test/source', 'Test/destination1')

    def copy2():
        shutil.copytree('Test/source', 'Test/destination2')

    # async
    def copy3():
        entries = list(os.scandir('Test/source'))
        choose_copy_method(entries, 'Test/source', 'Test/destination3')

    bench_mark(copy1)
    bench_mark(copy2)
    bench_mark(copy3)

    shutil.rmtree('Test')

if __name__ == "__main__":
    main()

出力:

コピー 1 のコストは 187.21 秒
コピー 2 のコストは 244.33 秒
コピー 3 のコストは 111.27 秒


非同期バージョンの方がシングルスレッドバージョンよりも高速であることがわかります。ただし、シングル スレッド バージョンはマルチスレッド バージョンよりも高速です。 (私のテスト環境があまり良くないかもしれません。試して結果を私に返信して送ってください)

バリー・スコット、ありがとう!

メリットとデメリット

非同期は良い選択です。しかし、完璧な解決策はありません。何か問題が見つかった場合は、私に返信してください。

終わり

python.org でディスカッションを書くのはこれが初めてです。何か問題がございましたら、お知らせください。ありがとう。

私のGithub: https://github.com/mengqinyuan
私の開発者: https://dev.to/mengqinyuan

リリースステートメント この記事は次の場所に転載されています: https://dev.to/mengqinyuan/add-multithreading-to-shutil--2lm1?1 侵害がある場合は、[email protected] に連絡して削除してください。
最新のチュートリアル もっと>

免責事項: 提供されるすべてのリソースの一部はインターネットからのものです。お客様の著作権またはその他の権利および利益の侵害がある場合は、詳細な理由を説明し、著作権または権利および利益の証拠を提出して、電子メール [email protected] に送信してください。 できるだけ早く対応させていただきます。

Copyright© 2022 湘ICP备2022001581号-3