Course contentsShow
← C#
Lesson 443 of 1,9084. Functional Programming with LINQFree lesson

Thread Affinity and Data Locality

Design query pipelines that respect data locality and reduce cross-thread communication overhead.

In this lesson, you will learn how to align your query execution with physical CPU hardware to minimize data movement and thread migration costs.

The Problem: When Data Travels

In high-performance LINQ pipelines, we often assume that as long as a query executes, it doesn't matter where it happens. However, modern CPUs rely heavily on L1/L2 caches. If a thread processing a sequence is migrated by the OS from one CPU core to another, the data it was just working on stays behind in the previous core's cache. The new core must then fetch that data from main memory, which is orders of magnitude slower. This is a "cache miss" caused by poor data locality.

Designing for Affinity

Thread affinity is the practice of keeping specific tasks bound to specific CPU cores. When designing query pipelines, you want your data to stay "warm" in the cache. You can reduce cross-thread overhead by ensuring that the thread that fetches the data is the same thread that processes the projection:

// Instead of dispatching fragments to random thread-pool threads,
// pin the processing to the local thread that fetched the segment.
var localResult = source
    .Where(item => item.IsValid)
    .Select(ProcessLocally) // Keeps data in L1/L2 cache
    .ToList();

By keeping your Select or Aggregate operations sequential and focused on the thread that initiated the data access, you minimize the "ping-pong" effect where data is serialized and passed across different processor caches.

Reducing Communication Overhead

Cross-thread communication—such as moving objects through ConcurrentQueue or waiting on thread-safe synchronization primitives—is expensive. When designing your LINQ pipelines, strive to complete as much work as possible within the local processing context. If you must use parallel processing, partition your data into chunks that fit entirely within a single core's cache (using Partitioner.Create if available, or manual range splitting) to ensure that once a thread grabs a chunk, it processes the entire pipeline for that chunk before returning to the pool.

Key Takeaway: Design your query pipelines to perform operations on the same thread that owns the data in local cache, effectively treating the CPU cache as a high-speed workspace for your LINQ projections.