C programming
search algorithm
algorithm optimization
coding efficiency
software development

Optimizing a search algorithm in C

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Search algorithms are among the most frequently executed routines in any codebase, so even small improvements can yield measurable gains at scale. C gives you direct control over memory layout, pointer arithmetic, and compiler intrinsics, making it an ideal language for squeezing every cycle out of a search routine. This article walks through four levels of optimization: sentinel linear search, binary search with branch reduction, hash-table lookups, and SIMD-accelerated scanning.

Linear Search with Sentinel

A naive linear search checks two conditions on every iteration: whether the index is in bounds and whether the current element matches the target. The sentinel technique eliminates the bounds check by placing the target value at the end of the array before searching.

c
1#include <string.h>
2
3int sentinel_search(int arr[], int n, int target) {
4    int last = arr[n - 1];
5    arr[n - 1] = target; // place sentinel
6
7    int i = 0;
8    while (arr[i] != target)
9        i++;
10
11    arr[n - 1] = last; // restore original value
12
13    if (i < n - 1 || arr[n - 1] == target)
14        return i;
15    return -1;
16}

The loop body now contains a single comparison instead of two. On large unsorted arrays this can cut wall-clock time by roughly 20-30 percent because the branch predictor only has to deal with one conditional branch per iteration. The time complexity remains O(n), but the constant factor shrinks.

Binary Search with Reduced Branching

Standard binary search uses a three-way comparison (less, equal, greater) on each iteration. A branchless variant collapses this to a single conditional move, which modern CPUs handle without a pipeline stall.

c
1int binary_search_branchless(const int arr[], int n, int target) {
2    int lo = 0;
3    int len = n;
4
5    while (len > 1) {
6        int half = len / 2;
7        // conditional move: no branch misprediction
8        lo += (arr[lo + half] < target) ? half : 0;
9        len -= half;
10    }
11
12    return arr[lo] == target ? lo : -1;
13}

This version keeps the O(log n) complexity of binary search while eliminating branch mispredictions. On a sorted array of one million integers, the branchless version typically outperforms the standard if-else binary search by 15-40 percent depending on the CPU architecture. The key insight is that replacing an unpredictable branch with a conditional move lets the pipeline proceed without stalling.

When you need constant-time lookups and can afford the memory, a hash table is the right choice. The following example uses open addressing with linear probing.

c
1#include <stdlib.h>
2#include <string.h>
3
4#define TABLE_SIZE 1024
5#define EMPTY -1
6
7typedef struct {
8    int keys[TABLE_SIZE];
9    int values[TABLE_SIZE];
10} HashTable;
11
12void ht_init(HashTable *ht) {
13    memset(ht->keys, EMPTY, sizeof(ht->keys));
14}
15
16static unsigned int hash(int key) {
17    return (unsigned int)key % TABLE_SIZE;
18}
19
20void ht_insert(HashTable *ht, int key, int value) {
21    unsigned int idx = hash(key);
22    while (ht->keys[idx] != EMPTY && ht->keys[idx] != key)
23        idx = (idx + 1) % TABLE_SIZE;
24    ht->keys[idx] = key;
25    ht->values[idx] = value;
26}
27
28int ht_search(const HashTable *ht, int key) {
29    unsigned int idx = hash(key);
30    while (ht->keys[idx] != EMPTY) {
31        if (ht->keys[idx] == key)
32            return ht->values[idx];
33        idx = (idx + 1) % TABLE_SIZE;
34    }
35    return -1; // not found
36}

Average-case lookup is O(1), but this degrades to O(n) if the load factor climbs too high. Keep the load factor below 0.7 by resizing the table when occupancy crosses that threshold. Choosing a power-of-two table size lets you replace the modulo operation with a bitmask (idx & (TABLE_SIZE - 1)), which is faster on most hardware.

SIMD-Accelerated Linear Scan

When the data is unsorted and too large for a hash table, SSE or AVX intrinsics let you compare multiple elements in a single instruction. The example below uses 128-bit SSE2 to check four integers at once.

c
1#include <emmintrin.h> // SSE2
2
3int simd_search(const int arr[], int n, int target) {
4    __m128i vtarget = _mm_set1_epi32(target);
5    int i = 0;
6
7    for (; i + 4 <= n; i += 4) {
8        __m128i vdata = _mm_loadu_si128((__m128i *)&arr[i]);
9        __m128i vcmp  = _mm_cmpeq_epi32(vdata, vtarget);
10        int mask = _mm_movemask_epi8(vcmp);
11        if (mask != 0) {
12            // find which of the 4 elements matched
13            return i + (__builtin_ctz(mask) / 4);
14        }
15    }
16    // handle remaining elements
17    for (; i < n; i++) {
18        if (arr[i] == target) return i;
19    }
20    return -1;
21}

This processes four elements per loop iteration, delivering close to a 4x throughput improvement on cache-resident data. With AVX2 you can widen to eight elements at once using __m256i and _mm256_cmpeq_epi32. Ensure the array is 16-byte aligned (or 32-byte for AVX) to use the faster aligned load intrinsics.

Time Complexity Comparison

MethodAverage CaseWorst CaseRequirement
Sentinel linear searchO(n)O(n)None
Branchless binary searchO(log n)O(log n)Sorted array
Hash table lookupO(1)O(n)Hash table built
SIMD linear scanO(n/k)O(n/k)SIMD support (k = lane width)

Common Pitfalls

  • Forgetting to restore the sentinel value. If you leave the sentinel in place, subsequent operations on the array will see corrupted data at the last index.
  • Using binary search on unsorted data. Binary search produces undefined results when the input is not sorted; always verify sort order or sort first.
  • Ignoring hash table load factor. Letting occupancy exceed 70-80 percent causes clustering and dramatically increases probe lengths.
  • Assuming SIMD alignment. Using _mm_load_si128 on an unaligned pointer triggers a segfault; use _mm_loadu_si128 for unaligned data or align your allocations with aligned_alloc.
  • Optimizing before profiling. Measure with real workloads before choosing a strategy; a branchless binary search on a 20-element array is slower than a simple loop due to setup overhead.

Summary

  • Sentinel linear search reduces per-iteration branches and speeds up unsorted scans by 20-30 percent.
  • Branchless binary search eliminates pipeline stalls from mispredicted branches on sorted data.
  • Hash tables deliver O(1) average lookups when you can trade memory for speed; keep load factor below 0.7.
  • SIMD intrinsics (SSE2/AVX2) process multiple elements per cycle, giving near-linear speedups on cache-resident arrays.
  • Always profile with realistic data before committing to an optimization strategy, because algorithmic improvements outweigh micro-optimizations on small inputs.

Course illustration
Course illustration

All Rights Reserved.