Object pooling and reusing large buffers reduce GC pressure by avoiding frequent allocation/deallocation of big Python objects (bytes, bytearray, large numpy arrays). For a JSON-to-numpy ingestion pipeline this matters: repeatedly creating 10s–100s MB objects triggers the allocator and GC, causing pauses and memory churn. Reuse keeps large buffers alive and repopulated, reducing allocator work and fragmentation.Concrete techniques and libraries- Pre-allocate numpy arrays and overwrite them in-place instead of allocating new arrays per message (use numpy.resize/[:n] or create fixed-shape arrays and fill slices).- Use bytearray or memoryview for parsing raw JSON bytes so you can refill buffers without reallocating.- Use mmap for very large datasets or files to avoid copying into Python heaps.- Consider PyArrow for zero-copy parsing/serialization and Feather/IPC formats; its buffers are C-owned and reduce Python-level churn.- Use multiprocessing.shared_memory (Python 3.8+) to share large buffers across processes without copies.- Use a simple object pool (queue of ready buffers) with a max pool size to bound memory.Example pool pattern (python):python
import queue
import numpy as np
class NumpyPool:
def __init__(self, shape, dtype, maxsize=10):
self.pool = queue.LifoQueue(maxsize)
for _ in range(maxsize):
self.pool.put(np.empty(shape, dtype=dtype))
def acquire(self, timeout=None):
return self.pool.get(timeout=timeout)
def release(self, arr):
# optionally zero or validate before returning
self.pool.put(arr)
# usage
pool = NumpyPool((1000000,), np.float32, maxsize=5)
arr = pool.acquire()
# parse JSON values into arr[:n] in-place
# ... processing ...
pool.release(arr)
Language-specific tips- Minimize creating many short-lived Python objects. Parse JSON with ijson or yajl-bindings (yajl/ijson) to stream tokens into preallocated buffers.- Use ujson/rapidjson for faster parsing when you still need Python objects, but prefer streaming/iterative parsing into arrays.- Manage references explicitly: del large objects when done and avoid lingering references (caches, closures).- Tune GC: disable generational GC temporarily during tight loops (gc.disable()/enable()) if you ensure no cyclic refs; run gc.collect() during low-load times.- For C-backed buffers (numpy, pyarrow), rely on their allocator — these produce fewer Python-level allocations.Monitoring and trade-offs- Measure with tracemalloc, objgraph, and psutil. Pooling trades slightly higher steady memory for lower CPU/GC overhead. Ensure pool size matches throughput and available memory to avoid OOM.This combination—preallocation, streaming parsing, C-backed zero-copy buffers, and a bounded object pool—significantly reduces GC pressure and memory churn in high-throughput ingestion pipelines.