Skip to main content

CPU Thread Affinity

This page explains how CPU thread affinity currently works in hlquery.

What Affinity Means

CPU thread affinity pins a thread to one or more CPU cores so the scheduler keeps that thread on a specific core set. In hlquery this is used to reduce cache misses and keep high-throughput worker paths stable.

Where It Is Used

Affinity is used in two places:

  1. Search thread pools (SearchThreadPool) in src/common/SearchThreadPool.cpp
  2. Socket I/O worker threads (SocketEngine::IOWorkerThread) in src/socketengines/epoll.cpp

Pool-by-Pool Core Selection

CPUAffinityManager::GetOptimalCoresForPool() chooses cores by pool type:

  • HTTP_REQUEST: first half of available CPU cores
  • SEARCH: second half of available CPU cores
  • WRITE: core 0 and optionally core 1
  • MANAGEMENT: core 0 (management pool affinity is disabled by default)

This logic is currently simple and based on std::thread::hardware_concurrency().

Thread Pool Behavior

When pools are initialized, affinity is enabled for:

  • HTTP pool
  • Search pool
  • Write pool

Affinity is disabled for:

  • Management pool

Each pool stores a vector of candidate cores via SetCPUAffinity(...). Worker threads are then assigned cores in round-robin order:

cpu_core = CPUCores[i % CPUCores.size()]

The same round-robin rule is used when scaling up pool threads.

Linux-Only Details

Affinity calls are compiled only on Linux (#if defined(__linux__)), using:

  • pthread_setaffinity_np(...) for thread affinity
  • sched_setaffinity(...) for process affinity (available in CPUAffinityManager)

On non-Linux platforms, affinity calls are no-ops.

I/O Worker Behavior

SocketEngine::IOWorkerThread() sets affinity inside the worker thread itself:

CPU_SET(WorkerID, &cpuset); pthread_setaffinity_np(pthread_self(), ...)

Because this is called from the worker thread context, the pinned thread is unambiguous.

Current Caveat In Search Thread Pools

In SearchThreadPool::SetThreadAffinity(...) and CPUAffinityManager::SetThreadAffinity(...), the thread_id parameter is currently ignored and pthread_self() is used.

That means affinity is applied to the calling thread, not explicitly to the target std::thread::id. Since pool affinity is applied from management/startup code after thread creation, behavior may not always match the intended per-worker pinning.

In short:

  • Intent: pin each pool worker to its assigned core
  • Current implementation detail: uses caller thread handle (pthread_self())

NUMA Notes

NUMA support is currently basic:

  • Topology detection assumes a single socket and one NUMA node
  • EnableNUMAOptimization() is a placeholder switch
  • OptimizeForNUMA() distributes threads using the detected topology model

For multi-socket or complex NUMA hosts, treat current behavior as best-effort rather than strict placement.