Skip to content

Commit 508de61

Browse files
committed
better way 74 정리
1 parent 5a70f48 commit 508de61

File tree

2 files changed

+208
-0
lines changed

2 files changed

+208
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,3 +105,4 @@
105105
[71. 생산자-소비자 큐로 deque 를 사용하라](./summary/BetterWay71.md)
106106
[72. 정렬된 시퀀스를 검색할 때는 bisect 를 사용하라](./summary/BetterWay72.md)
107107
[73. 우선순위 큐로 heapq 를 사용하는 방법을 알아두라](./summary/BetterWay73.md)
108+
[74. bytes 를 복사하지 않고 다루려면 memoryview와 bytearray를 사용하라](./summary/BetterWay74.md)

summary/BetterWay74.md

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
# 74. bytes 를 복사하지 않고 다루려면 memoryview와 bytearray를 사용하라
2+
3+
## 1. 파이선에서의 적절한 I/O 지원 선택 중요성
4+
5+
- 파이선이 CPU 위주의 계산 작업을 추가적인 노력없이 병렬화해줄 수는 없으나 스루풋이 높은 병렬 I/O 를 다양한 방식으로 지원 가능
6+
- I/O 지원을 잘못 사용하여 파이선이 I/O 위주의 부하에 대해서도 느리다는 결론으로 이어짐
7+
8+
- 네트워크를 통해 스트리밍하는 미디어 서버 예
9+
- 플레이 중인 비디오 앞, 뒤로 이동 기능
10+
- 반복 기능
11+
12+
```python
13+
def timecode_to_index(video_id, timecode):
14+
...
15+
# 비디오 데이터의 바이트 오프셋을 반환한다
16+
17+
def request_chunk(video_id, byte_offset, size):
18+
...
19+
# video_id에 대한 비디오 데이터 중에서 바이트 오프셋부터# size만큼을 반환한다
20+
21+
video_id = ...
22+
timecode = '01:09:14:28
23+
byte_offset = timecode_to_index(video_id, timecode)
24+
size = 20 * 1024 * 1024
25+
video_data = request_chunk(video_id, byte_offset, size)
26+
```
27+
28+
- `request_chunk` 요청을 받아서 요청에 해당하는 데이터를 도렬주는 서버 측 핸들러 구현
29+
- 요청 받은 데이터 덩어리를 메모리 캐시에 들어 있는 수 기가바이트 크기의 비디오 정보에서 꺼낸 후 소켓을 통해 클라이언트에게 돌려주는 과정에 집중
30+
- 해당 코드의 지연 시간과 스루풋
31+
1. `video_data` 에서 20MB 의 비디오 덩어리를 가져오는 데 걸리는 시간
32+
2. 데이터를 클라이언트에게 송신하는 데 걸리는 시간
33+
- 2번이 무한히 빠르다고 가정하면 1번에만 집중
34+
- 데이터 덩어리를 만들기 위해 bytes 인스턴스를 슬라이싱하는 방법 시간 측정으로 확인 가능
35+
36+
```python
37+
socket = ... # 클라이언트가 연결한 소켓
38+
video_data = ... # video_id에 해당하는 데이터가 들어 있는# bytes
39+
byte_offset = ... # 요청받은 시작 위치
40+
size = 20 * 1024 * 1024 # 요청받은 데이터 크기
41+
42+
chunk = video_data[byte_offset:byte_offset + size]
43+
socket.send(chunk)
44+
```
45+
46+
- 대략 5밀리초
47+
- 서버의 최대 스루풋
48+
- 이론적으로 20MB / 5밀리초 = 7.3GB
49+
- 병렬로 새 데이터 덩어리를 요청할 수 있는 클라이언트의 최대 개수
50+
- 1 CPU 초 / 5밀리초 = 200
51+
- **문제**
52+
- 기반 데이터를 `bytes` 인스턴스로 슬라이싱하려면 메모리를 복사해야 하는데 이 과정이 CPU 시간 점유
53+
54+
```python
55+
import timeit
56+
57+
def run_test():
58+
chunk = video_data[byte_offset:byte_offset + size]
59+
# socket.send(chunk)를 호출해야 하지만 벤치마크를 위해 무시한다
60+
61+
result = timeit.timeit(
62+
stmt='run_test()',
63+
globals=globals(),
64+
number=100) / 100
65+
66+
print(f'{result:0.9f}')
67+
68+
>>>
69+
0.004925669
70+
```
71+
72+
## 2. `memoryview` 내장 타입 사용
73+
74+
- CPython 의 고성능 버퍼 프로토콜을 프로그램에 노출
75+
- 버퍼 프로토콜은 파이선 런타임과 C 확장이 bytes 와 같은 객체를 통하지 않고 하부 데이터 버퍼에 접근할 수 있게 해주는 저수준 C API
76+
- 슬라이싱에서 데이터를 복사하지 않고 새로운 `memoryview` 인스턴스를 생성함
77+
78+
```python
79+
data = '동해물과 abc 백두산이 마르고 닳도록'.encode("utf8")
80+
view = memoryview(data)
81+
chunk = view[12:19]
82+
print(chunk)
83+
print('크기:', chunk.nbytes)
84+
print('뷰의 데이터:', chunk.tobytes())
85+
print('내부 데이터:', chunk.obj)
86+
87+
>>>
88+
<memory at 0x000002245F779D00>
89+
크기: 7
90+
뷰의 데이터: b' abc \xeb\xb0'
91+
내부 데이터: b'\xeb\x8f\x99\xed\x95\xb4\xeb\xac\xbc\xea\xb3\xbc abc\xeb\xb0\xb1\xeb\x91\x90\xec\x82\xb0\xec\x9d\xb4 \xeb\xa7\x88\xeb\xa5\xb4\xea\xb3\xa0 \xeb\x8b\xb3\xeb\x8f\x84\xeb\xa1\x9d
92+
```
93+
94+
- zero-copy 연산을 활성화하여 NumPy 같은 수치 계산 C 확장이나 예제 프로그램 같은 I/O 위주 프로그램이 커다란 메모리를 빠르게 처리해야 할 경우 성능 향상 가능
95+
96+
- `bytes` 슬라이스를 `memoryview` 로 변경한 후 벤치마크 수행한 결과
97+
98+
```python
99+
video_view = memoryview(video_data)
100+
def run_test():
101+
chunk = video_view[byte_offset:byte_offset + size]
102+
# socket.send(chunk)를 호출해야 하지만 벤치마크를 위해 무시한다
103+
104+
result = timeit.timeit(
105+
stmt='run_test()',
106+
globals=globals(),
107+
number=100) / 100
108+
109+
print(f'{result:0.9f}')
110+
111+
>>>
112+
0.000000250
113+
```
114+
115+
- 성능 개선으로 인해 프로그램의 성능은 CPU bound 보단 소켓 연결 성능에 따라 제한
116+
117+
- 일부 클라이언트가 여러 사용자에게 방송하기 위해 서버러 라이브 비디오 스트림 보내는 예
118+
- 사용자가 가장 최근에 보낸 비디오 데이터를 캐시에 삽입, 다른 클라이언트가 캐시에 있는 비디오 데이터를 읽도록 해야 함
119+
- `socket.recv` 메소드는 bytes 인스턴스 반환
120+
- 슬라이스 연산과 `bytes.join` 메소드를 사용하여 `byte_offset` 에 있는 기존 캐시 데이터를 새로운 데이터로 splice 가능
121+
- 벤치마크 수행
122+
- 1MB 데이터를 받아 비디오 캐시를 갱신하는데 걸린 시간
123+
- 33밀리초
124+
- 수신 시 최대 스루풋
125+
- 1MB / 33밀리초 = 31MB/s
126+
- 스트리밍 방송 클라이언트는 31개로 제한
127+
128+
```python
129+
socket = ... # 클라이언트가 연결한 소켓
130+
video_cache = ... # 서버로 들어오는 비디오 스트림의 캐시
131+
byte_offset = ... # 데이터 버퍼 위치
132+
size = 1024 * 1024 # 데이터 덩어리 크기
133+
chunk = socket.recv(size)
134+
video_view = memoryview(video_cache)
135+
before = video_view[:byte_offset]
136+
after = video_view[byte_offset + size:]
137+
new_cache = b''.join([before, chunk, after])
138+
```
139+
140+
```python
141+
def run_test():
142+
chunk = socket.recv(size)
143+
before = video_view[:byte_offset]
144+
after = video_view[byte_offset + size:]
145+
new_cache = b''.join([before, chunk, after])
146+
147+
result = timeit.timeit(
148+
stmt='run_test()',
149+
globals=globals(),
150+
number=100) / 100
151+
152+
print(f'{result:0.9f}')
153+
154+
>>>
155+
0.033520550
156+
```
157+
158+
- `bytes` 인스턴스는 read-only 이므로 인덱스를 사용해 값을 변경할 수 없으므로 `bytearrary` 사용
159+
- `bytearray`
160+
- `bytes` 에서 원하는 위치에 있는 값 변경 가능한 가변 버전
161+
162+
```python
163+
my_bytes = b'hello'
164+
my_bytes[0] = b'\x79'
165+
166+
>>>
167+
Traceback ...
168+
TypeError: 'bytes' object does not support item assignment
169+
```
170+
171+
```python
172+
my_array = bytearray('hello 안녕'.encode("utf8")) # b''가 아니라# '' 문자열
173+
my_array[0] = 0x79
174+
print(my_array)
175+
176+
>>>
177+
bytearray(b'yello \xec\x95\x88\xeb\x85\x95')
178+
```
179+
180+
- `memoryview` 를 이용한 개선책
181+
- `socket.recv_info``RawIOBase.readInto` 와 같은 라이브러리 메소드가 버퍼 프로토콜을 사용해 데이터를 빠르게 받아들이거나 읽을 수 있음
182+
183+
```python
184+
video_array = bytearray(video_cache)
185+
write_view = memoryview(video_array)
186+
chunk = write_view[byte_offset:byte_offset + size]
187+
188+
socket.recv_into(chunk)
189+
```
190+
191+
- 벤치마크 수행 결과
192+
193+
```python
194+
def run_test():
195+
chunk = write_view[byte_offset:byte_offset + size]
196+
socket.recv_into(chunk)
197+
198+
result = timeit.timeit(
199+
stmt='run_test()',
200+
globals=globals(),
201+
number=100) / 100
202+
203+
print(f'{result:0.9f}')
204+
205+
>>>
206+
0.000033925
207+
```

0 commit comments

Comments
 (0)