py-sharedmemory 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- memory/__init__.py +1 -0
- memory/converter.py +106 -0
- memory/factory.py +12 -0
- memory/receiver.py +75 -0
- memory/sender.py +149 -0
- py_sharedmemory-1.0.0.dist-info/LICENSE +21 -0
- py_sharedmemory-1.0.0.dist-info/METADATA +179 -0
- py_sharedmemory-1.0.0.dist-info/RECORD +10 -0
- py_sharedmemory-1.0.0.dist-info/WHEEL +5 -0
- py_sharedmemory-1.0.0.dist-info/top_level.txt +1 -0
memory/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .factory import create_shared_memory_pair, SharedMemorySender, SharedMemoryReceiver
|
memory/converter.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
def _encode_int(item:int):
|
|
4
|
+
size = item.bit_length() // 8 + 1
|
|
5
|
+
signed = item < 0
|
|
6
|
+
bytes_data = item.to_bytes(size, 'little', signed=signed)
|
|
7
|
+
return ('i', size, signed), memoryview(bytes_data)
|
|
8
|
+
|
|
9
|
+
def _decode_int(byte_data:memoryview, info:tuple):
|
|
10
|
+
return int.from_bytes(byte_data, 'little', signed=info[2])
|
|
11
|
+
|
|
12
|
+
def _encode_float(item:float):
|
|
13
|
+
byte_data = np.float64(item).tobytes()
|
|
14
|
+
return ('f', 8), memoryview(byte_data)
|
|
15
|
+
|
|
16
|
+
def _decode_float(byte_data:memoryview, info:tuple):
|
|
17
|
+
return np.frombuffer(byte_data, dtype=np.float64)[0]
|
|
18
|
+
|
|
19
|
+
def _encode_bool(item:bool):
|
|
20
|
+
byte_data = item.to_bytes(1, 'little')
|
|
21
|
+
return ('b', 1), memoryview(byte_data)
|
|
22
|
+
|
|
23
|
+
def _decode_bool(byte_data:memoryview, info:tuple):
|
|
24
|
+
return bool.from_bytes(byte_data, 'little')
|
|
25
|
+
|
|
26
|
+
def _encode_bytes(item:bytes):
|
|
27
|
+
return ('r', len(item)), memoryview(item)
|
|
28
|
+
|
|
29
|
+
def _decode_bytes(byte_data:memoryview, info:tuple):
|
|
30
|
+
if byte_data is None:
|
|
31
|
+
return b''
|
|
32
|
+
return bytes(byte_data)
|
|
33
|
+
|
|
34
|
+
def _encode_str(item:str):
|
|
35
|
+
bytes_data = str.encode(item)
|
|
36
|
+
return ('s', len(bytes_data)), memoryview(bytes_data)
|
|
37
|
+
|
|
38
|
+
def _decode_str(byte_data:memoryview, info:tuple):
|
|
39
|
+
if byte_data is None:
|
|
40
|
+
return ''
|
|
41
|
+
return bytes(byte_data).decode()
|
|
42
|
+
|
|
43
|
+
def _encode_ndarray(item:np.ndarray):
|
|
44
|
+
byte_data = item.tobytes()
|
|
45
|
+
return ('n', len(byte_data), item.dtype, item.shape), memoryview(byte_data)
|
|
46
|
+
|
|
47
|
+
def _decode_ndarray(byte_data:memoryview, info:tuple):
|
|
48
|
+
return np.frombuffer(byte_data, dtype=info[2]).copy().reshape(info[3])
|
|
49
|
+
|
|
50
|
+
def _encode_ndtype(item:np.dtype):
|
|
51
|
+
byte_data = str.encode(str(item))
|
|
52
|
+
return ('t', len(byte_data)), memoryview(byte_data)
|
|
53
|
+
|
|
54
|
+
def _decode_ndtype(byte_data:memoryview, info:tuple):
|
|
55
|
+
return np.dtype(bytes(byte_data).decode())
|
|
56
|
+
|
|
57
|
+
iterables = {
|
|
58
|
+
tuple: 'p',
|
|
59
|
+
list: 'l',
|
|
60
|
+
dict: 'd',
|
|
61
|
+
set: 'u',
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
encoder = {
|
|
65
|
+
bytes: _encode_bytes,
|
|
66
|
+
int: _encode_int,
|
|
67
|
+
float: _encode_float,
|
|
68
|
+
bool: _encode_bool,
|
|
69
|
+
str: _encode_str,
|
|
70
|
+
np.ndarray: _encode_ndarray,
|
|
71
|
+
}
|
|
72
|
+
decoder = {
|
|
73
|
+
'r': _decode_bytes,
|
|
74
|
+
'i': _decode_int,
|
|
75
|
+
'f': _decode_float,
|
|
76
|
+
'b': _decode_bool,
|
|
77
|
+
's': _decode_str,
|
|
78
|
+
'n': _decode_ndarray,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
def encode(item):
|
|
82
|
+
if item is None:
|
|
83
|
+
return (None, 0), b''
|
|
84
|
+
|
|
85
|
+
datatype = type(item)
|
|
86
|
+
enc = encoder.get(datatype, None)
|
|
87
|
+
if enc is not None:
|
|
88
|
+
return enc(item)
|
|
89
|
+
|
|
90
|
+
if isinstance(item, np.dtype):
|
|
91
|
+
return _encode_ndtype(item)
|
|
92
|
+
|
|
93
|
+
raise ValueError(f"Unsupported data type: {type(item)}")
|
|
94
|
+
|
|
95
|
+
def decode(bytes_data:memoryview, info:tuple):
|
|
96
|
+
if info[0] is None:
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
dec = decoder.get(info[0], None)
|
|
100
|
+
if dec is not None:
|
|
101
|
+
return dec(bytes_data, info)
|
|
102
|
+
|
|
103
|
+
if info[0] == 't':
|
|
104
|
+
return _decode_ndtype(bytes_data, info)
|
|
105
|
+
|
|
106
|
+
raise ValueError(f"Unsupported data type: {info[0]}")
|
memory/factory.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import multiprocessing as mp
|
|
2
|
+
|
|
3
|
+
from .receiver import SharedMemoryReceiver
|
|
4
|
+
from .sender import SharedMemorySender
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def create_shared_memory_pair(capacity):
|
|
8
|
+
q_data = mp.Queue(capacity)
|
|
9
|
+
q_ack = mp.Queue()
|
|
10
|
+
sender = SharedMemorySender(capacity, q_data, q_ack)
|
|
11
|
+
receiver = SharedMemoryReceiver(q_data, q_ack)
|
|
12
|
+
return sender, receiver
|
memory/receiver.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import multiprocessing as mp
|
|
2
|
+
import queue
|
|
3
|
+
from multiprocessing.shared_memory import SharedMemory
|
|
4
|
+
|
|
5
|
+
from .converter import decode, iterables
|
|
6
|
+
|
|
7
|
+
iterable_keys = set(iterables.values())
|
|
8
|
+
non_dict_iterables = set(iterables.keys()) - {dict}
|
|
9
|
+
|
|
10
|
+
class SharedMemoryReceiver:
|
|
11
|
+
def __init__(self, queue_data_in:mp.Queue, queue_ack_out:mp.Queue):
|
|
12
|
+
self.queue_data_in = queue_data_in
|
|
13
|
+
self.queue_ack_out = queue_ack_out
|
|
14
|
+
|
|
15
|
+
def _recover_data_recursive(self, shm:SharedMemory, info:tuple, byte_idx=0):
|
|
16
|
+
if info[0] not in iterable_keys:
|
|
17
|
+
if info[1] == 0: # size
|
|
18
|
+
return decode(None, info)
|
|
19
|
+
return decode(shm.buf[byte_idx:byte_idx+info[1]], info)
|
|
20
|
+
|
|
21
|
+
if info[0] == 'd': # dict
|
|
22
|
+
data = {}
|
|
23
|
+
for info_child, key in zip(info[3], info[4]):
|
|
24
|
+
item = self._recover_data_recursive(shm, info_child, byte_idx)
|
|
25
|
+
byte_idx += info_child[1]
|
|
26
|
+
data[key] = item
|
|
27
|
+
return data
|
|
28
|
+
|
|
29
|
+
# tuple, list, set
|
|
30
|
+
data = []
|
|
31
|
+
for info_child in info[3]:
|
|
32
|
+
item = self._recover_data_recursive(shm, info_child, byte_idx)
|
|
33
|
+
byte_idx += info_child[1]
|
|
34
|
+
data.append(item)
|
|
35
|
+
|
|
36
|
+
if info[2] in non_dict_iterables:
|
|
37
|
+
return info[2](data)
|
|
38
|
+
|
|
39
|
+
# NamedTuple needs to be handled separately
|
|
40
|
+
return info[2](*data)
|
|
41
|
+
|
|
42
|
+
def _recover_data(self, info:tuple):
|
|
43
|
+
if info[2] == 0:
|
|
44
|
+
return self._recover_data_recursive(None, info[1:])
|
|
45
|
+
|
|
46
|
+
name = info[0]
|
|
47
|
+
info = info[1:]
|
|
48
|
+
shm = SharedMemory(name=name, size=info[1])
|
|
49
|
+
data = self._recover_data_recursive(shm, info)
|
|
50
|
+
shm.close()
|
|
51
|
+
return data
|
|
52
|
+
|
|
53
|
+
def get_nowait(self):
|
|
54
|
+
if self.queue_data_in.empty():
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
info = self.queue_data_in.get_nowait()
|
|
59
|
+
except queue.Empty:
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
if info is None:
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
item = self._recover_data(info)
|
|
66
|
+
self.queue_ack_out.put_nowait(info[0])
|
|
67
|
+
del info
|
|
68
|
+
return item
|
|
69
|
+
|
|
70
|
+
def get(self, timeout=None):
|
|
71
|
+
info = self.queue_data_in.get(timeout=timeout)
|
|
72
|
+
item = self._recover_data(info)
|
|
73
|
+
self.queue_ack_out.put_nowait(info[0])
|
|
74
|
+
del info
|
|
75
|
+
return item
|
memory/sender.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import multiprocessing as mp
|
|
2
|
+
import queue
|
|
3
|
+
from multiprocessing.shared_memory import SharedMemory
|
|
4
|
+
|
|
5
|
+
from .converter import encode, iterables
|
|
6
|
+
|
|
7
|
+
class _Separator:
|
|
8
|
+
def __init__(self, obj):
|
|
9
|
+
self.obj = obj
|
|
10
|
+
|
|
11
|
+
def _iterable_handler_list(stack, parents, entry):
|
|
12
|
+
stack.append(_Separator((type(entry), entry.__class__,)))
|
|
13
|
+
parents.append(([], []))
|
|
14
|
+
for field in reversed(entry): # reverse to maintain order in final binary
|
|
15
|
+
stack.append(field)
|
|
16
|
+
|
|
17
|
+
def _iterable_handler_set(stack, parents, entry):
|
|
18
|
+
stack.append(_Separator((set,entry.__class__,)))
|
|
19
|
+
parents.append(([], []))
|
|
20
|
+
for field in entry:
|
|
21
|
+
stack.append(field)
|
|
22
|
+
|
|
23
|
+
def _iterable_handler_dict(stack, parents, entry):
|
|
24
|
+
keys = list(entry.keys())
|
|
25
|
+
stack.append(_Separator((dict,entry.__class__,keys)))
|
|
26
|
+
parents.append(([], []))
|
|
27
|
+
for key in reversed(keys): # reverse to maintain order in final binary
|
|
28
|
+
stack.append(entry[key])
|
|
29
|
+
|
|
30
|
+
iterable_handlers = {
|
|
31
|
+
list: _iterable_handler_list,
|
|
32
|
+
tuple: _iterable_handler_list,
|
|
33
|
+
set: _iterable_handler_set,
|
|
34
|
+
dict: _iterable_handler_dict,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
tuple_key = iterables[tuple]
|
|
38
|
+
|
|
39
|
+
class SharedMemorySender:
|
|
40
|
+
def __init__(self, capacity, queue_data_out:mp.Queue, queue_ack_in:mp.Queue):
|
|
41
|
+
self.open_handles = {}
|
|
42
|
+
self.queue_data_out = queue_data_out
|
|
43
|
+
self.queue_ack_in = queue_ack_in
|
|
44
|
+
self.capacity = capacity
|
|
45
|
+
|
|
46
|
+
def _prepare_binary(self, data):
|
|
47
|
+
parents:list[tuple[list[memoryview], list[tuple]]] = []
|
|
48
|
+
stack = [data]
|
|
49
|
+
while stack:
|
|
50
|
+
entry = stack.pop()
|
|
51
|
+
|
|
52
|
+
if isinstance(entry, _Separator):
|
|
53
|
+
# children processed, gather the results
|
|
54
|
+
bytes_data, child_infos = parents.pop()
|
|
55
|
+
entry_type = entry.obj[0]
|
|
56
|
+
key = iterables.get(entry_type, tuple_key)
|
|
57
|
+
size = sum(info[1] for info in child_infos)
|
|
58
|
+
clazz = entry.obj[1]
|
|
59
|
+
if entry_type == dict:
|
|
60
|
+
info = (key, size, clazz, child_infos, entry.obj[2])
|
|
61
|
+
else:
|
|
62
|
+
info = (key, size, clazz, child_infos)
|
|
63
|
+
|
|
64
|
+
if not parents:
|
|
65
|
+
# root entry was tuple, return the result
|
|
66
|
+
del parents, stack
|
|
67
|
+
return info, bytes_data
|
|
68
|
+
|
|
69
|
+
# append to parent binary
|
|
70
|
+
parents[-1][0].extend(bytes_data)
|
|
71
|
+
parents[-1][1].append(info)
|
|
72
|
+
continue
|
|
73
|
+
|
|
74
|
+
entry_type = type(entry)
|
|
75
|
+
if entry_type in iterables:
|
|
76
|
+
iterable_handlers[entry_type](stack, parents, entry)
|
|
77
|
+
continue
|
|
78
|
+
if isinstance(entry, tuple):
|
|
79
|
+
# NamedTuple
|
|
80
|
+
_iterable_handler_list(stack, parents, entry)
|
|
81
|
+
continue
|
|
82
|
+
|
|
83
|
+
info, bytes_data = encode(entry)
|
|
84
|
+
if not parents:
|
|
85
|
+
# root entry was single data, return the result
|
|
86
|
+
del parents, stack
|
|
87
|
+
return info, [bytes_data]
|
|
88
|
+
|
|
89
|
+
parents[-1][0].append(bytes_data)
|
|
90
|
+
parents[-1][1].append(info)
|
|
91
|
+
|
|
92
|
+
def put(self, data):
|
|
93
|
+
self.process_ack()
|
|
94
|
+
while len(self.open_handles) >= self.capacity:
|
|
95
|
+
self.wait_for_ack()
|
|
96
|
+
|
|
97
|
+
info, bytes_datas = self._prepare_binary(data)
|
|
98
|
+
size = info[1]
|
|
99
|
+
if size == 0:
|
|
100
|
+
info = (None, *info)
|
|
101
|
+
self.queue_data_out.put(info)
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
shm = SharedMemory(create=True, size=size)
|
|
105
|
+
info = (shm.name, *info)
|
|
106
|
+
|
|
107
|
+
offset = 0
|
|
108
|
+
for item in bytes_datas:
|
|
109
|
+
size = len(item)
|
|
110
|
+
shm.buf[offset:offset + size] = item
|
|
111
|
+
offset += size
|
|
112
|
+
|
|
113
|
+
self.queue_data_out.put(info)
|
|
114
|
+
self.open_handles[shm.name] = shm
|
|
115
|
+
|
|
116
|
+
def close_handle(self, shm_name:str):
|
|
117
|
+
shm = self.open_handles[shm_name]
|
|
118
|
+
shm.close()
|
|
119
|
+
shm.unlink()
|
|
120
|
+
del self.open_handles[shm_name]
|
|
121
|
+
|
|
122
|
+
def has_space(self):
|
|
123
|
+
self.process_ack()
|
|
124
|
+
return len(self.open_handles) < self.capacity
|
|
125
|
+
|
|
126
|
+
def process_ack(self):
|
|
127
|
+
try:
|
|
128
|
+
while True:
|
|
129
|
+
retour = self.queue_ack_in.get_nowait()
|
|
130
|
+
if retour is None:
|
|
131
|
+
return
|
|
132
|
+
self.close_handle(retour)
|
|
133
|
+
except queue.Empty:
|
|
134
|
+
pass
|
|
135
|
+
|
|
136
|
+
def close_all(self):
|
|
137
|
+
for shm_name in list(self.open_handles.keys()):
|
|
138
|
+
self.close_handle(shm_name)
|
|
139
|
+
|
|
140
|
+
def wait_for_ack(self):
|
|
141
|
+
shm_name = self.queue_ack_in.get()
|
|
142
|
+
self.close_handle(shm_name)
|
|
143
|
+
|
|
144
|
+
def wait_for_all_ack(self):
|
|
145
|
+
while len(self.open_handles) > 0:
|
|
146
|
+
self.wait_for_ack()
|
|
147
|
+
|
|
148
|
+
def __del__(self):
|
|
149
|
+
self.close_all()
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 товіаѕ
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: py-sharedmemory
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Shared Memory Pipe for Fast Multiprocessing Data Sharing of Large Objects (>1MB)
|
|
5
|
+
Author: Tobias Würth
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2024 товіаѕ
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Repository, https://github.com/tobiaswuerth/python_shared_memory_queue
|
|
29
|
+
Project-URL: Bug Tracker, https://github.com/tobiaswuerth/python_shared_memory_queue/issues
|
|
30
|
+
Keywords: python,python3,multiprocessing,sharedmemory,queue,pipe
|
|
31
|
+
Classifier: Programming Language :: Python :: 3
|
|
32
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
33
|
+
Classifier: Operating System :: OS Independent
|
|
34
|
+
Requires-Python: >=3.9
|
|
35
|
+
Description-Content-Type: text/markdown
|
|
36
|
+
License-File: LICENSE
|
|
37
|
+
Requires-Dist: numpy >=1.26
|
|
38
|
+
|
|
39
|
+
# Shared Memory Pipe for Fast Multiprocessing Data Sharing of Large Objects (>1MB)
|
|
40
|
+
|
|
41
|
+
Since [multiprocessing](https://docs.python.org/3/library/multiprocessing.html) [Queue](https://docs.python.org/3/library/multiprocessing.html#multiprocessing.Queue)s pickles the data, there is a certain performance decrease for larger data objects and huge amounts of I/O.
|
|
42
|
+
|
|
43
|
+
This is the reason why I created a substitute which instead converts data objects into bytes (if needed), puts the bytes into shared memory and only transfers meta-data about the data structure via Queues to the receiving end.
|
|
44
|
+
|
|
45
|
+
The following data types are currently supported:
|
|
46
|
+
```
|
|
47
|
+
- bytes
|
|
48
|
+
- int
|
|
49
|
+
- float
|
|
50
|
+
- bool
|
|
51
|
+
- str
|
|
52
|
+
- np.ndarray
|
|
53
|
+
- np.dtype
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
As well as any nested structures for the given types:
|
|
57
|
+
```
|
|
58
|
+
- tuple/NamedTuple
|
|
59
|
+
- list
|
|
60
|
+
- dict
|
|
61
|
+
- set
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
# Considerations
|
|
65
|
+
There is a certain overhead to the conversion process which is especially noticable for smaller objects and more complex data structures.
|
|
66
|
+
Use the following approach depending on the size of the data you're handling:
|
|
67
|
+
|
|
68
|
+
||10B|100B|1KB|10KB|100KB|1MB|10MB|100MB|1GB|10GB|
|
|
69
|
+
|---|---|---|---|---|---|---|---|---|---|---|
|
|
70
|
+
|``mp.Queue()``|✅|✅|✅|✅|✅|❌|❌|❌|❌|❌|
|
|
71
|
+
|``SharedMemory``|❌|❌|❌|❌|❌|✅|✅|✅|✅|✅|
|
|
72
|
+
|
|
73
|
+
Actual performance heavily relies on your memory speed.
|
|
74
|
+
|
|
75
|
+
# Usage example
|
|
76
|
+
```python
|
|
77
|
+
import multiprocessing as mp
|
|
78
|
+
|
|
79
|
+
from memory import create_shared_memory_pair, SharedMemorySender, SharedMemoryReceiver
|
|
80
|
+
|
|
81
|
+
def producer_sm(sender:SharedMemorySender):
|
|
82
|
+
your_data = "your data"
|
|
83
|
+
|
|
84
|
+
# ...
|
|
85
|
+
sender.put(your_data) # blocks until there is space
|
|
86
|
+
|
|
87
|
+
# or
|
|
88
|
+
if sender.has_space():
|
|
89
|
+
sender.put(your_data)
|
|
90
|
+
else:
|
|
91
|
+
# your else case
|
|
92
|
+
|
|
93
|
+
# ...
|
|
94
|
+
sender.wait_for_all_ack() # wait for all data to be received before closing the sender
|
|
95
|
+
# exit
|
|
96
|
+
|
|
97
|
+
def consumer_sm(receiver:SharedMemoryReceiver, loops, size):
|
|
98
|
+
# ...
|
|
99
|
+
data = receiver.get() # blocks
|
|
100
|
+
|
|
101
|
+
# or
|
|
102
|
+
data = receiver.get(timeout=3) # raises queue.Empty exception after 3s
|
|
103
|
+
|
|
104
|
+
# or
|
|
105
|
+
data = receiver.get_nowait()
|
|
106
|
+
if data is None:
|
|
107
|
+
# handle empty case
|
|
108
|
+
|
|
109
|
+
# ...
|
|
110
|
+
|
|
111
|
+
if __name__ == '__main__':
|
|
112
|
+
sender, receiver = create_shared_memory_pair(capacity=5)
|
|
113
|
+
|
|
114
|
+
mp.Process(target=producer_sm, args=(sender,)).start()
|
|
115
|
+
mp.Process(target=consumer_sm, args=(receiver,)).start()
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
# Performance Testing
|
|
120
|
+
Note that in this testing example Producer and Consumer where dependent upon each other (due to Queue capacity), that's why they take a similar amount of time. Your actual performance may vary depending on the data type, structure and overall system performance.
|
|
121
|
+
|
|
122
|
+
Since there is no inter-process data transfer, I/O drops accordingly. E.g. in my case -2.4GB/s
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
--------------------
|
|
126
|
+
Bytes: 100 (100 B), loops: 100000, queue capacity: 1000
|
|
127
|
+
Producer done in 0.78797s @ 12.10 MB/s
|
|
128
|
+
Consumer done in 0.78698s @ 12.12 MB/s
|
|
129
|
+
Producer done in SM 5.37207s @ 1.78 MB/s
|
|
130
|
+
Consumer done in SM 5.37207s @ 1.78 MB/s ❌
|
|
131
|
+
--------------------
|
|
132
|
+
Bytes: 1000 (1000 B), loops: 100000, queue capacity: 1000
|
|
133
|
+
Producer done in 0.92900s @ 102.66 MB/s
|
|
134
|
+
Consumer done in 0.91599s @ 104.11 MB/s
|
|
135
|
+
Producer done in SM 5.45683s @ 17.48 MB/s
|
|
136
|
+
Consumer done in SM 5.45582s @ 17.48 MB/s ❌
|
|
137
|
+
--------------------
|
|
138
|
+
Bytes: 10000 (9.77 KB), loops: 100000, queue capacity: 1000
|
|
139
|
+
Producer done in 2.16847s @ 439.79 MB/s
|
|
140
|
+
Consumer done in 2.16147s @ 441.22 MB/s
|
|
141
|
+
Producer done in SM 5.64625s @ 168.90 MB/s
|
|
142
|
+
Consumer done in SM 5.64625s @ 168.90 MB/s ❌
|
|
143
|
+
--------------------
|
|
144
|
+
Bytes: 100000 (97.66 KB), loops: 100000, queue capacity: 1000
|
|
145
|
+
Producer done in 2.80213s @ 3.32 GB/s
|
|
146
|
+
Consumer done in 2.80013s @ 3.33 GB/s
|
|
147
|
+
Producer done in SM 8.24400s @ 1.13 GB/s
|
|
148
|
+
Consumer done in SM 8.24200s @ 1.13 GB/s ❌
|
|
149
|
+
--------------------
|
|
150
|
+
Bytes: 1000000 (976.56 KB), loops: 10000, queue capacity: 1000
|
|
151
|
+
Producer done in 4.87300s @ 1.91 GB/s
|
|
152
|
+
Consumer done in 4.87300s @ 1.91 GB/s
|
|
153
|
+
Producer done in SM 4.01900s @ 2.32 GB/s
|
|
154
|
+
Consumer done in SM 4.01800s @ 2.32 GB/s ✅
|
|
155
|
+
--------------------
|
|
156
|
+
Bytes: 10000000 (9.54 MB), loops: 1000, queue capacity: 1000
|
|
157
|
+
Producer done in 6.25722s @ 1.49 GB/s
|
|
158
|
+
Consumer done in 6.28221s @ 1.48 GB/s
|
|
159
|
+
Producer done in SM 3.79851s @ 2.45 GB/s
|
|
160
|
+
Consumer done in SM 3.80359s @ 2.45 GB/s ✅
|
|
161
|
+
--------------------
|
|
162
|
+
Bytes: 100000000 (95.37 MB), loops: 1000, queue capacity: 100
|
|
163
|
+
Producer done in 64.91876s @ 1.43 GB/s
|
|
164
|
+
Consumer done in 65.20476s @ 1.43 GB/s
|
|
165
|
+
Producer done in SM 38.08093s @ 2.45 GB/s
|
|
166
|
+
Consumer done in SM 38.17893s @ 2.44 GB/s ✅
|
|
167
|
+
--------------------
|
|
168
|
+
Bytes: 1000000000 (953.67 MB), loops: 100, queue capacity: 10
|
|
169
|
+
Producer done in 63.22359s @ 1.47 GB/s
|
|
170
|
+
Consumer done in 66.07801s @ 1.41 GB/s
|
|
171
|
+
Producer done in SM 36.39488s @ 2.56 GB/s
|
|
172
|
+
Consumer done in SM 37.61108s @ 2.48 GB/s ✅
|
|
173
|
+
--------------------
|
|
174
|
+
Bytes: 10000000000 (9.31 GB), loops: 10, queue capacity: 10
|
|
175
|
+
Producer done in CRASHED
|
|
176
|
+
Consumer done in CRASHED
|
|
177
|
+
Producer done in SM 28.21499s @ 3.30 GB/s
|
|
178
|
+
Consumer done in SM 34.32684s @ 2.71 GB/s ✅✅
|
|
179
|
+
```
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
memory/__init__.py,sha256=3FFTy2frl1KxNej5rvximE5z6oCVlORC2Ia8Ac9jTkQ,88
|
|
2
|
+
memory/converter.py,sha256=S_zqjuaJpWzK2eGmBLUCPip32kSkZY82M7AVg_7Ryi0,2966
|
|
3
|
+
memory/factory.py,sha256=PD1zdr9srMbbNYhkI8dRz_7e8Wq420uTX07WfExs4sQ,356
|
|
4
|
+
memory/receiver.py,sha256=ZUFuP6GS1E-oAQmGpCF--vWjn_5Jb2dHi4aL2fb-8k0,2398
|
|
5
|
+
memory/sender.py,sha256=IO-KyWu5tqG4avd1sAOLiVy__g2sdMRmfYUamgxn44w,4820
|
|
6
|
+
py_sharedmemory-1.0.0.dist-info/LICENSE,sha256=h0J2LgUZdY3fXKnyBle1HiJoBrUk8dIZpXfVeBvgIe0,1090
|
|
7
|
+
py_sharedmemory-1.0.0.dist-info/METADATA,sha256=uMhsgDJO3PUAFPcXmcXtuY2A0KR8135Mf7LaIC4cfoA,7158
|
|
8
|
+
py_sharedmemory-1.0.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
9
|
+
py_sharedmemory-1.0.0.dist-info/top_level.txt,sha256=plUqRjD5ukqJEMYsc-bhdIrLgYnGmx3mALaoMw-3E8o,7
|
|
10
|
+
py_sharedmemory-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
memory
|