`shutil.copytree` 속도를 높이세요!

WBOY
풀어 주다: 2024-08-28 18:32:06
원래의
396명이 탐색했습니다.

Speed up `shutil.copytree` !

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

로그인 후 복사

두 가지 비교:

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을 수정한 버전입니다.

copy1 비용은 173.04780609999943s
copy2 비용은 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()
로그인 후 복사

출력:

copy1의 비용은 187.21초
copy2 비용은 244.33초
copy3 비용은 111.27초입니다


싱글 스레드 버전보다 비동기 버전이 더 빠른 것을 확인할 수 있습니다. 그러나 단일 스레드 버전은 다중 스레드 버전보다 빠릅니다. (제 테스트 환경이 좋지 않을 수도 있으니, 결과를 답장으로 보내주시면 됩니다.)

Barry Scott에게 감사드립니다!

장점 & 단점

비동기는 좋은 선택입니다. 그러나 완벽한 솔루션은 없습니다. 문제를 발견하시면 저에게 답장을 보내주세요.

python.org에 토론을 쓰는 것은 이번이 처음입니다. 문제가 있으면 알려주시기 바랍니다. 감사합니다.

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

위 내용은 `shutil.copytree` 속도를 높이세요!의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

원천:dev.to
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿
회사 소개 부인 성명 Sitemap
PHP 중국어 웹사이트:공공복지 온라인 PHP 교육,PHP 학습자의 빠른 성장을 도와주세요!