Skip to main content

Pipes vs. Shared Memory: Architectural & Performance Tradeoffs

Medium

Interview Question: "What is the fundamental architectural tradeoff between Unix pipes and shared memory for Inter-Process Communication (IPC)? When would you choose one over the other?"

Two processes are isolated from each other by design — that's the whole point of a process. IPC (inter-process communication) is how you deliberately punch a hole through that isolation. Pipes and shared memory are opposite answers to "how much do you trust the kernel to manage that hole for you?"


The analogy: the postman vs. the shared whiteboard​

Two coworkers in locked, adjacent offices need to collaborate:

  • Pipes — the postal courier. Worker A writes a note, hands it to a courier (the kernel), who walks it over and hands it to Worker B. Orderly and safe — nothing gets lost or mixed up, and if Worker B's inbox is full, the courier just makes Worker A wait. But every single note costs a courier trip.
  • Shared memory — the glass whiteboard. The crew knocks a hole in the wall and installs a whiteboard both offices can see and write on. Worker A writes, Worker B sees it instantly — no courier, no delay. But if both grab a marker and write over each other's work at the same time, the result is garbage. There's no courier to stop them; they have to agree on a system themselves (locks).
Pipes
kernel copies data
safe, but 2 copies + a syscall per transfer
Shared memory
direct shared RAM
zero copies, but you handle locking

Pipes: the kernel does everything, including the waiting​

A pipe is a one-way byte stream, fully managed by the kernel.

  • A ring buffer in the kernel. On Linux, it's typically 16 pages (64KB), adjustable with fcntl(fd, F_SETPIPE_SZ).
  • Two copies per transfer. Data goes app → kernel buffer (copy_from_user) → other app (copy_to_user). You never touch the other process's memory directly.
  • The kernel handles blocking for you. Read from an empty pipe and you sleep until data shows up. Write to a full one and you sleep until the reader catches up. Close all write ends and the reader gets EOF. Write to a closed read end and you get SIGPIPE.

One subtlety worth knowing: writes up to PIPE_BUF bytes (4096 on Linux) are atomic — two processes writing ≤4KB each to the same pipe will never have their bytes interleaved. Go over that limit and the kernel is free to interleave chunks from concurrent writers, which can corrupt a structured message.


Shared memory: maximum speed, zero built-in safety​

Shared memory (shm_open() + mmap(), or System V shmget()) is the fastest IPC there is, because it isn't really "communication" at the OS level at all — it's just two processes pointing at the same physical RAM.

  • Zero copies. The OS sets up both processes' page tables so their virtual addresses map to the exact same physical frames. After that one-time setup, reads and writes are plain memory access — no syscalls, no copying, bus-speed throughput (>30 GB/s).
  • The kernel steps out of the data path entirely. That's the speed, and also the danger: if Process A writes while Process B is reading the same spot, you get silent corruption. There's no automatic blocking, no automatic ordering — you have to build that yourself, typically with a process-shared mutex (pthread_mutexattr_setpshared), a POSIX semaphore, or a Linux futex.

Comparison​

DimensionPipes (pipe(), mkfifo)Shared Memory (mmap(), shm_open())
Data pathOne-way byte stream through a kernel bufferDirect, random-access shared RAM
Memory copies2 (user → kernel → user)0
Syscalls per transferOne per read()/write()None — only during setup
SynchronizationAutomatic, handled by the kernelManual — you add the locks
ThroughputLimited by syscall + copy overheadLimited only by RAM bus speed
If the writer crashesReader just gets a clean EOFA lock held inside the crash stays locked forever
Good forShell pipelines (ls | grep), simple streamingVideo pipelines, high-frequency trading, shared ML models

Say this in the interview​

"The tradeoff is kernel overhead versus synchronization complexity. Pipes copy data through a kernel buffer twice and cost a syscall per transfer, but the kernel handles blocking and ordering for you automatically. Shared memory maps the same physical RAM into both processes for true zero-copy, bus-speed access, but the kernel is completely out of the loop on synchronization — you're responsible for locks, and a crash mid-write can leave a lock stuck forever."


Code: measuring the throughput gap​

"""
IPC Performance Benchmark: Pipes vs Shared Memory
Demonstrates:
1. Pipe transfer latency and throughput (double memory copy)
2. Shared Memory zero-copy latency and throughput
"""

import time
import multiprocessing
from multiprocessing import shared_memory
import numpy as np

PAYLOAD_SIZE = 10 * 1024 * 1024 # 10 MB payload
CYCLES = 10

def pipe_sender(conn, data: bytes):
for _ in range(CYCLES):
conn.send_bytes(data)

def pipe_receiver(conn):
for _ in range(CYCLES):
_ = conn.recv_bytes()


def benchmark_pipes():
data = b"X" * PAYLOAD_SIZE
parent_conn, child_conn = multiprocessing.Pipe()

p = multiprocessing.Process(target=pipe_sender, args=(child_conn, data))
start = time.perf_counter()
p.start()
pipe_receiver(parent_conn)
p.join()
elapsed = time.perf_counter() - start

mb_transferred = (PAYLOAD_SIZE * CYCLES) / (1024 * 1024)
throughput = mb_transferred / elapsed
return elapsed, throughput


def shm_worker(shm_name: str, size: int, lock, event_written, event_read):
existing_shm = shared_memory.SharedMemory(name=shm_name)
buf = existing_shm.buf
for _ in range(CYCLES):
event_written.wait()
event_written.clear()
# Direct zero-copy read from shared buffer
_ = bytes(buf[:100]) # Inspect partial content
event_read.set()
existing_shm.close()


def benchmark_shared_memory():
# Allocate 10MB of shared physical RAM
shm = shared_memory.SharedMemory(create=True, size=PAYLOAD_SIZE)
lock = multiprocessing.Lock()
event_written = multiprocessing.Event()
event_read = multiprocessing.Event()

p = multiprocessing.Process(
target=shm_worker,
args=(shm.name, PAYLOAD_SIZE, lock, event_written, event_read)
)
p.start()

raw_data = b"Y" * PAYLOAD_SIZE
start = time.perf_counter()
for _ in range(CYCLES):
# Direct memory write into shared physical frame
shm.buf[:PAYLOAD_SIZE] = raw_data
event_written.set()
event_read.wait()
event_read.clear()

elapsed = time.perf_counter() - start
p.join()
shm.close()
shm.unlink()

mb_transferred = (PAYLOAD_SIZE * CYCLES) / (1024 * 1024)
throughput = mb_transferred / elapsed
return elapsed, throughput


def main():
print("=== IPC Performance Benchmark: Pipes vs Shared Memory ===")
print(f"Transferring {PAYLOAD_SIZE / (1024 * 1024):.1f} MB payload across {CYCLES} iterations...\n")

pipe_time, pipe_tput = benchmark_pipes()
print(f"Pipes (Kernel Double-Copy):")
print(f" Total Time: {pipe_time:.3f} s | Throughput: {pipe_tput:6.1f} MB/s")

shm_time, shm_tput = benchmark_shared_memory()
print(f"\nShared Memory (Zero-Copy Direct RAM):")
print(f" Total Time: {shm_time:.3f} s | Throughput: {shm_tput:6.1f} MB/s")

speedup = shm_tput / pipe_tput if pipe_tput > 0 else 1.0
print(f"\nResult: Shared Memory achieved ~{speedup:.1f}x higher throughput than Pipes.")
print("Direct cause: Avoided kernel user-space buffer copies and per-read/write system calls.")

if __name__ == "__main__":
main()