bytes_복사하지_않고_다루기

매일 공부(ML)·2022년 9월 6일
0

이어드림

목록 보기
131/146

bytes를 복사하지 않고 다루려면 memoryview와 bytearray를 사용하라

파이썬이 CPU 위주의 계산 작업을 추가적인 노력 없이 병렬화는 불가하지만 스루풋이 높은 병렬 I/O를 다양한 방식으로 지원할 수 있다.

그러나, 그럼에도 불구하고 I/O 도구를 잘못 사용하여 과부화가 걸려서 속도가 느려질 수 있다.

코드로 보는 예시

  • 영화나 드라마를 스트리밍하는 미디어 만들기

    • 핵심 기능: 건너뛰거나 반복기능 가능

    • 데이터 덩어리를 서버에 요청해서 기능 구현

def timecode_to_index(video_id, timecode):
    ...
    # 비디오 데이터의 바이트 오프셋을 반환한다.

def request_chunk(video_id, byte_offset, size):
    ...
    # video_id에 대한 비디오 데이터 중에서 바이트 오프셋부터 size만큼 반환한다.

video_id =...
timecode = '01:09:14:28'
byte_offset = timecode_to_index(video_id, timecode)
size = 20 * 1024 * 1024
video_data = request_chunk(video_id, byte_offset, size)

#request_chunk 요청 받기

socket = ... #클라이언트가 연결한 소켓
video_data = ... #video_id 에 해당하는 데이터가 들어있는 bytes
byte_offset = ... #요청받은 시작 위치
size = 20 * 1024 * 1024

chunk = video_data[byte_offset:byte_offset + size]
socket.send(chunk)

코드의 지연 시간과 스루풋은 데이터를 가져오는데 걸리는 시간과 데이터를 클라이언트에 송신하는데 걸리는 시간이라는 두 가지 요인에 의해 결정된다..

최대 성능을 알아보려면 소켓 송신 부분을 무시하여 데이터 덩어리를 만들기 위해서 bytes 인스턴스 슬라이싱하는 방법에 걸리는 시간 측정


import timeit

def run_test():
    chunk =. video_data[byte_offset:byte_offset + size]
    #socket.send(chunk)를 호출해야 하지만 벤치마크를 위해 무시

result = timeit.timeit(
    stmt = 'run_test()',
    globals = globals(),
    number=100) / 100

print(f'{result:0.9f} 초') #0.004925669 초

위의 코드는 최대 개수가 작고 동시 접속에 비하면 적다.

그리고, 문제는 기반 데이터를 bytes 인스턴스로 슬라이싱하려면 메모리를 복사해야하는데, 이 과정을 CPU 시간을 점유

그러므로, 파이썬이 제공하는 memoryview 내장 타입을 사용하라

memoryview는 CPython의 고성능 버퍼 프로토콜을 프로그램에 노출 시켜서 bytes와 같은 객체를 통하지 않고 하부 데이터를 버퍼에 접근시키는 저수준 C API

슬라이싱을 하면 데이터 복사가 아니라 새로운 인스턴스 형성


data = '동해물과 abc 백두산이 마르고 닳도록'.encode("utf8")
view = memoryview(data)
chunk = view[12:19]
print(chunk)
print("크기:", chunk.nbytes)
print('뷰의 데이터:', chunk.tobytes())
print("내부의 데이터:", chunk.obj)

#bytes 슬라이스를 memoryview로 바꿔서 마이크로 벤치마크 수행

video_view = memoryview(video_data)

def run_test():
    chunk = video_view[byte_offset:byte_offset + size]
    #socket.send(chunk)를 호출해야 하지만 벤치마크를 위해 무시한다.

result = timeit.timeit(
    stmt = 'run_test()',
    globals=globals(),
    numbers=100) / 100

print(f'{result:0.9f} 초') # 0.0000000250 초

사용자가 가장 최근에 보낸 비디오 데이터를 캐시에 넣고 다른 클라이언트가 캐시에 있는 비디오 데이터를 읽게 하기


socket = ... #클라이언트가 연결한 소켓
video_cache = ... # 서버로 들어오는 비디오 스트림의 캐시
byte_offset = ... #데이터 버퍼 위치
size = 1024 * 1024 #데이타 덩어리 크기
chunk = socket.recv(size)
video_view = memoryview(video_cache)
before = video_view[:byte_offset]
after = video_view[byte_offset + size:]
new_cache = b''.join([before, chunk, after])

## 확장성이 없는 코드들
#bytearray를 활용하여 확장성 늘리기

my_array = bytearray('hello 안녕'.encode("utf-8")) #b''가 아니라 ''문자열
my_array[0] = 0 x79
print(my_array)

#betearray도 memoryview를 사용하여 감싼다.
#Memoryview를 슬라이싱해서 객체를 만들고 대입

#복사 비용 감축
my_array = bytearray('row, row, row your 보트'.encode("utf-8"))
my_view = memoryview(my_array)
write_view = my_view[3:13]
write_view[:] = b'-10 bytes-'
print(my_array)
#스플라이스를 하지 않고 하부의 bytearray에 데이터 수신

video_array = bytearray(video_cache)
write_view = memoryview(video_array)
chunk = write_view[byte_offset:byte_offset+size]
socket.recv_info(chunk)
#socket 의 성능 비교
def run_test():
    chunk = write_view[byte_offset:byte_offset+size]
    socket.recv_info(chunk)

result = timeit.timeit(
    stmt = 'run_test()',
    globals = globals(),
    number = 100) / 100

print(f'{result:0.9f} 초')

Summary

  • memoryview 내장 타입은 객체의 슬라이스에 대해 파이썬 고성능 버퍼 프로토콜로 읽고 쓰기를 지원하는, 복사기가 없는 인터페이스 제공

  • bytearray 내장 타입은 복사가 없는 읽기 함수(socket.recv_from과 같은)에 사용할 수 있는 bytes와 비슷한 변경 가능한 타입 제공

  • memoryview로 bytearray를 감싸면 복사에 따른 비용을 추가 부담하지 않고도 수신 받은 데이터를 버퍼에 원하는 위치에 스플라이스

profile
성장을 도울 아카이빙 블로그

0개의 댓글