py-sharedmemory 1.0.0__tar.gz → 1.0.1__tar.gz
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.
- py_sharedmemory-1.0.1/PKG-INFO +241 -0
- py_sharedmemory-1.0.1/README.md +224 -0
- {py_sharedmemory-1.0.0 → py_sharedmemory-1.0.1}/memory/sender.py +2 -2
- py_sharedmemory-1.0.1/py_sharedmemory.egg-info/PKG-INFO +241 -0
- {py_sharedmemory-1.0.0 → py_sharedmemory-1.0.1}/pyproject.toml +4 -5
- py_sharedmemory-1.0.0/PKG-INFO +0 -179
- py_sharedmemory-1.0.0/README.md +0 -141
- py_sharedmemory-1.0.0/py_sharedmemory.egg-info/PKG-INFO +0 -179
- {py_sharedmemory-1.0.0 → py_sharedmemory-1.0.1}/LICENSE +0 -0
- {py_sharedmemory-1.0.0 → py_sharedmemory-1.0.1}/memory/__init__.py +0 -0
- {py_sharedmemory-1.0.0 → py_sharedmemory-1.0.1}/memory/converter.py +0 -0
- {py_sharedmemory-1.0.0 → py_sharedmemory-1.0.1}/memory/factory.py +0 -0
- {py_sharedmemory-1.0.0 → py_sharedmemory-1.0.1}/memory/receiver.py +0 -0
- {py_sharedmemory-1.0.0 → py_sharedmemory-1.0.1}/py_sharedmemory.egg-info/SOURCES.txt +0 -0
- {py_sharedmemory-1.0.0 → py_sharedmemory-1.0.1}/py_sharedmemory.egg-info/dependency_links.txt +0 -0
- {py_sharedmemory-1.0.0 → py_sharedmemory-1.0.1}/py_sharedmemory.egg-info/requires.txt +0 -0
- {py_sharedmemory-1.0.0 → py_sharedmemory-1.0.1}/py_sharedmemory.egg-info/top_level.txt +0 -0
- {py_sharedmemory-1.0.0 → py_sharedmemory-1.0.1}/setup.cfg +0 -0
- {py_sharedmemory-1.0.0 → py_sharedmemory-1.0.1}/tests/test_shared_memory.py +0 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: py-sharedmemory
|
|
3
|
+
Version: 1.0.1
|
|
4
|
+
Summary: A replacement for Python multiprocessing.Queue that moves data objects into shared memory as raw bytes and sends only minimal metadata between processes — reducing I/O bottlenecks and improving throughput for larger data objects (>1MB).
|
|
5
|
+
Author: Tobias Würth
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/tobiaswuerth/python_shared_memory_queue
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/tobiaswuerth/python_shared_memory_queue/issues
|
|
9
|
+
Keywords: python,performance,queue,multiprocessing,numpy,zero-copy,data-transfer,low-latency,inter-process-communication,shared-memory,high-throughput,parallel-processing,large-data,memoryview
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Requires-Python: >=3.9
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: numpy>=1.26
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
# py-sharedmemory: High-Performance Multiprocessing Queue Alternative for Large Data
|
|
19
|
+
|
|
20
|
+
Python's standard `multiprocessing.Queue` pickles data for inter-process communication. This often creates performance bottlenecks in demanding applications like Deep Reinforcement Learning (DeepRL) training, scientific computing, or distributed systems that handle large data objects (e.g., NumPy arrays, tensors >1MB) or involve high I/O volumes between many processes (like actors, replay buffers, and trainers).
|
|
21
|
+
|
|
22
|
+
`py-sharedmemory` offers a high-performance replacement. It bypasses pickling by converting data into bytes and moving them into shared memory. Only minimal metadata is sent via the standard queue mechanism. This significantly reduces serialization overhead and inter-process I/O, drastically improving data transfer throughput for data-intensive Python multiprocessing tasks. If your application struggles with slow `multiprocessing.Queue` performance due to large object transfers, `py-sharedmemory` provides a faster alternative.
|
|
23
|
+
|
|
24
|
+
Install using:
|
|
25
|
+
```
|
|
26
|
+
pip install py-sharedmemory
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
# Usage example
|
|
32
|
+
```python
|
|
33
|
+
import multiprocessing as mp
|
|
34
|
+
|
|
35
|
+
from memory import create_shared_memory_pair, SharedMemorySender, SharedMemoryReceiver
|
|
36
|
+
|
|
37
|
+
def producer_sm(sender:SharedMemorySender):
|
|
38
|
+
your_data = "your data"
|
|
39
|
+
|
|
40
|
+
sender.put(your_data) # blocks until there is space
|
|
41
|
+
# ...
|
|
42
|
+
|
|
43
|
+
# or
|
|
44
|
+
if sender.has_space():
|
|
45
|
+
sender.put(your_data)
|
|
46
|
+
# ...
|
|
47
|
+
|
|
48
|
+
sender.wait_for_all_ack() # wait for all data to be received before closing the sender
|
|
49
|
+
# ...
|
|
50
|
+
|
|
51
|
+
def consumer_sm(receiver:SharedMemoryReceiver, loops, size):
|
|
52
|
+
data = receiver.get() # blocks
|
|
53
|
+
# ...
|
|
54
|
+
|
|
55
|
+
# or
|
|
56
|
+
data = receiver.get(timeout=3) # raises queue.Empty exception after 3s
|
|
57
|
+
# ...
|
|
58
|
+
|
|
59
|
+
# or
|
|
60
|
+
data = receiver.get_nowait()
|
|
61
|
+
if data is None:
|
|
62
|
+
# handle empty case
|
|
63
|
+
# ...
|
|
64
|
+
|
|
65
|
+
if __name__ == '__main__':
|
|
66
|
+
sender, receiver = create_shared_memory_pair(capacity=5)
|
|
67
|
+
|
|
68
|
+
mp.Process(target=producer_sm, args=(sender,)).start()
|
|
69
|
+
mp.Process(target=consumer_sm, args=(receiver,)).start()
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
# Supported Types
|
|
75
|
+
|
|
76
|
+
The following data types are currently supported:
|
|
77
|
+
```
|
|
78
|
+
- bytes
|
|
79
|
+
- int
|
|
80
|
+
- float
|
|
81
|
+
- bool
|
|
82
|
+
- str
|
|
83
|
+
- np.ndarray
|
|
84
|
+
- np.dtype
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
As well as any nested structures of the following types:
|
|
88
|
+
```
|
|
89
|
+
- tuple/NamedTuple
|
|
90
|
+
- list
|
|
91
|
+
- dict
|
|
92
|
+
- set
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
# Explanation
|
|
98
|
+
|
|
99
|
+
Let's assume I have the following data:
|
|
100
|
+
```python
|
|
101
|
+
my_data = {
|
|
102
|
+
"idx": 12345,
|
|
103
|
+
"source": "actor_3",
|
|
104
|
+
"data_batch": np.zeros((64, 80, 4, 84, 84, 3), dtype=np.float32)
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
This object uses about ~1.7GB of RAM. Considering a bigger application with multiple processes generating, storing, providing and processing data, the total amount of inter-process I/O is quickly in the hundreds of GB/s. This is demanding on the system.
|
|
108
|
+
|
|
109
|
+
Using `py-sharedmemory` the data is instead moved into the system shared memory, which is accessible from other processes using a key. The information transferred via standard multiprocessing queues is reduced to this meta-data used for accessing and reconstructing the object:
|
|
110
|
+
```python
|
|
111
|
+
(
|
|
112
|
+
'wnsm_1adcf1aa', # shared memory key
|
|
113
|
+
'd', 1734082569, # total byte size
|
|
114
|
+
<class 'dict'>, [ # data structure information
|
|
115
|
+
('i', 2, False),
|
|
116
|
+
('s', 7),
|
|
117
|
+
('n', 1734082560, dtype('float32'), (64, 80, 4, 84, 84, 3))
|
|
118
|
+
], ['idx', 'source', 'data_batch']
|
|
119
|
+
)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
<details>
|
|
123
|
+
|
|
124
|
+
<summary>Large example</summary>
|
|
125
|
+
|
|
126
|
+
Assuming this is your data object:
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
from typing import NamedTuple
|
|
130
|
+
class TestNamedTuple(NamedTuple):
|
|
131
|
+
name: str
|
|
132
|
+
value: int
|
|
133
|
+
|
|
134
|
+
extensive_test_data = {
|
|
135
|
+
"test_id": 98765,
|
|
136
|
+
"description": "This is a comprehensive test data object.",
|
|
137
|
+
"is_active": True,
|
|
138
|
+
"parameters": {
|
|
139
|
+
"learning_rate": 0.001,
|
|
140
|
+
"batch_size": 128,
|
|
141
|
+
"optimizer": "adam",
|
|
142
|
+
"epsilon": 1e-8,
|
|
143
|
+
"byte_config": b'\x01\x02\x03\x04'
|
|
144
|
+
},
|
|
145
|
+
"data_samples": [
|
|
146
|
+
{
|
|
147
|
+
"sample_idx": 0,
|
|
148
|
+
"image_data": np.random.rand(32, 32, 3).astype(np.float32),
|
|
149
|
+
"label": 10,
|
|
150
|
+
"confidence": 0.95,
|
|
151
|
+
"flags": [True, False, True],
|
|
152
|
+
"metadata": ("image", "png", 32, 32)
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
"sample_idx": 1,
|
|
156
|
+
"image_data": np.zeros((16, 16, 1), dtype=np.uint8),
|
|
157
|
+
"label": 5,
|
|
158
|
+
"confidence": 0.88,
|
|
159
|
+
"flags": [False, False, True],
|
|
160
|
+
"metadata": ("image", "jpeg", 16, 16)
|
|
161
|
+
}
|
|
162
|
+
],
|
|
163
|
+
"settings_list": [
|
|
164
|
+
100,
|
|
165
|
+
"high",
|
|
166
|
+
False,
|
|
167
|
+
0.5,
|
|
168
|
+
b'setting_bytes',
|
|
169
|
+
(1, 'a', True),
|
|
170
|
+
{'nested_list_dict': [1, 2, {'deep_key': 'deep_value'}]},
|
|
171
|
+
np.array([1.1, 2.2, 3.3])
|
|
172
|
+
],
|
|
173
|
+
"unique_identifiers": {101, 202, 303, 101},
|
|
174
|
+
"status_codes": set([b'success', b'pending', b'failed']),
|
|
175
|
+
"data_types_used": [
|
|
176
|
+
np.dtype('float32'),
|
|
177
|
+
np.dtype('int64'),
|
|
178
|
+
np.dtype('uint8'),
|
|
179
|
+
np.dtype('bool_')
|
|
180
|
+
],
|
|
181
|
+
"complex_nested_structure": {
|
|
182
|
+
"level1_dict": {
|
|
183
|
+
"level2_list": [
|
|
184
|
+
"string_in_list",
|
|
185
|
+
12345,
|
|
186
|
+
True,
|
|
187
|
+
(10, 20, 30),
|
|
188
|
+
{"level3_dict": {"array_in_dict": np.arange(5)}},
|
|
189
|
+
TestNamedTuple(name='example', value=99)
|
|
190
|
+
],
|
|
191
|
+
"level2_set": {9, 8, 7, "set_string"},
|
|
192
|
+
"level2_tuple": (
|
|
193
|
+
"tuple_item_1",
|
|
194
|
+
56.78,
|
|
195
|
+
b'tuple_bytes',
|
|
196
|
+
[1, 2, 3],
|
|
197
|
+
{"nested_in_tuple": "hello"}
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
"empty_structures": {
|
|
202
|
+
"empty_list": [],
|
|
203
|
+
"empty_dict": {},
|
|
204
|
+
"empty_set": set(),
|
|
205
|
+
"empty_tuple": ()
|
|
206
|
+
},
|
|
207
|
+
"large_array_example": np.random.rand(64, 80, 4, 84, 84, 3).astype(np.float32)
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Then the meta-data produced for this object is this:
|
|
212
|
+
|
|
213
|
+
`('wnsm_eb8850c8', 'd', 1734095454, <class 'dict'>, [('i', 3, False), ('s', 41), ('b', 1), ('d', 26, <class 'dict'>, [('f', 8), ('i', 2, False), ('s', 4), ('f', 8), ('r', 4)], ['learning_rate', 'batch_size', 'optimizer', 'epsilon', 'byte_config']), ('l', 12591, <class 'list'>, [('d', 12311, <class 'dict'>, [('i', 1, False), ('n', 12288, dtype('float32'), (32, 32, 3)), ('i', 1, False), ('f', 8), ('l', 3, <class 'list'>, [('b', 1), ('b', 1), ('b', 1)]), ('p', 10, <class 'tuple'>, [('s', 5), ('s', 3), ('i', 1, False), ('i', 1, False)])], ['sample_idx', 'image_data', 'label', 'confidence', 'flags', 'metadata']), ('d', 280, <class 'dict'>, [('i', 1, False), ('n', 256, dtype('uint8'), (16, 16, 1)), ('i', 1, False), ('f', 8), ('l', 3, <class 'list'>, [('b', 1), ('b', 1), ('b', 1)]), ('p', 11, <class 'tuple'>, [('s', 5), ('s', 4), ('i', 1, False), ('i', 1, False)])], ['sample_idx', 'image_data', 'label', 'confidence', 'flags', 'metadata'])]), ('l', 66, <class 'list'>, [('i', 1, False), ('s', 4), ('b', 1), ('f', 8), ('r', 13), ('p', 3, <class 'tuple'>, [('i', 1, False), ('s', 1), ('b', 1)]), ('d', 12, <class 'dict'>, [('l', 12, <class 'list'>, [('i', 1, False), ('i', 1, False), ('d', 10, <class 'dict'>, [('s', 10)], ['deep_key'])])], ['nested_list_dict']), ('n', 24, dtype('float64'), (3,))]), ('u', 5, <class 'set'>, [('i', 2, False), ('i', 1, False), ('i', 2, False)]), ('u', 20, <class 'set'>, [('r', 6), ('r', 7), ('r', 7)]), ('l', 21, <class 'list'>, [('t', 7), ('t', 5), ('t', 5), ('t', 4)]), ('d', 120, <class 'dict'>, [('d', 120, <class 'dict'>, [('l', 68, <class 'list'>, [('s', 14), ('i', 2, False), ('b', 1), ('p', 3, <class 'tuple'>, [('i', 1, False), ('i', 1, False), ('i', 1, False)]), ('d', 40, <class 'dict'>, [('d', 40, <class 'dict'>, [('n', 40, dtype('int64'), (5,))], ['array_in_dict'])], ['level3_dict']), ('p', 8, <class '__main__.TestNamedTuple'>, [('s', 7), ('i', 1, False)])]), ('u', 13, <class 'set'>, [('i', 1, False), ('s', 10), ('i', 1, False), ('i', 1, False)]), ('p', 39, <class 'tuple'>, [('s', 12), ('f', 8), ('r', 11), ('l', 3, <class 'list'>, [('i', 1, False), ('i', 1, False), ('i', 1, False)]), ('d', 5, <class 'dict'>, [('s', 5)], ['nested_in_tuple'])])], ['level2_list', 'level2_set', 'level2_tuple'])], ['level1_dict']), ('d', 0, <class 'dict'>, [('l', 0, <class 'list'>, []), ('d', 0, <class 'dict'>, [], []), ('u', 0, <class 'set'>, []), ('p', 0, <class 'tuple'>, [])], ['empty_list', 'empty_dict', 'empty_set', 'empty_tuple']), ('n', 1734082560, dtype('float32'), (64, 80, 4, 84, 84, 3))], ['test_id', 'description', 'is_active', 'parameters', 'data_samples', 'settings_list', 'unique_identifiers', 'status_codes', 'data_types_used', 'complex_nested_structure', 'empty_structures', 'large_array_example'])`
|
|
214
|
+
|
|
215
|
+
</details>
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
# Considerations
|
|
220
|
+
There is a certain overhead to the conversion process which is especially noticable for smaller objects and more complex data structures.
|
|
221
|
+
Use the following heuristic depending on the size of the data you are handling:
|
|
222
|
+
|
|
223
|
+
||10B|100B|1KB|10KB|100KB|1MB|10MB|100MB|1GB|10GB|
|
|
224
|
+
|---|---|---|---|---|---|---|---|---|---|---|
|
|
225
|
+
|``mp.Queue()``|✅|✅|✅|✅|✅|❌|❌|❌|❌|❌|
|
|
226
|
+
|``py-sharedmemory``|❌|❌|❌|❌|❌|✅|✅|✅|✅|✅|
|
|
227
|
+
|
|
228
|
+
Actual performance heavily relies on your memory speed.
|
|
229
|
+
|
|
230
|
+
# Performance Testing
|
|
231
|
+
|
|
232
|
+
I measured the time it takes to send/receive data comparing the standard queue and this shared memory implementation:
|
|
233
|
+
|
|
234
|
+
> Note: The standard queue implementation uses a sender thread, that is why the dark blue bar for 'standard send' apparently has more throughput, since it took less time for each cycle, but the actual time until the data is transfered is longer behind the scenes, making this comparison only somewhat useful.
|
|
235
|
+
|
|
236
|
+

|
|
237
|
+
|
|
238
|
+
What we can see here is that starting at around 1MB sized packages, my `py-sharedmemory` implementation starts to outperform the standard implementation.
|
|
239
|
+
Furthermore, and this is most importantly, no I/O is produced. Also, the standard implementation crashes for 10GB packages.
|
|
240
|
+
|
|
241
|
+
Generally I noticed that my system runs much more smoothly and stable when using `py-sharedmemory` with consistent put/get durations and no application slowdowns, when comparing to the standard implementation.
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# py-sharedmemory: High-Performance Multiprocessing Queue Alternative for Large Data
|
|
2
|
+
|
|
3
|
+
Python's standard `multiprocessing.Queue` pickles data for inter-process communication. This often creates performance bottlenecks in demanding applications like Deep Reinforcement Learning (DeepRL) training, scientific computing, or distributed systems that handle large data objects (e.g., NumPy arrays, tensors >1MB) or involve high I/O volumes between many processes (like actors, replay buffers, and trainers).
|
|
4
|
+
|
|
5
|
+
`py-sharedmemory` offers a high-performance replacement. It bypasses pickling by converting data into bytes and moving them into shared memory. Only minimal metadata is sent via the standard queue mechanism. This significantly reduces serialization overhead and inter-process I/O, drastically improving data transfer throughput for data-intensive Python multiprocessing tasks. If your application struggles with slow `multiprocessing.Queue` performance due to large object transfers, `py-sharedmemory` provides a faster alternative.
|
|
6
|
+
|
|
7
|
+
Install using:
|
|
8
|
+
```
|
|
9
|
+
pip install py-sharedmemory
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Usage example
|
|
15
|
+
```python
|
|
16
|
+
import multiprocessing as mp
|
|
17
|
+
|
|
18
|
+
from memory import create_shared_memory_pair, SharedMemorySender, SharedMemoryReceiver
|
|
19
|
+
|
|
20
|
+
def producer_sm(sender:SharedMemorySender):
|
|
21
|
+
your_data = "your data"
|
|
22
|
+
|
|
23
|
+
sender.put(your_data) # blocks until there is space
|
|
24
|
+
# ...
|
|
25
|
+
|
|
26
|
+
# or
|
|
27
|
+
if sender.has_space():
|
|
28
|
+
sender.put(your_data)
|
|
29
|
+
# ...
|
|
30
|
+
|
|
31
|
+
sender.wait_for_all_ack() # wait for all data to be received before closing the sender
|
|
32
|
+
# ...
|
|
33
|
+
|
|
34
|
+
def consumer_sm(receiver:SharedMemoryReceiver, loops, size):
|
|
35
|
+
data = receiver.get() # blocks
|
|
36
|
+
# ...
|
|
37
|
+
|
|
38
|
+
# or
|
|
39
|
+
data = receiver.get(timeout=3) # raises queue.Empty exception after 3s
|
|
40
|
+
# ...
|
|
41
|
+
|
|
42
|
+
# or
|
|
43
|
+
data = receiver.get_nowait()
|
|
44
|
+
if data is None:
|
|
45
|
+
# handle empty case
|
|
46
|
+
# ...
|
|
47
|
+
|
|
48
|
+
if __name__ == '__main__':
|
|
49
|
+
sender, receiver = create_shared_memory_pair(capacity=5)
|
|
50
|
+
|
|
51
|
+
mp.Process(target=producer_sm, args=(sender,)).start()
|
|
52
|
+
mp.Process(target=consumer_sm, args=(receiver,)).start()
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
# Supported Types
|
|
58
|
+
|
|
59
|
+
The following data types are currently supported:
|
|
60
|
+
```
|
|
61
|
+
- bytes
|
|
62
|
+
- int
|
|
63
|
+
- float
|
|
64
|
+
- bool
|
|
65
|
+
- str
|
|
66
|
+
- np.ndarray
|
|
67
|
+
- np.dtype
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
As well as any nested structures of the following types:
|
|
71
|
+
```
|
|
72
|
+
- tuple/NamedTuple
|
|
73
|
+
- list
|
|
74
|
+
- dict
|
|
75
|
+
- set
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
# Explanation
|
|
81
|
+
|
|
82
|
+
Let's assume I have the following data:
|
|
83
|
+
```python
|
|
84
|
+
my_data = {
|
|
85
|
+
"idx": 12345,
|
|
86
|
+
"source": "actor_3",
|
|
87
|
+
"data_batch": np.zeros((64, 80, 4, 84, 84, 3), dtype=np.float32)
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
This object uses about ~1.7GB of RAM. Considering a bigger application with multiple processes generating, storing, providing and processing data, the total amount of inter-process I/O is quickly in the hundreds of GB/s. This is demanding on the system.
|
|
91
|
+
|
|
92
|
+
Using `py-sharedmemory` the data is instead moved into the system shared memory, which is accessible from other processes using a key. The information transferred via standard multiprocessing queues is reduced to this meta-data used for accessing and reconstructing the object:
|
|
93
|
+
```python
|
|
94
|
+
(
|
|
95
|
+
'wnsm_1adcf1aa', # shared memory key
|
|
96
|
+
'd', 1734082569, # total byte size
|
|
97
|
+
<class 'dict'>, [ # data structure information
|
|
98
|
+
('i', 2, False),
|
|
99
|
+
('s', 7),
|
|
100
|
+
('n', 1734082560, dtype('float32'), (64, 80, 4, 84, 84, 3))
|
|
101
|
+
], ['idx', 'source', 'data_batch']
|
|
102
|
+
)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
<details>
|
|
106
|
+
|
|
107
|
+
<summary>Large example</summary>
|
|
108
|
+
|
|
109
|
+
Assuming this is your data object:
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
from typing import NamedTuple
|
|
113
|
+
class TestNamedTuple(NamedTuple):
|
|
114
|
+
name: str
|
|
115
|
+
value: int
|
|
116
|
+
|
|
117
|
+
extensive_test_data = {
|
|
118
|
+
"test_id": 98765,
|
|
119
|
+
"description": "This is a comprehensive test data object.",
|
|
120
|
+
"is_active": True,
|
|
121
|
+
"parameters": {
|
|
122
|
+
"learning_rate": 0.001,
|
|
123
|
+
"batch_size": 128,
|
|
124
|
+
"optimizer": "adam",
|
|
125
|
+
"epsilon": 1e-8,
|
|
126
|
+
"byte_config": b'\x01\x02\x03\x04'
|
|
127
|
+
},
|
|
128
|
+
"data_samples": [
|
|
129
|
+
{
|
|
130
|
+
"sample_idx": 0,
|
|
131
|
+
"image_data": np.random.rand(32, 32, 3).astype(np.float32),
|
|
132
|
+
"label": 10,
|
|
133
|
+
"confidence": 0.95,
|
|
134
|
+
"flags": [True, False, True],
|
|
135
|
+
"metadata": ("image", "png", 32, 32)
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
"sample_idx": 1,
|
|
139
|
+
"image_data": np.zeros((16, 16, 1), dtype=np.uint8),
|
|
140
|
+
"label": 5,
|
|
141
|
+
"confidence": 0.88,
|
|
142
|
+
"flags": [False, False, True],
|
|
143
|
+
"metadata": ("image", "jpeg", 16, 16)
|
|
144
|
+
}
|
|
145
|
+
],
|
|
146
|
+
"settings_list": [
|
|
147
|
+
100,
|
|
148
|
+
"high",
|
|
149
|
+
False,
|
|
150
|
+
0.5,
|
|
151
|
+
b'setting_bytes',
|
|
152
|
+
(1, 'a', True),
|
|
153
|
+
{'nested_list_dict': [1, 2, {'deep_key': 'deep_value'}]},
|
|
154
|
+
np.array([1.1, 2.2, 3.3])
|
|
155
|
+
],
|
|
156
|
+
"unique_identifiers": {101, 202, 303, 101},
|
|
157
|
+
"status_codes": set([b'success', b'pending', b'failed']),
|
|
158
|
+
"data_types_used": [
|
|
159
|
+
np.dtype('float32'),
|
|
160
|
+
np.dtype('int64'),
|
|
161
|
+
np.dtype('uint8'),
|
|
162
|
+
np.dtype('bool_')
|
|
163
|
+
],
|
|
164
|
+
"complex_nested_structure": {
|
|
165
|
+
"level1_dict": {
|
|
166
|
+
"level2_list": [
|
|
167
|
+
"string_in_list",
|
|
168
|
+
12345,
|
|
169
|
+
True,
|
|
170
|
+
(10, 20, 30),
|
|
171
|
+
{"level3_dict": {"array_in_dict": np.arange(5)}},
|
|
172
|
+
TestNamedTuple(name='example', value=99)
|
|
173
|
+
],
|
|
174
|
+
"level2_set": {9, 8, 7, "set_string"},
|
|
175
|
+
"level2_tuple": (
|
|
176
|
+
"tuple_item_1",
|
|
177
|
+
56.78,
|
|
178
|
+
b'tuple_bytes',
|
|
179
|
+
[1, 2, 3],
|
|
180
|
+
{"nested_in_tuple": "hello"}
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
"empty_structures": {
|
|
185
|
+
"empty_list": [],
|
|
186
|
+
"empty_dict": {},
|
|
187
|
+
"empty_set": set(),
|
|
188
|
+
"empty_tuple": ()
|
|
189
|
+
},
|
|
190
|
+
"large_array_example": np.random.rand(64, 80, 4, 84, 84, 3).astype(np.float32)
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Then the meta-data produced for this object is this:
|
|
195
|
+
|
|
196
|
+
`('wnsm_eb8850c8', 'd', 1734095454, <class 'dict'>, [('i', 3, False), ('s', 41), ('b', 1), ('d', 26, <class 'dict'>, [('f', 8), ('i', 2, False), ('s', 4), ('f', 8), ('r', 4)], ['learning_rate', 'batch_size', 'optimizer', 'epsilon', 'byte_config']), ('l', 12591, <class 'list'>, [('d', 12311, <class 'dict'>, [('i', 1, False), ('n', 12288, dtype('float32'), (32, 32, 3)), ('i', 1, False), ('f', 8), ('l', 3, <class 'list'>, [('b', 1), ('b', 1), ('b', 1)]), ('p', 10, <class 'tuple'>, [('s', 5), ('s', 3), ('i', 1, False), ('i', 1, False)])], ['sample_idx', 'image_data', 'label', 'confidence', 'flags', 'metadata']), ('d', 280, <class 'dict'>, [('i', 1, False), ('n', 256, dtype('uint8'), (16, 16, 1)), ('i', 1, False), ('f', 8), ('l', 3, <class 'list'>, [('b', 1), ('b', 1), ('b', 1)]), ('p', 11, <class 'tuple'>, [('s', 5), ('s', 4), ('i', 1, False), ('i', 1, False)])], ['sample_idx', 'image_data', 'label', 'confidence', 'flags', 'metadata'])]), ('l', 66, <class 'list'>, [('i', 1, False), ('s', 4), ('b', 1), ('f', 8), ('r', 13), ('p', 3, <class 'tuple'>, [('i', 1, False), ('s', 1), ('b', 1)]), ('d', 12, <class 'dict'>, [('l', 12, <class 'list'>, [('i', 1, False), ('i', 1, False), ('d', 10, <class 'dict'>, [('s', 10)], ['deep_key'])])], ['nested_list_dict']), ('n', 24, dtype('float64'), (3,))]), ('u', 5, <class 'set'>, [('i', 2, False), ('i', 1, False), ('i', 2, False)]), ('u', 20, <class 'set'>, [('r', 6), ('r', 7), ('r', 7)]), ('l', 21, <class 'list'>, [('t', 7), ('t', 5), ('t', 5), ('t', 4)]), ('d', 120, <class 'dict'>, [('d', 120, <class 'dict'>, [('l', 68, <class 'list'>, [('s', 14), ('i', 2, False), ('b', 1), ('p', 3, <class 'tuple'>, [('i', 1, False), ('i', 1, False), ('i', 1, False)]), ('d', 40, <class 'dict'>, [('d', 40, <class 'dict'>, [('n', 40, dtype('int64'), (5,))], ['array_in_dict'])], ['level3_dict']), ('p', 8, <class '__main__.TestNamedTuple'>, [('s', 7), ('i', 1, False)])]), ('u', 13, <class 'set'>, [('i', 1, False), ('s', 10), ('i', 1, False), ('i', 1, False)]), ('p', 39, <class 'tuple'>, [('s', 12), ('f', 8), ('r', 11), ('l', 3, <class 'list'>, [('i', 1, False), ('i', 1, False), ('i', 1, False)]), ('d', 5, <class 'dict'>, [('s', 5)], ['nested_in_tuple'])])], ['level2_list', 'level2_set', 'level2_tuple'])], ['level1_dict']), ('d', 0, <class 'dict'>, [('l', 0, <class 'list'>, []), ('d', 0, <class 'dict'>, [], []), ('u', 0, <class 'set'>, []), ('p', 0, <class 'tuple'>, [])], ['empty_list', 'empty_dict', 'empty_set', 'empty_tuple']), ('n', 1734082560, dtype('float32'), (64, 80, 4, 84, 84, 3))], ['test_id', 'description', 'is_active', 'parameters', 'data_samples', 'settings_list', 'unique_identifiers', 'status_codes', 'data_types_used', 'complex_nested_structure', 'empty_structures', 'large_array_example'])`
|
|
197
|
+
|
|
198
|
+
</details>
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
# Considerations
|
|
203
|
+
There is a certain overhead to the conversion process which is especially noticable for smaller objects and more complex data structures.
|
|
204
|
+
Use the following heuristic depending on the size of the data you are handling:
|
|
205
|
+
|
|
206
|
+
||10B|100B|1KB|10KB|100KB|1MB|10MB|100MB|1GB|10GB|
|
|
207
|
+
|---|---|---|---|---|---|---|---|---|---|---|
|
|
208
|
+
|``mp.Queue()``|✅|✅|✅|✅|✅|❌|❌|❌|❌|❌|
|
|
209
|
+
|``py-sharedmemory``|❌|❌|❌|❌|❌|✅|✅|✅|✅|✅|
|
|
210
|
+
|
|
211
|
+
Actual performance heavily relies on your memory speed.
|
|
212
|
+
|
|
213
|
+
# Performance Testing
|
|
214
|
+
|
|
215
|
+
I measured the time it takes to send/receive data comparing the standard queue and this shared memory implementation:
|
|
216
|
+
|
|
217
|
+
> Note: The standard queue implementation uses a sender thread, that is why the dark blue bar for 'standard send' apparently has more throughput, since it took less time for each cycle, but the actual time until the data is transfered is longer behind the scenes, making this comparison only somewhat useful.
|
|
218
|
+
|
|
219
|
+

|
|
220
|
+
|
|
221
|
+
What we can see here is that starting at around 1MB sized packages, my `py-sharedmemory` implementation starts to outperform the standard implementation.
|
|
222
|
+
Furthermore, and this is most importantly, no I/O is produced. Also, the standard implementation crashes for 10GB packages.
|
|
223
|
+
|
|
224
|
+
Generally I noticed that my system runs much more smoothly and stable when using `py-sharedmemory` with consistent put/get durations and no application slowdowns, when comparing to the standard implementation.
|
|
@@ -91,7 +91,7 @@ class SharedMemorySender:
|
|
|
91
91
|
|
|
92
92
|
def put(self, data):
|
|
93
93
|
self.process_ack()
|
|
94
|
-
while len(self.open_handles) >= self.capacity:
|
|
94
|
+
while self.capacity and len(self.open_handles) >= self.capacity:
|
|
95
95
|
self.wait_for_ack()
|
|
96
96
|
|
|
97
97
|
info, bytes_datas = self._prepare_binary(data)
|
|
@@ -121,7 +121,7 @@ class SharedMemorySender:
|
|
|
121
121
|
|
|
122
122
|
def has_space(self):
|
|
123
123
|
self.process_ack()
|
|
124
|
-
return len(self.open_handles) < self.capacity
|
|
124
|
+
return not self.capacity or len(self.open_handles) < self.capacity
|
|
125
125
|
|
|
126
126
|
def process_ack(self):
|
|
127
127
|
try:
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: py-sharedmemory
|
|
3
|
+
Version: 1.0.1
|
|
4
|
+
Summary: A replacement for Python multiprocessing.Queue that moves data objects into shared memory as raw bytes and sends only minimal metadata between processes — reducing I/O bottlenecks and improving throughput for larger data objects (>1MB).
|
|
5
|
+
Author: Tobias Würth
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/tobiaswuerth/python_shared_memory_queue
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/tobiaswuerth/python_shared_memory_queue/issues
|
|
9
|
+
Keywords: python,performance,queue,multiprocessing,numpy,zero-copy,data-transfer,low-latency,inter-process-communication,shared-memory,high-throughput,parallel-processing,large-data,memoryview
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Requires-Python: >=3.9
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: numpy>=1.26
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
# py-sharedmemory: High-Performance Multiprocessing Queue Alternative for Large Data
|
|
19
|
+
|
|
20
|
+
Python's standard `multiprocessing.Queue` pickles data for inter-process communication. This often creates performance bottlenecks in demanding applications like Deep Reinforcement Learning (DeepRL) training, scientific computing, or distributed systems that handle large data objects (e.g., NumPy arrays, tensors >1MB) or involve high I/O volumes between many processes (like actors, replay buffers, and trainers).
|
|
21
|
+
|
|
22
|
+
`py-sharedmemory` offers a high-performance replacement. It bypasses pickling by converting data into bytes and moving them into shared memory. Only minimal metadata is sent via the standard queue mechanism. This significantly reduces serialization overhead and inter-process I/O, drastically improving data transfer throughput for data-intensive Python multiprocessing tasks. If your application struggles with slow `multiprocessing.Queue` performance due to large object transfers, `py-sharedmemory` provides a faster alternative.
|
|
23
|
+
|
|
24
|
+
Install using:
|
|
25
|
+
```
|
|
26
|
+
pip install py-sharedmemory
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
# Usage example
|
|
32
|
+
```python
|
|
33
|
+
import multiprocessing as mp
|
|
34
|
+
|
|
35
|
+
from memory import create_shared_memory_pair, SharedMemorySender, SharedMemoryReceiver
|
|
36
|
+
|
|
37
|
+
def producer_sm(sender:SharedMemorySender):
|
|
38
|
+
your_data = "your data"
|
|
39
|
+
|
|
40
|
+
sender.put(your_data) # blocks until there is space
|
|
41
|
+
# ...
|
|
42
|
+
|
|
43
|
+
# or
|
|
44
|
+
if sender.has_space():
|
|
45
|
+
sender.put(your_data)
|
|
46
|
+
# ...
|
|
47
|
+
|
|
48
|
+
sender.wait_for_all_ack() # wait for all data to be received before closing the sender
|
|
49
|
+
# ...
|
|
50
|
+
|
|
51
|
+
def consumer_sm(receiver:SharedMemoryReceiver, loops, size):
|
|
52
|
+
data = receiver.get() # blocks
|
|
53
|
+
# ...
|
|
54
|
+
|
|
55
|
+
# or
|
|
56
|
+
data = receiver.get(timeout=3) # raises queue.Empty exception after 3s
|
|
57
|
+
# ...
|
|
58
|
+
|
|
59
|
+
# or
|
|
60
|
+
data = receiver.get_nowait()
|
|
61
|
+
if data is None:
|
|
62
|
+
# handle empty case
|
|
63
|
+
# ...
|
|
64
|
+
|
|
65
|
+
if __name__ == '__main__':
|
|
66
|
+
sender, receiver = create_shared_memory_pair(capacity=5)
|
|
67
|
+
|
|
68
|
+
mp.Process(target=producer_sm, args=(sender,)).start()
|
|
69
|
+
mp.Process(target=consumer_sm, args=(receiver,)).start()
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
# Supported Types
|
|
75
|
+
|
|
76
|
+
The following data types are currently supported:
|
|
77
|
+
```
|
|
78
|
+
- bytes
|
|
79
|
+
- int
|
|
80
|
+
- float
|
|
81
|
+
- bool
|
|
82
|
+
- str
|
|
83
|
+
- np.ndarray
|
|
84
|
+
- np.dtype
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
As well as any nested structures of the following types:
|
|
88
|
+
```
|
|
89
|
+
- tuple/NamedTuple
|
|
90
|
+
- list
|
|
91
|
+
- dict
|
|
92
|
+
- set
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
# Explanation
|
|
98
|
+
|
|
99
|
+
Let's assume I have the following data:
|
|
100
|
+
```python
|
|
101
|
+
my_data = {
|
|
102
|
+
"idx": 12345,
|
|
103
|
+
"source": "actor_3",
|
|
104
|
+
"data_batch": np.zeros((64, 80, 4, 84, 84, 3), dtype=np.float32)
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
This object uses about ~1.7GB of RAM. Considering a bigger application with multiple processes generating, storing, providing and processing data, the total amount of inter-process I/O is quickly in the hundreds of GB/s. This is demanding on the system.
|
|
108
|
+
|
|
109
|
+
Using `py-sharedmemory` the data is instead moved into the system shared memory, which is accessible from other processes using a key. The information transferred via standard multiprocessing queues is reduced to this meta-data used for accessing and reconstructing the object:
|
|
110
|
+
```python
|
|
111
|
+
(
|
|
112
|
+
'wnsm_1adcf1aa', # shared memory key
|
|
113
|
+
'd', 1734082569, # total byte size
|
|
114
|
+
<class 'dict'>, [ # data structure information
|
|
115
|
+
('i', 2, False),
|
|
116
|
+
('s', 7),
|
|
117
|
+
('n', 1734082560, dtype('float32'), (64, 80, 4, 84, 84, 3))
|
|
118
|
+
], ['idx', 'source', 'data_batch']
|
|
119
|
+
)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
<details>
|
|
123
|
+
|
|
124
|
+
<summary>Large example</summary>
|
|
125
|
+
|
|
126
|
+
Assuming this is your data object:
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
from typing import NamedTuple
|
|
130
|
+
class TestNamedTuple(NamedTuple):
|
|
131
|
+
name: str
|
|
132
|
+
value: int
|
|
133
|
+
|
|
134
|
+
extensive_test_data = {
|
|
135
|
+
"test_id": 98765,
|
|
136
|
+
"description": "This is a comprehensive test data object.",
|
|
137
|
+
"is_active": True,
|
|
138
|
+
"parameters": {
|
|
139
|
+
"learning_rate": 0.001,
|
|
140
|
+
"batch_size": 128,
|
|
141
|
+
"optimizer": "adam",
|
|
142
|
+
"epsilon": 1e-8,
|
|
143
|
+
"byte_config": b'\x01\x02\x03\x04'
|
|
144
|
+
},
|
|
145
|
+
"data_samples": [
|
|
146
|
+
{
|
|
147
|
+
"sample_idx": 0,
|
|
148
|
+
"image_data": np.random.rand(32, 32, 3).astype(np.float32),
|
|
149
|
+
"label": 10,
|
|
150
|
+
"confidence": 0.95,
|
|
151
|
+
"flags": [True, False, True],
|
|
152
|
+
"metadata": ("image", "png", 32, 32)
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
"sample_idx": 1,
|
|
156
|
+
"image_data": np.zeros((16, 16, 1), dtype=np.uint8),
|
|
157
|
+
"label": 5,
|
|
158
|
+
"confidence": 0.88,
|
|
159
|
+
"flags": [False, False, True],
|
|
160
|
+
"metadata": ("image", "jpeg", 16, 16)
|
|
161
|
+
}
|
|
162
|
+
],
|
|
163
|
+
"settings_list": [
|
|
164
|
+
100,
|
|
165
|
+
"high",
|
|
166
|
+
False,
|
|
167
|
+
0.5,
|
|
168
|
+
b'setting_bytes',
|
|
169
|
+
(1, 'a', True),
|
|
170
|
+
{'nested_list_dict': [1, 2, {'deep_key': 'deep_value'}]},
|
|
171
|
+
np.array([1.1, 2.2, 3.3])
|
|
172
|
+
],
|
|
173
|
+
"unique_identifiers": {101, 202, 303, 101},
|
|
174
|
+
"status_codes": set([b'success', b'pending', b'failed']),
|
|
175
|
+
"data_types_used": [
|
|
176
|
+
np.dtype('float32'),
|
|
177
|
+
np.dtype('int64'),
|
|
178
|
+
np.dtype('uint8'),
|
|
179
|
+
np.dtype('bool_')
|
|
180
|
+
],
|
|
181
|
+
"complex_nested_structure": {
|
|
182
|
+
"level1_dict": {
|
|
183
|
+
"level2_list": [
|
|
184
|
+
"string_in_list",
|
|
185
|
+
12345,
|
|
186
|
+
True,
|
|
187
|
+
(10, 20, 30),
|
|
188
|
+
{"level3_dict": {"array_in_dict": np.arange(5)}},
|
|
189
|
+
TestNamedTuple(name='example', value=99)
|
|
190
|
+
],
|
|
191
|
+
"level2_set": {9, 8, 7, "set_string"},
|
|
192
|
+
"level2_tuple": (
|
|
193
|
+
"tuple_item_1",
|
|
194
|
+
56.78,
|
|
195
|
+
b'tuple_bytes',
|
|
196
|
+
[1, 2, 3],
|
|
197
|
+
{"nested_in_tuple": "hello"}
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
"empty_structures": {
|
|
202
|
+
"empty_list": [],
|
|
203
|
+
"empty_dict": {},
|
|
204
|
+
"empty_set": set(),
|
|
205
|
+
"empty_tuple": ()
|
|
206
|
+
},
|
|
207
|
+
"large_array_example": np.random.rand(64, 80, 4, 84, 84, 3).astype(np.float32)
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Then the meta-data produced for this object is this:
|
|
212
|
+
|
|
213
|
+
`('wnsm_eb8850c8', 'd', 1734095454, <class 'dict'>, [('i', 3, False), ('s', 41), ('b', 1), ('d', 26, <class 'dict'>, [('f', 8), ('i', 2, False), ('s', 4), ('f', 8), ('r', 4)], ['learning_rate', 'batch_size', 'optimizer', 'epsilon', 'byte_config']), ('l', 12591, <class 'list'>, [('d', 12311, <class 'dict'>, [('i', 1, False), ('n', 12288, dtype('float32'), (32, 32, 3)), ('i', 1, False), ('f', 8), ('l', 3, <class 'list'>, [('b', 1), ('b', 1), ('b', 1)]), ('p', 10, <class 'tuple'>, [('s', 5), ('s', 3), ('i', 1, False), ('i', 1, False)])], ['sample_idx', 'image_data', 'label', 'confidence', 'flags', 'metadata']), ('d', 280, <class 'dict'>, [('i', 1, False), ('n', 256, dtype('uint8'), (16, 16, 1)), ('i', 1, False), ('f', 8), ('l', 3, <class 'list'>, [('b', 1), ('b', 1), ('b', 1)]), ('p', 11, <class 'tuple'>, [('s', 5), ('s', 4), ('i', 1, False), ('i', 1, False)])], ['sample_idx', 'image_data', 'label', 'confidence', 'flags', 'metadata'])]), ('l', 66, <class 'list'>, [('i', 1, False), ('s', 4), ('b', 1), ('f', 8), ('r', 13), ('p', 3, <class 'tuple'>, [('i', 1, False), ('s', 1), ('b', 1)]), ('d', 12, <class 'dict'>, [('l', 12, <class 'list'>, [('i', 1, False), ('i', 1, False), ('d', 10, <class 'dict'>, [('s', 10)], ['deep_key'])])], ['nested_list_dict']), ('n', 24, dtype('float64'), (3,))]), ('u', 5, <class 'set'>, [('i', 2, False), ('i', 1, False), ('i', 2, False)]), ('u', 20, <class 'set'>, [('r', 6), ('r', 7), ('r', 7)]), ('l', 21, <class 'list'>, [('t', 7), ('t', 5), ('t', 5), ('t', 4)]), ('d', 120, <class 'dict'>, [('d', 120, <class 'dict'>, [('l', 68, <class 'list'>, [('s', 14), ('i', 2, False), ('b', 1), ('p', 3, <class 'tuple'>, [('i', 1, False), ('i', 1, False), ('i', 1, False)]), ('d', 40, <class 'dict'>, [('d', 40, <class 'dict'>, [('n', 40, dtype('int64'), (5,))], ['array_in_dict'])], ['level3_dict']), ('p', 8, <class '__main__.TestNamedTuple'>, [('s', 7), ('i', 1, False)])]), ('u', 13, <class 'set'>, [('i', 1, False), ('s', 10), ('i', 1, False), ('i', 1, False)]), ('p', 39, <class 'tuple'>, [('s', 12), ('f', 8), ('r', 11), ('l', 3, <class 'list'>, [('i', 1, False), ('i', 1, False), ('i', 1, False)]), ('d', 5, <class 'dict'>, [('s', 5)], ['nested_in_tuple'])])], ['level2_list', 'level2_set', 'level2_tuple'])], ['level1_dict']), ('d', 0, <class 'dict'>, [('l', 0, <class 'list'>, []), ('d', 0, <class 'dict'>, [], []), ('u', 0, <class 'set'>, []), ('p', 0, <class 'tuple'>, [])], ['empty_list', 'empty_dict', 'empty_set', 'empty_tuple']), ('n', 1734082560, dtype('float32'), (64, 80, 4, 84, 84, 3))], ['test_id', 'description', 'is_active', 'parameters', 'data_samples', 'settings_list', 'unique_identifiers', 'status_codes', 'data_types_used', 'complex_nested_structure', 'empty_structures', 'large_array_example'])`
|
|
214
|
+
|
|
215
|
+
</details>
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
# Considerations
|
|
220
|
+
There is a certain overhead to the conversion process which is especially noticable for smaller objects and more complex data structures.
|
|
221
|
+
Use the following heuristic depending on the size of the data you are handling:
|
|
222
|
+
|
|
223
|
+
||10B|100B|1KB|10KB|100KB|1MB|10MB|100MB|1GB|10GB|
|
|
224
|
+
|---|---|---|---|---|---|---|---|---|---|---|
|
|
225
|
+
|``mp.Queue()``|✅|✅|✅|✅|✅|❌|❌|❌|❌|❌|
|
|
226
|
+
|``py-sharedmemory``|❌|❌|❌|❌|❌|✅|✅|✅|✅|✅|
|
|
227
|
+
|
|
228
|
+
Actual performance heavily relies on your memory speed.
|
|
229
|
+
|
|
230
|
+
# Performance Testing
|
|
231
|
+
|
|
232
|
+
I measured the time it takes to send/receive data comparing the standard queue and this shared memory implementation:
|
|
233
|
+
|
|
234
|
+
> Note: The standard queue implementation uses a sender thread, that is why the dark blue bar for 'standard send' apparently has more throughput, since it took less time for each cycle, but the actual time until the data is transfered is longer behind the scenes, making this comparison only somewhat useful.
|
|
235
|
+
|
|
236
|
+

|
|
237
|
+
|
|
238
|
+
What we can see here is that starting at around 1MB sized packages, my `py-sharedmemory` implementation starts to outperform the standard implementation.
|
|
239
|
+
Furthermore, and this is most importantly, no I/O is produced. Also, the standard implementation crashes for 10GB packages.
|
|
240
|
+
|
|
241
|
+
Generally I noticed that my system runs much more smoothly and stable when using `py-sharedmemory` with consistent put/get durations and no application slowdowns, when comparing to the standard implementation.
|
|
@@ -8,20 +8,19 @@ packages = ['memory']
|
|
|
8
8
|
[project]
|
|
9
9
|
name = "py-sharedmemory"
|
|
10
10
|
authors = [{name = "Tobias Würth"}]
|
|
11
|
-
version = "1.0.
|
|
11
|
+
version = "1.0.1"
|
|
12
12
|
requires-python = ">=3.9"
|
|
13
13
|
dependencies = [
|
|
14
14
|
"numpy>=1.26"
|
|
15
15
|
]
|
|
16
16
|
|
|
17
|
-
description = "
|
|
18
|
-
keywords = ["python", "
|
|
17
|
+
description = "A replacement for Python multiprocessing.Queue that moves data objects into shared memory as raw bytes and sends only minimal metadata between processes — reducing I/O bottlenecks and improving throughput for larger data objects (>1MB)."
|
|
18
|
+
keywords = ["python", "performance", "queue", "multiprocessing", "numpy", "zero-copy", "data-transfer", "low-latency", "inter-process-communication", "shared-memory", "high-throughput", "parallel-processing", "large-data", "memoryview"]
|
|
19
19
|
readme = {file = "README.md", content-type = "text/markdown"}
|
|
20
|
-
license =
|
|
20
|
+
license = "MIT"
|
|
21
21
|
|
|
22
22
|
classifiers = [
|
|
23
23
|
"Programming Language :: Python :: 3",
|
|
24
|
-
"License :: OSI Approved :: MIT License",
|
|
25
24
|
"Operating System :: OS Independent",
|
|
26
25
|
]
|
|
27
26
|
|
py_sharedmemory-1.0.0/PKG-INFO
DELETED
|
@@ -1,179 +0,0 @@
|
|
|
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
|
-
```
|
py_sharedmemory-1.0.0/README.md
DELETED
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
# Shared Memory Pipe for Fast Multiprocessing Data Sharing of Large Objects (>1MB)
|
|
2
|
-
|
|
3
|
-
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.
|
|
4
|
-
|
|
5
|
-
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.
|
|
6
|
-
|
|
7
|
-
The following data types are currently supported:
|
|
8
|
-
```
|
|
9
|
-
- bytes
|
|
10
|
-
- int
|
|
11
|
-
- float
|
|
12
|
-
- bool
|
|
13
|
-
- str
|
|
14
|
-
- np.ndarray
|
|
15
|
-
- np.dtype
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
As well as any nested structures for the given types:
|
|
19
|
-
```
|
|
20
|
-
- tuple/NamedTuple
|
|
21
|
-
- list
|
|
22
|
-
- dict
|
|
23
|
-
- set
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
# Considerations
|
|
27
|
-
There is a certain overhead to the conversion process which is especially noticable for smaller objects and more complex data structures.
|
|
28
|
-
Use the following approach depending on the size of the data you're handling:
|
|
29
|
-
|
|
30
|
-
||10B|100B|1KB|10KB|100KB|1MB|10MB|100MB|1GB|10GB|
|
|
31
|
-
|---|---|---|---|---|---|---|---|---|---|---|
|
|
32
|
-
|``mp.Queue()``|✅|✅|✅|✅|✅|❌|❌|❌|❌|❌|
|
|
33
|
-
|``SharedMemory``|❌|❌|❌|❌|❌|✅|✅|✅|✅|✅|
|
|
34
|
-
|
|
35
|
-
Actual performance heavily relies on your memory speed.
|
|
36
|
-
|
|
37
|
-
# Usage example
|
|
38
|
-
```python
|
|
39
|
-
import multiprocessing as mp
|
|
40
|
-
|
|
41
|
-
from memory import create_shared_memory_pair, SharedMemorySender, SharedMemoryReceiver
|
|
42
|
-
|
|
43
|
-
def producer_sm(sender:SharedMemorySender):
|
|
44
|
-
your_data = "your data"
|
|
45
|
-
|
|
46
|
-
# ...
|
|
47
|
-
sender.put(your_data) # blocks until there is space
|
|
48
|
-
|
|
49
|
-
# or
|
|
50
|
-
if sender.has_space():
|
|
51
|
-
sender.put(your_data)
|
|
52
|
-
else:
|
|
53
|
-
# your else case
|
|
54
|
-
|
|
55
|
-
# ...
|
|
56
|
-
sender.wait_for_all_ack() # wait for all data to be received before closing the sender
|
|
57
|
-
# exit
|
|
58
|
-
|
|
59
|
-
def consumer_sm(receiver:SharedMemoryReceiver, loops, size):
|
|
60
|
-
# ...
|
|
61
|
-
data = receiver.get() # blocks
|
|
62
|
-
|
|
63
|
-
# or
|
|
64
|
-
data = receiver.get(timeout=3) # raises queue.Empty exception after 3s
|
|
65
|
-
|
|
66
|
-
# or
|
|
67
|
-
data = receiver.get_nowait()
|
|
68
|
-
if data is None:
|
|
69
|
-
# handle empty case
|
|
70
|
-
|
|
71
|
-
# ...
|
|
72
|
-
|
|
73
|
-
if __name__ == '__main__':
|
|
74
|
-
sender, receiver = create_shared_memory_pair(capacity=5)
|
|
75
|
-
|
|
76
|
-
mp.Process(target=producer_sm, args=(sender,)).start()
|
|
77
|
-
mp.Process(target=consumer_sm, args=(receiver,)).start()
|
|
78
|
-
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
# Performance Testing
|
|
82
|
-
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.
|
|
83
|
-
|
|
84
|
-
Since there is no inter-process data transfer, I/O drops accordingly. E.g. in my case -2.4GB/s
|
|
85
|
-
|
|
86
|
-
```
|
|
87
|
-
--------------------
|
|
88
|
-
Bytes: 100 (100 B), loops: 100000, queue capacity: 1000
|
|
89
|
-
Producer done in 0.78797s @ 12.10 MB/s
|
|
90
|
-
Consumer done in 0.78698s @ 12.12 MB/s
|
|
91
|
-
Producer done in SM 5.37207s @ 1.78 MB/s
|
|
92
|
-
Consumer done in SM 5.37207s @ 1.78 MB/s ❌
|
|
93
|
-
--------------------
|
|
94
|
-
Bytes: 1000 (1000 B), loops: 100000, queue capacity: 1000
|
|
95
|
-
Producer done in 0.92900s @ 102.66 MB/s
|
|
96
|
-
Consumer done in 0.91599s @ 104.11 MB/s
|
|
97
|
-
Producer done in SM 5.45683s @ 17.48 MB/s
|
|
98
|
-
Consumer done in SM 5.45582s @ 17.48 MB/s ❌
|
|
99
|
-
--------------------
|
|
100
|
-
Bytes: 10000 (9.77 KB), loops: 100000, queue capacity: 1000
|
|
101
|
-
Producer done in 2.16847s @ 439.79 MB/s
|
|
102
|
-
Consumer done in 2.16147s @ 441.22 MB/s
|
|
103
|
-
Producer done in SM 5.64625s @ 168.90 MB/s
|
|
104
|
-
Consumer done in SM 5.64625s @ 168.90 MB/s ❌
|
|
105
|
-
--------------------
|
|
106
|
-
Bytes: 100000 (97.66 KB), loops: 100000, queue capacity: 1000
|
|
107
|
-
Producer done in 2.80213s @ 3.32 GB/s
|
|
108
|
-
Consumer done in 2.80013s @ 3.33 GB/s
|
|
109
|
-
Producer done in SM 8.24400s @ 1.13 GB/s
|
|
110
|
-
Consumer done in SM 8.24200s @ 1.13 GB/s ❌
|
|
111
|
-
--------------------
|
|
112
|
-
Bytes: 1000000 (976.56 KB), loops: 10000, queue capacity: 1000
|
|
113
|
-
Producer done in 4.87300s @ 1.91 GB/s
|
|
114
|
-
Consumer done in 4.87300s @ 1.91 GB/s
|
|
115
|
-
Producer done in SM 4.01900s @ 2.32 GB/s
|
|
116
|
-
Consumer done in SM 4.01800s @ 2.32 GB/s ✅
|
|
117
|
-
--------------------
|
|
118
|
-
Bytes: 10000000 (9.54 MB), loops: 1000, queue capacity: 1000
|
|
119
|
-
Producer done in 6.25722s @ 1.49 GB/s
|
|
120
|
-
Consumer done in 6.28221s @ 1.48 GB/s
|
|
121
|
-
Producer done in SM 3.79851s @ 2.45 GB/s
|
|
122
|
-
Consumer done in SM 3.80359s @ 2.45 GB/s ✅
|
|
123
|
-
--------------------
|
|
124
|
-
Bytes: 100000000 (95.37 MB), loops: 1000, queue capacity: 100
|
|
125
|
-
Producer done in 64.91876s @ 1.43 GB/s
|
|
126
|
-
Consumer done in 65.20476s @ 1.43 GB/s
|
|
127
|
-
Producer done in SM 38.08093s @ 2.45 GB/s
|
|
128
|
-
Consumer done in SM 38.17893s @ 2.44 GB/s ✅
|
|
129
|
-
--------------------
|
|
130
|
-
Bytes: 1000000000 (953.67 MB), loops: 100, queue capacity: 10
|
|
131
|
-
Producer done in 63.22359s @ 1.47 GB/s
|
|
132
|
-
Consumer done in 66.07801s @ 1.41 GB/s
|
|
133
|
-
Producer done in SM 36.39488s @ 2.56 GB/s
|
|
134
|
-
Consumer done in SM 37.61108s @ 2.48 GB/s ✅
|
|
135
|
-
--------------------
|
|
136
|
-
Bytes: 10000000000 (9.31 GB), loops: 10, queue capacity: 10
|
|
137
|
-
Producer done in CRASHED
|
|
138
|
-
Consumer done in CRASHED
|
|
139
|
-
Producer done in SM 28.21499s @ 3.30 GB/s
|
|
140
|
-
Consumer done in SM 34.32684s @ 2.71 GB/s ✅✅
|
|
141
|
-
```
|
|
@@ -1,179 +0,0 @@
|
|
|
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
|
-
```
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{py_sharedmemory-1.0.0 → py_sharedmemory-1.0.1}/py_sharedmemory.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|