„Wenn ein Arbeiter seine Arbeit gut machen will, muss er zuerst seine Werkzeuge schärfen.“ – Konfuzius, „Die Gespräche des Konfuzius. Lu Linggong“
Titelseite > Programmierung > Beschleunigen Sie „shutil.copytree“!

Beschleunigen Sie „shutil.copytree“!

Veröffentlicht am 04.11.2024
Durchsuche:866

Speed up `shutil.copytree` !

Besprechen Sie die Beschleunigung von „shutil.copytree“.

Schreiben Sie hier

Dies ist eine Diskussion über , siehe: https://discuss.python.org/t/speed-up-shutil-copytree/62078. Wenn Sie Ideen haben, senden Sie mir diese bitte!

Hintergrund

shutil ist ein sehr nützliches Modul in Python. Sie finden es in Github: https://github.com/python/cpython/blob/master/Lib/shutil.py

shutil.copytree ist eine Funktion, die einen Ordner in einen anderen Ordner kopiert.

In dieser Funktion wird die Funktion _copytree zum Kopieren aufgerufen.

Was macht _copytree?

  1. Angegebene Dateien/Verzeichnisse werden ignoriert.
  2. Zielverzeichnisse werden erstellt.
  3. Kopieren von Dateien oder Verzeichnissen beim Umgang mit symbolischen Links.
  4. Erfassung und eventuelle Meldung aufgetretener Fehler (z. B. Berechtigungsprobleme).
  5. Metadaten des Quellverzeichnisses in das Zielverzeichnis replizieren.

Probleme

_Die Copytree-Geschwindigkeit ist nicht sehr hoch, wenn die Anzahl der Dateien groß ist oder die Dateigröße groß ist.

Hier testen:

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)

Das Ergebnis ist:

write_in_5000_files dauert 4,084963083267212 Sekunden
Das Kopieren dauert 27,12768316268921 Sekunden

Was ich getan habe

Multithreading

Ich verwende Multithread, um den Kopiervorgang zu beschleunigen. Und ich benenne die Funktion in _copytree_single_threaded um und füge eine neue Funktion hinzu: _copytree_multithreaded. Hier ist der 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

Ich füge ein Urteil hinzu, um zu entscheiden, ob Multithread verwendet werden soll oder nicht.

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)

Prüfen

Ich schreibe 50000 Dateien in den Quellordner. Benchmark:

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")

Schreiben:

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()

Zwei im Vergleich:

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

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

  • "my_shutil" ist meine modifizierte Version von Shutil.

Kopie1 kostet 173,04780609999943s
copy2 kostet 155,81321870000102s

copy2 ist um einiges schneller als copy1. Sie können mehrmals ausführen.

Vor- und Nachteile

Die Verwendung von Multithread kann den Kopiervorgang beschleunigen. Es wird jedoch die Speichernutzung erhöhen. Aber wir müssen den Multithread im Code nicht neu schreiben.

Asynchron

Danke an „Barry Scott“. Ich werde seinem/ihrem Vorschlag folgen:

Möglicherweise erzielen Sie die gleiche Verbesserung bei geringerem Overhead, wenn Sie asynchrone E/A verwenden.

Ich schreibe diesen Code:

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()

Ausgabe:

Kopie1 kostet 187,21 Sek.
copy2 kostet 244,33s
copy3 kostet 111,27s


Sie können sehen, dass die asynchrone Version schneller ist als die Single-Thread-Version. Aber die Single-Thread-Version ist schneller als die Multi-Thread-Version. (Vielleicht ist meine Testumgebung nicht sehr gut, Sie können versuchen, mir Ihr Ergebnis als Antwort zu senden)

Vielen Dank, Barry Scott!

Vor- und Nachteile

Async ist eine gute Wahl. Aber keine Lösung ist perfekt. Wenn Sie ein Problem finden, können Sie mir eine Antwort senden.

Ende

Dies ist das erste Mal, dass ich eine Diskussion auf python.org schreibe. Wenn es ein Problem gibt, lassen Sie es mich bitte wissen. Danke schön.

Mein Github: https://github.com/mengqinyuan
Mein Dev.to: https://dev.to/mengqinyuan

Freigabeerklärung Dieser Artikel ist abgedruckt unter: https://dev.to/mengqinyuan/add-multithreading-to-shutil--2lm1?1 Bei Verstößen wenden Sie sich bitte an [email protected], um ihn zu löschen
Neuestes Tutorial Mehr>

Haftungsausschluss: Alle bereitgestellten Ressourcen stammen teilweise aus dem Internet. Wenn eine Verletzung Ihres Urheberrechts oder anderer Rechte und Interessen vorliegt, erläutern Sie bitte die detaillierten Gründe und legen Sie einen Nachweis des Urheberrechts oder Ihrer Rechte und Interessen vor und senden Sie ihn dann an die E-Mail-Adresse: [email protected] Wir werden die Angelegenheit so schnell wie möglich für Sie erledigen.

Copyright© 2022 湘ICP备2022001581号-3