Performance Trade-offs & Optimization Strategy Questions
Deciding what to optimize, how far, and at what cost to other qualities. Covers performance vs readability/reliability/cost trade-offs, prioritizing the optimization with the highest payoff, knowing when a system is fast enough, and sequencing optimization work. Emphasizes optimization as a strategic engineering judgment rather than a reflex.
EasyTechnical
90 practiced
Implement an LRU cache in Python with O(1) get and put operations. Provide a class with methods: get(key) -> value or -1, and put(key, value). The cache must support a maximum capacity and evict the least recently used item when full. Use standard Python (no external libs).
Sample Answer
Use an ordered doubly-linked list plus a hash map (dict) to achieve O(1) get and put: dict maps keys to nodes in the linked list; the list maintains usage order (most-recent at head, least-recent at tail). On get or put (update), move the node to head. When capacity exceeded, remove tail node and delete its key from the dict.Key points:- Doubly-linked list supports O(1) removal and insertion when given a node.- Dict provides O(1) access to nodes by key.Time complexity: get and put are O(1) average. Space: O(capacity).Edge cases:- capacity <= 0: decide behavior (above code will evict immediately; in production raise ValueError).- updating existing key vs inserting new key handled correctly.- For ML production: this pattern is useful for caching feature lookups, preprocessed tensors, or model inference results to reduce latency.
python
class Node:
def __init__(self, key, val):
self.key = key
self.val = val
self.prev = None
self.next = None
class LRUCache:
def __init__(self, capacity: int):
self.capacity = capacity
self.cache = {} # key -> Node
# Dummy head and tail to avoid edge checks
self.head = Node(None, None) # most recently used after head
self.tail = Node(None, None) # least recently used before tail
self.head.next = self.tail
self.tail.prev = self.head
# Helper: remove node from list
def _remove(self, node: Node):
prev, nxt = node.prev, node.next
prev.next = nxt
nxt.prev = prev
# Helper: insert node right after head (most recent)
def _add_to_head(self, node: Node):
node.next = self.head.next
node.prev = self.head
self.head.next.prev = node
self.head.next = node
def get(self, key: int) -> int:
node = self.cache.get(key)
if not node:
return -1
# mark as recently used
self._remove(node)
self._add_to_head(node)
return node.val
def put(self, key: int, value: int) -> None:
node = self.cache.get(key)
if node:
# update value and mark recent
node.val = value
self._remove(node)
self._add_to_head(node)
else:
new_node = Node(key, value)
self.cache[key] = new_node
self._add_to_head(new_node)
if len(self.cache) > self.capacity:
# evict least recently used (tail.prev)
lru = self.tail.prev
self._remove(lru)
del self.cache[lru.key]HardTechnical
60 practiced
Discuss the performance implications of encrypting model inputs and outputs (TLS vs application-level encryption) for a low-latency inference API. Include CPU overhead, latency, key management, and when server-side TLS is sufficient versus needing payload-level encryption.
Sample Answer
Requirements and constraints:- Low-latency inference API (target p99 latency budget e.g., <50–100 ms).- Sensitive inputs/outputs (PII, PHI, IP) may require stronger controls.- High QPS and small payloads (typical for inference).High-level tradeoff summary:- Server-side TLS secures transport (confidentiality/integrity in transit) with minimal app complexity and hardware-accelerated crypto (AES-NI). For most use cases TLS alone is sufficient and lowest-latency: modern TLS 1.3 with session resumption and HPKE-like KEX yields a single-Round-Trip handshake and negligible per-request overhead once sessions established.- Application-level (payload) encryption (e.g., encrypting JSON body with AES-GCM or public-key envelope) adds CPU work per request, extra serialization, and key management complexity, increasing latency—especially for small, frequent requests where fixed per-request costs dominate.CPU overhead & latency:- TLS: once handshake/resumption done, symmetric crypto per-record is hardware-accelerated. Typical per-request crypto cost is microseconds to low hundreds of microseconds for small payloads. Handshakes cost ~1–10 ms (first connection) but are amortized via keep-alive, connection pools.- Payload-level symmetric encryption: AES-GCM encrypt/decrypt of ~1 KB costs ~10–100 µs on modern CPUs (with AES-NI). But added marshalling, nonce handling, authentication tag checks, and often extra I/O (e.g., separate crypto libraries, per-request key derivation) push per-request latency up—practically adding 0.1–2 ms depending on implementation, language, and payload size. As payloads grow, bandwidth and CPU scale linearly.- Asymmetric payload encryption (per-request public-key ops) is expensive: RSA/ECDH per message is milliseconds and generally unacceptable for low-latency per-request encryption unless used only for envelope key exchange.Key management:- TLS keys handled by the TLS stack and can leverage hardware (HSM, cloud KMS-backed TLS terminators). Application-level encryption requires you manage encryption keys, rotation, access policies, and secrets in KMS/HSM; you must also design secure envelope encryption (generate ephemeral symmetric keys per request, encrypt them with KMS/ECDH) to avoid frequent expensive asymmetric ops.- Key rotation, auditing, and multi-tenant key separation add complexity and potential latency if KMS calls are on the critical path. Cache decrypted keys in secure process memory or use locally cached envelope keys with TTLs to reduce KMS latency.When server-side TLS is sufficient:- Threat model: adversary is in transit (network eavesdropper). No requirement for end-to-end encryption beyond network boundary.- Deployments where inference servers and downstream systems are trusted (within VPC, zero-trust border controls, or using mTLS inside cluster) and compliance requirements accept TLS.- When low latency is critical and payloads are small and non-extremely sensitive.When payload-level encryption is needed:- Threat model includes compromised server, cloud provider, or insider requiring that server process cannot see plaintext (confidential compute / client-side encryption).- Regulatory or contractual requirements demanding end-to-end encryption or customer-managed keys (CMKs) where the server must not hold decryption keys.- Multi-tenant inference where outputs must remain isolated even from host operators.Practical recommendations:- Start with modern TLS 1.3 + mTLS for mutual auth and use connection pooling, keep-alives, and session resumption to minimize handshake cost.- Profile real traffic: measure crypto cost per request, CPU utilization, and p99 latency with and without payload encryption.- If payload encryption required, use envelope encryption: client generates ephemeral symmetric key, encrypts payload with AES-GCM, encrypts symmetric key with server public key or KMS-wrapped key. Cache ephemeral/decrypted keys securely to avoid per-request KMS latency.- Offload crypto to hardware (AES-NI, dedicated crypto instances) or sidecars (trusted proxies or secure enclaves) to reduce app CPU impact.- Document key-rotation and incident response plans; use HSM/KMS for root keys and minimize KMS calls on hot path.Bottom line: use server-side TLS by default for best latency/cost tradeoff; adopt payload-level encryption only when threat model or compliance requires that inference servers cannot access plaintext — then design with envelope encryption, hardware acceleration, and aggressive caching to meet low-latency constraints.
HardTechnical
70 practiced
You run a benchmark: throughput is much lower than expected, but both CPU and GPU utilization are low. Network metrics show high RPC latency and serialization time. Outline a diagnostic approach to pinpoint the bottleneck (network vs serialization vs RPC framework) and propose optimizations with trade-offs.
Sample Answer
Start by clarifying what “high RPC latency and serialization time” means in your traces/metrics (p50/p95/p99, per-call breakdown). Then follow a systematic triangulation: isolate components, measure, and iterate.Diagnostic steps1. Reproduce with controlled microbenchmarks - Single-request latency test and synthetic throughput test (no model inference): isolate RPC+serialization+network overhead.2. Measure component latencies end-to-end and in-process - Instrument wall-clock: client serialize -> network send -> server receive -> server deserialize -> model inference -> server serialize -> network -> client deserialize. - Record per-step durations and p50/p95/p99.3. Eliminate variables - Local loopback test (client and server on same host or use unix sockets) to remove network — if latency drops, network is significant. - Bypass serialization by sending raw bytes or pre-serialized payloads — if latency drops, serialization is culprit. - Replace RPC framework with a minimal TCP/HTTP echo server to test framework overhead.4. Profile serialization - CPU flamegraphs during serialize/deserialize; check allocations and copies. - Test alternative serializers (protobuf vs flatbuffers vs msgpack vs raw numpy/tfrecords) on representative payloads.5. Network profiling - Packet captures (tcpdump) to see fragmentation, retransmits, MTU issues. - Measure bandwidth vs throughput; check congestion, NIC offload, and kernel/network buffers.6. RPC framework checks - Inspect thread pools, connection pooling, batching behavior, context propagation (auth/metadata) overhead, TLS costs. - Test different serialization-integration modes (zero-copy, memory-mapped).Optimizations & trade-offs- Reduce serialization cost - Use zero-copy formats (FlatBuffers/Cap’n Proto) or send raw tensors (memory-mapped or shared memory). Trade-off: more brittle schema, less tooling, safety concerns. - Compress payloads (lz4/zstd) for large tensors to reduce bandwidth. Trade-off: CPU for compression; beneficial when network is bottleneck.- Reduce RPC framework overhead - Use lighter protocols (gRPC with proto vs custom binary) or enable gRPC keepalive/connection pooling and HTTP/2 multiplexing. Trade-off: engineering work, potential security implications. - Batch requests/async pipelines to amortize per-call overhead. Trade-off: increased latency for individual requests.- Network-level fixes - Increase MTU, enable TCP segmentation offload, tune socket buffers, use RDMA or faster network (10G/100G) or colocate services. Trade-off: infra cost, ops complexity.- Architectural changes - Move inference closer to data (edge or co-location), use model sharding, or adopt streaming RPCs to send chunks. Trade-off: deployment complexity and consistency.- Hybrid: send lightweight metadata to decide if full payload needed (early reject) or employ caching.What to report- Show before/after p50/p95/p99, CPU/GPU utilization, network throughput, and resource cost. Recommend incremental changes: first microbench and zero-copy serialization, then batching and network tuning, then protocol or infra upgrades.
EasyTechnical
60 practiced
You're given a Python inference microservice that is CPU-bound. Describe three low-effort profiling steps you would run to confirm the bottleneck and one small code change you might try that could give a noticeable improvement with minimal risk.
Sample Answer
1) Quick system check (confirm CPU-bound): run top/htop or ps aux --sort=-%cpu while sending typical requests to the service. Verify CPU usage spikes (not I/O, not network). Check load average and per-core saturation.2) Lightweight sampling profiler: run py-spy top --pid <pid> or py-spy record --flame <pid> while the service is busy. Sampling gives immediate function-level hotspots without restarting or adding overhead.3) Short deterministic profiling run: instrument a representative inference call with cProfile (or line_profiler for line-level) for a few requests:This confirms exact hotspots and time spent in Python code vs extension libs.Small, low-risk code change: add memoization to any pure/ deterministic preprocessing or feature-extraction function using functools.lru_cache. Example:Why: avoids repeated expensive Python work for identical inputs (common in batching, repeated requests, or cached client behavior). Effort: minimal change, easy to revert. Impact: often orders-of-magnitude reduction in CPU for that path. Risk: increased memory use — mitigate with bounded maxsize and cache invalidation.
bash
python -m cProfile -o profile.out serve_inference.py
snakeviz profile.outpython
from functools import lru_cache
@lru_cache(maxsize=10000)
def preprocess_key(tokenized_input_tuple):
# convert mutable input to tuple key, do deterministic work
...HardSystem Design
71 practiced
You are designing a model caching strategy across microservices where models are large and updated frequently. Propose a deployment pattern to reduce update propagation latency while minimizing memory overhead (e.g., shared memory, memory-mapped files, lazy-loading). Discuss trade-offs and safety concerns.
Sample Answer
Requirements & constraints:- Large models (hundreds of MB–GB) deployed to many microservices- Frequent model updates (minutes–hours)- Low update propagation latency (ideally seconds)- Minimize per-process memory overhead and cold-start latency- Safety: consistency, rollback, A/B testing, no corruptionHigh-level deployment pattern — Shared memory + memory-mapped files with lazy loading and versioned atomic switch:1. Central model store (artifact registry / object storage) + model manager service that publishes version metadata and checksum.2. Node-level model agent (daemon) per host: - Downloads new model artifact into a host-local model cache directory as versioned files. - Writes file to disk atomically (temp -> rename) and publishes readiness to manager.3. Serve processes (microservices) use memory-mapped files (mmap) to load model tensors lazily: - mmap same versioned file read-only so OS shares pages across processes — huge memory savings. - Lazy page-fault-backed access reduces cold-start time; optionally prefetch hot layers.4. Atomic version switch via symlink or IPC: - Agent updates a version pointer (atomic rename of symlink or updating shared memory control block). - Processes detect new version (inotify, health endpoint polling, or SIGUSR) and switch: memory-map new file then drop old mapping.5. Safety and rollout: - Staged rollout: blue-green or canary. Agent marks new version “canary” and only promotes after health checks. - Validate checksum and signature before publish. - Keep last N versions for immediate rollback. - Use read-only mounts and file permissions to avoid corruption.Trade-offs:- Pros: Low memory overhead (OS-level page sharing), fast propagation (atomic switch), per-host bandwidth optimization.- Cons: More complex infra (agent, versioning), OS-dependent behavior (mmap semantics), potential latency on first access due to page faults, GC of GPU memory if models also loaded to GPU needs careful coordination.- Consistency: If models have internal state or runtime caches, ensure model code is stateless or provides transactional reload hooks.Implementation notes:- For GPU models, map CPU-side model file and implement coordinated GPU buffer swap: pre-load weights into temp GPU buffers then atomically swap pointers.- Instrument metrics: page-fault rates, switch latency, memory savings, canary error rate.- Consider alternative: shared model server (gRPC) that serves in-memory models centrally — simpler consistency but higher RPC latency and single point of scale.This pattern balances low-latency propagation, minimal per-process memory duplication, and safety via atomic switches, validation, and staged rollouts.
Unlock Full Question Bank
Get access to hundreds of Performance Trade-offs & Optimization Strategy interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.