파이썬이 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} 초')
memoryview 내장 타입은 객체의 슬라이스에 대해 파이썬 고성능 버퍼 프로토콜로 읽고 쓰기를 지원하는, 복사기가 없는 인터페이스 제공
bytearray 내장 타입은 복사가 없는 읽기 함수(socket.recv_from과 같은)에 사용할 수 있는 bytes와 비슷한 변경 가능한 타입 제공
memoryview로 bytearray를 감싸면 복사에 따른 비용을 추가 부담하지 않고도 수신 받은 데이터를 버퍼에 원하는 위치에 스플라이스