py-sharedmemory 1.0.0__tar.gz → 2.0.0__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.
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2024 товіаѕ
3
+ Copyright (c) 2025 товіаѕ
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -0,0 +1,243 @@
1
+ Metadata-Version: 2.4
2
+ Name: py-sharedmemory
3
+ Version: 2.0.0
4
+ Summary: Optimized Python IPC: Uses shared memory to bypass multiprocessing queue I/O bottlenecks, ideal for large data (1MB+) in scientific computing, RL, etc. Reduces system load and improves latency consistency
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
+ [![PyPI version](https://badge.fury.io/py/py-sharedmemory.svg)](https://badge.fury.io/py/py-sharedmemory)
21
+
22
+ Install using:
23
+ ```
24
+ pip install py-sharedmemory
25
+ ```
26
+
27
+ 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).
28
+
29
+ `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.
30
+
31
+ ---
32
+
33
+ # Usage example
34
+ ```python
35
+ import multiprocessing as mp
36
+
37
+ from memory import create_shared_memory_pair, SharedMemorySender, SharedMemoryReceiver
38
+
39
+ def producer_sm(sender:SharedMemorySender):
40
+ your_data = "your data"
41
+
42
+ sender.put(your_data) # blocks until there is space
43
+ # ...
44
+
45
+ # or
46
+ if sender.has_space():
47
+ sender.put(your_data)
48
+ # ...
49
+
50
+ sender.wait_for_all_ack() # wait for all data to be received before closing the sender
51
+ # ...
52
+
53
+ def consumer_sm(receiver:SharedMemoryReceiver, loops, size):
54
+ data = receiver.get() # blocks
55
+ # ...
56
+
57
+ # or
58
+ data = receiver.get(timeout=3) # raises queue.Empty exception after 3s
59
+ # ...
60
+
61
+ # or
62
+ data = receiver.get_nowait()
63
+ if data is None:
64
+ # handle empty case
65
+ # ...
66
+
67
+ if __name__ == '__main__':
68
+ sender, receiver = create_shared_memory_pair(capacity=5)
69
+
70
+ mp.Process(target=producer_sm, args=(sender,)).start()
71
+ mp.Process(target=consumer_sm, args=(receiver,)).start()
72
+ ```
73
+
74
+ ---
75
+
76
+ # Supported Types
77
+
78
+ The following data types are currently supported:
79
+ ```
80
+ - bytes
81
+ - int
82
+ - float
83
+ - bool
84
+ - str
85
+ - np.ndarray
86
+ - np.dtype
87
+ ```
88
+
89
+ As well as any nested structures of the following types:
90
+ ```
91
+ - tuple/NamedTuple
92
+ - list
93
+ - dict
94
+ - set
95
+ ```
96
+
97
+ ---
98
+
99
+ # Explanation
100
+
101
+ Let's assume I have the following data:
102
+ ```python
103
+ my_data = {
104
+ "idx": 12345,
105
+ "source": "actor_3",
106
+ "data_batch": np.zeros((64, 80, 4, 84, 84, 3), dtype=np.float32)
107
+ }
108
+ ```
109
+ 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.
110
+
111
+ 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:
112
+ ```python
113
+ (
114
+ 'wnsm_1adcf1aa', # shared memory key
115
+ 'd', 1734082569, # total byte size
116
+ <class 'dict'>, [ # data structure information
117
+ ('i', 2, False),
118
+ ('s', 7),
119
+ ('n', 1734082560, dtype('float32'), (64, 80, 4, 84, 84, 3))
120
+ ], ['idx', 'source', 'data_batch']
121
+ )
122
+ ```
123
+
124
+ <details>
125
+
126
+ <summary>Large example</summary>
127
+
128
+ Assuming this is your data object:
129
+
130
+ ```python
131
+ from typing import NamedTuple
132
+ class TestNamedTuple(NamedTuple):
133
+ name: str
134
+ value: int
135
+
136
+ extensive_test_data = {
137
+ "test_id": 98765,
138
+ "description": "This is a comprehensive test data object.",
139
+ "is_active": True,
140
+ "parameters": {
141
+ "learning_rate": 0.001,
142
+ "batch_size": 128,
143
+ "optimizer": "adam",
144
+ "epsilon": 1e-8,
145
+ "byte_config": b'\x01\x02\x03\x04'
146
+ },
147
+ "data_samples": [
148
+ {
149
+ "sample_idx": 0,
150
+ "image_data": np.random.rand(32, 32, 3).astype(np.float32),
151
+ "label": 10,
152
+ "confidence": 0.95,
153
+ "flags": [True, False, True],
154
+ "metadata": ("image", "png", 32, 32)
155
+ },
156
+ {
157
+ "sample_idx": 1,
158
+ "image_data": np.zeros((16, 16, 1), dtype=np.uint8),
159
+ "label": 5,
160
+ "confidence": 0.88,
161
+ "flags": [False, False, True],
162
+ "metadata": ("image", "jpeg", 16, 16)
163
+ }
164
+ ],
165
+ "settings_list": [
166
+ 100,
167
+ "high",
168
+ False,
169
+ 0.5,
170
+ b'setting_bytes',
171
+ (1, 'a', True),
172
+ {'nested_list_dict': [1, 2, {'deep_key': 'deep_value'}]},
173
+ np.array([1.1, 2.2, 3.3])
174
+ ],
175
+ "unique_identifiers": {101, 202, 303, 101},
176
+ "status_codes": set([b'success', b'pending', b'failed']),
177
+ "data_types_used": [
178
+ np.dtype('float32'),
179
+ np.dtype('int64'),
180
+ np.dtype('uint8'),
181
+ np.dtype('bool_')
182
+ ],
183
+ "complex_nested_structure": {
184
+ "level1_dict": {
185
+ "level2_list": [
186
+ "string_in_list",
187
+ 12345,
188
+ True,
189
+ (10, 20, 30),
190
+ {"level3_dict": {"array_in_dict": np.arange(5)}},
191
+ TestNamedTuple(name='example', value=99)
192
+ ],
193
+ "level2_set": {9, 8, 7, "set_string"},
194
+ "level2_tuple": (
195
+ "tuple_item_1",
196
+ 56.78,
197
+ b'tuple_bytes',
198
+ [1, 2, 3],
199
+ {"nested_in_tuple": "hello"}
200
+ )
201
+ }
202
+ },
203
+ "empty_structures": {
204
+ "empty_list": [],
205
+ "empty_dict": {},
206
+ "empty_set": set(),
207
+ "empty_tuple": ()
208
+ },
209
+ "large_array_example": np.random.rand(64, 80, 4, 84, 84, 3).astype(np.float32)
210
+ }
211
+ ```
212
+
213
+ Then the meta-data produced for this object is this:
214
+
215
+ `('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'])`
216
+
217
+ </details>
218
+
219
+ ---
220
+
221
+ # Considerations
222
+ There is a certain overhead to the conversion process which is especially noticable for smaller objects and more complex data structures.
223
+ Use the following heuristic depending on the size of the data you are handling:
224
+
225
+ ||10B|100B|1KB|10KB|100KB|1MB|10MB|100MB|1GB|10GB|
226
+ |---|---|---|---|---|---|---|---|---|---|---|
227
+ |``mp.Queue()``|✅|✅|✅|✅|✅|❌|❌|❌|❌|❌|
228
+ |``py-sharedmemory``|❌|❌|❌|❌|❌|✅|✅|✅|✅|✅|
229
+
230
+ Actual performance heavily relies on your memory speed.
231
+
232
+ # Performance Testing
233
+
234
+ I measured the time it takes to send/receive data comparing the standard queue and this shared memory implementation:
235
+
236
+ > 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.
237
+
238
+ ![performance comparison](https://github.com/user-attachments/assets/2a7b4f93-7698-46d5-8bb8-9d5bc7863c8f)
239
+
240
+ What we can see here is that starting at around 1MB sized packages, my `py-sharedmemory` implementation starts to outperform the standard implementation.
241
+ Furthermore, and this is most importantly, no I/O is produced. Also, the standard implementation crashes for 10GB packages.
242
+
243
+ 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,226 @@
1
+ # py-sharedmemory: High-Performance Multiprocessing Queue Alternative for Large Data
2
+
3
+ [![PyPI version](https://badge.fury.io/py/py-sharedmemory.svg)](https://badge.fury.io/py/py-sharedmemory)
4
+
5
+ Install using:
6
+ ```
7
+ pip install py-sharedmemory
8
+ ```
9
+
10
+ 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).
11
+
12
+ `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.
13
+
14
+ ---
15
+
16
+ # Usage example
17
+ ```python
18
+ import multiprocessing as mp
19
+
20
+ from memory import create_shared_memory_pair, SharedMemorySender, SharedMemoryReceiver
21
+
22
+ def producer_sm(sender:SharedMemorySender):
23
+ your_data = "your data"
24
+
25
+ sender.put(your_data) # blocks until there is space
26
+ # ...
27
+
28
+ # or
29
+ if sender.has_space():
30
+ sender.put(your_data)
31
+ # ...
32
+
33
+ sender.wait_for_all_ack() # wait for all data to be received before closing the sender
34
+ # ...
35
+
36
+ def consumer_sm(receiver:SharedMemoryReceiver, loops, size):
37
+ data = receiver.get() # blocks
38
+ # ...
39
+
40
+ # or
41
+ data = receiver.get(timeout=3) # raises queue.Empty exception after 3s
42
+ # ...
43
+
44
+ # or
45
+ data = receiver.get_nowait()
46
+ if data is None:
47
+ # handle empty case
48
+ # ...
49
+
50
+ if __name__ == '__main__':
51
+ sender, receiver = create_shared_memory_pair(capacity=5)
52
+
53
+ mp.Process(target=producer_sm, args=(sender,)).start()
54
+ mp.Process(target=consumer_sm, args=(receiver,)).start()
55
+ ```
56
+
57
+ ---
58
+
59
+ # Supported Types
60
+
61
+ The following data types are currently supported:
62
+ ```
63
+ - bytes
64
+ - int
65
+ - float
66
+ - bool
67
+ - str
68
+ - np.ndarray
69
+ - np.dtype
70
+ ```
71
+
72
+ As well as any nested structures of the following types:
73
+ ```
74
+ - tuple/NamedTuple
75
+ - list
76
+ - dict
77
+ - set
78
+ ```
79
+
80
+ ---
81
+
82
+ # Explanation
83
+
84
+ Let's assume I have the following data:
85
+ ```python
86
+ my_data = {
87
+ "idx": 12345,
88
+ "source": "actor_3",
89
+ "data_batch": np.zeros((64, 80, 4, 84, 84, 3), dtype=np.float32)
90
+ }
91
+ ```
92
+ 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.
93
+
94
+ 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:
95
+ ```python
96
+ (
97
+ 'wnsm_1adcf1aa', # shared memory key
98
+ 'd', 1734082569, # total byte size
99
+ <class 'dict'>, [ # data structure information
100
+ ('i', 2, False),
101
+ ('s', 7),
102
+ ('n', 1734082560, dtype('float32'), (64, 80, 4, 84, 84, 3))
103
+ ], ['idx', 'source', 'data_batch']
104
+ )
105
+ ```
106
+
107
+ <details>
108
+
109
+ <summary>Large example</summary>
110
+
111
+ Assuming this is your data object:
112
+
113
+ ```python
114
+ from typing import NamedTuple
115
+ class TestNamedTuple(NamedTuple):
116
+ name: str
117
+ value: int
118
+
119
+ extensive_test_data = {
120
+ "test_id": 98765,
121
+ "description": "This is a comprehensive test data object.",
122
+ "is_active": True,
123
+ "parameters": {
124
+ "learning_rate": 0.001,
125
+ "batch_size": 128,
126
+ "optimizer": "adam",
127
+ "epsilon": 1e-8,
128
+ "byte_config": b'\x01\x02\x03\x04'
129
+ },
130
+ "data_samples": [
131
+ {
132
+ "sample_idx": 0,
133
+ "image_data": np.random.rand(32, 32, 3).astype(np.float32),
134
+ "label": 10,
135
+ "confidence": 0.95,
136
+ "flags": [True, False, True],
137
+ "metadata": ("image", "png", 32, 32)
138
+ },
139
+ {
140
+ "sample_idx": 1,
141
+ "image_data": np.zeros((16, 16, 1), dtype=np.uint8),
142
+ "label": 5,
143
+ "confidence": 0.88,
144
+ "flags": [False, False, True],
145
+ "metadata": ("image", "jpeg", 16, 16)
146
+ }
147
+ ],
148
+ "settings_list": [
149
+ 100,
150
+ "high",
151
+ False,
152
+ 0.5,
153
+ b'setting_bytes',
154
+ (1, 'a', True),
155
+ {'nested_list_dict': [1, 2, {'deep_key': 'deep_value'}]},
156
+ np.array([1.1, 2.2, 3.3])
157
+ ],
158
+ "unique_identifiers": {101, 202, 303, 101},
159
+ "status_codes": set([b'success', b'pending', b'failed']),
160
+ "data_types_used": [
161
+ np.dtype('float32'),
162
+ np.dtype('int64'),
163
+ np.dtype('uint8'),
164
+ np.dtype('bool_')
165
+ ],
166
+ "complex_nested_structure": {
167
+ "level1_dict": {
168
+ "level2_list": [
169
+ "string_in_list",
170
+ 12345,
171
+ True,
172
+ (10, 20, 30),
173
+ {"level3_dict": {"array_in_dict": np.arange(5)}},
174
+ TestNamedTuple(name='example', value=99)
175
+ ],
176
+ "level2_set": {9, 8, 7, "set_string"},
177
+ "level2_tuple": (
178
+ "tuple_item_1",
179
+ 56.78,
180
+ b'tuple_bytes',
181
+ [1, 2, 3],
182
+ {"nested_in_tuple": "hello"}
183
+ )
184
+ }
185
+ },
186
+ "empty_structures": {
187
+ "empty_list": [],
188
+ "empty_dict": {},
189
+ "empty_set": set(),
190
+ "empty_tuple": ()
191
+ },
192
+ "large_array_example": np.random.rand(64, 80, 4, 84, 84, 3).astype(np.float32)
193
+ }
194
+ ```
195
+
196
+ Then the meta-data produced for this object is this:
197
+
198
+ `('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'])`
199
+
200
+ </details>
201
+
202
+ ---
203
+
204
+ # Considerations
205
+ There is a certain overhead to the conversion process which is especially noticable for smaller objects and more complex data structures.
206
+ Use the following heuristic depending on the size of the data you are handling:
207
+
208
+ ||10B|100B|1KB|10KB|100KB|1MB|10MB|100MB|1GB|10GB|
209
+ |---|---|---|---|---|---|---|---|---|---|---|
210
+ |``mp.Queue()``|✅|✅|✅|✅|✅|❌|❌|❌|❌|❌|
211
+ |``py-sharedmemory``|❌|❌|❌|❌|❌|✅|✅|✅|✅|✅|
212
+
213
+ Actual performance heavily relies on your memory speed.
214
+
215
+ # Performance Testing
216
+
217
+ I measured the time it takes to send/receive data comparing the standard queue and this shared memory implementation:
218
+
219
+ > 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.
220
+
221
+ ![performance comparison](https://github.com/user-attachments/assets/2a7b4f93-7698-46d5-8bb8-9d5bc7863c8f)
222
+
223
+ What we can see here is that starting at around 1MB sized packages, my `py-sharedmemory` implementation starts to outperform the standard implementation.
224
+ Furthermore, and this is most importantly, no I/O is produced. Also, the standard implementation crashes for 10GB packages.
225
+
226
+ 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,3 @@
1
+ from .factory import create_shared_memory_pair
2
+ from .sender import SharedMemorySender
3
+ from .receiver import SharedMemoryReceiver
@@ -0,0 +1,53 @@
1
+ from typing import NamedTuple
2
+ from multiprocessing.shared_memory import SharedMemory
3
+ import pickle
4
+ from pickle import PickleBuffer
5
+
6
+
7
+ class SMInfo(NamedTuple):
8
+ smh_name: str
9
+ total_bytes: int
10
+ buffer_lengths: list[int]
11
+
12
+
13
+ def data_to_smh(data: any) -> tuple[SharedMemory, SMInfo]:
14
+ buffers: list[PickleBuffer] = []
15
+ buffer_lengths: list[int] = []
16
+
17
+ def _append_buffer(buffer: PickleBuffer):
18
+ memview = buffer.raw()
19
+ buffers.append(memview)
20
+ buffer_lengths.append(len(memview))
21
+
22
+ main = pickle.dumps(data, pickle.HIGHEST_PROTOCOL, buffer_callback=_append_buffer)
23
+
24
+ len_main = len(main)
25
+ total_bytes = len_main + sum(buffer_lengths)
26
+ buffer_lengths.insert(0, len_main)
27
+ buffers.insert(0, main)
28
+
29
+ shm: SharedMemory = SharedMemory(create=True, size=total_bytes)
30
+ offset = 0
31
+ for length, buffer in zip(buffer_lengths, buffers):
32
+ shm.buf[offset : offset + length] = buffer
33
+ offset += length
34
+
35
+ info: SMInfo = SMInfo(shm.name, total_bytes, buffer_lengths)
36
+ return shm, info
37
+
38
+
39
+ def data_from_smh(info: SMInfo) -> any:
40
+ shm: SharedMemory = SharedMemory(name=info.smh_name, size=info.total_bytes)
41
+ local_buffers: bytes = bytes(shm.buf[: info.total_bytes])
42
+ shm.close()
43
+
44
+ # unpack data
45
+ buffers = []
46
+ offset = 0
47
+ for length in info.buffer_lengths:
48
+ buffer = local_buffers[offset : offset + length]
49
+ buffers.append(buffer)
50
+ offset += length
51
+
52
+ main = buffers.pop(0)
53
+ return pickle.loads(main, buffers=buffers)
@@ -0,0 +1,13 @@
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
+ data_queue = mp.Queue()
9
+ ack_queue = mp.Queue()
10
+
11
+ sender = SharedMemorySender(capacity, data_queue, ack_queue)
12
+ receiver = SharedMemoryReceiver(data_queue, ack_queue)
13
+ return sender, receiver
@@ -0,0 +1,23 @@
1
+ from multiprocessing.queues import Queue
2
+
3
+ from .convert import SMInfo, data_from_smh
4
+
5
+
6
+ class SharedMemoryReceiver:
7
+ def __init__(self, q_data_in: Queue, q_ack_out: Queue):
8
+ self.q_data_in: Queue = q_data_in
9
+ self.q_ack_out: Queue = q_ack_out
10
+
11
+ def _process_info(self, info: SMInfo):
12
+ assert info, "No info received"
13
+ data = data_from_smh(info)
14
+ self.q_ack_out.put(info.smh_name)
15
+ del info
16
+ return data
17
+
18
+ def get_nowait(self):
19
+ return self.get(block=False)
20
+
21
+ def get(self, block: bool = True, timeout: float = None):
22
+ info: SMInfo = self.q_data_in.get(block, timeout)
23
+ return self._process_info(info)