resume
▄▀█ █▀▄ █ ▀█▀ █▄█ ▄▀█
█▀█ █▄▀ █  █   █  █▀█
█▀▄▀█ █▀█ ▀█▀ ▄▀█ █   █▀▀
█ ▀ █ █▄█  █  █▀█ █▄▄ ██▄

hello, my name is Aditya — an engineer by choice

There's More to Performance Than Big-O

Sep 15, 20262 min read350 words

You are given a sorted vector of type u16 containing 0x200 (512) elements, and your task is to check whether a given element X is present in the vector.

You would probably choose binary search, as it provides the best asymptotic complexity for this problem which is O(log N).

The basic implementation is as follows,

Rust
#[inline(always)]
fn binary_search(a: &[u16], target: u16) -> bool {
    let mut lo = 0usize;
    let mut hi = a.len();

    while lo < hi {
        let mid = lo + ((hi - lo) >> 1);
        let x = unsafe { *a.get_unchecked(mid) };

        if x < target {
            lo = mid + 1;
        } else if x > target {
            hi = mid;
        } else {
            return true;
        }
    }

    false
}
Binary search implementation in Rust

After benchmarking and profiling the implementation above,

Metric Value (per search)
Latency 66 ns (± 4)
CPU cycles 174 cycles/search
Instructions 116
Branches 26.32
Branch misses 21%
IPC 0.67
Benchmarking results of binary search with a randomly selected target in a vector containing 512 elements

Searching through 512 elements in around 66 nanoseconds ain't bad. For every search, the CPU executes about 0x74 (116) instructions while taking 0xAE (174) cycles to complete. This gives an IPC of 0.67, which leaves plenty of execution capacity unused.

tip
A CPU can execute multiple instructions simultaneously in a single cycle. This is called ILP, or Instruction-Level Parallelism.

SIMD #

Using vector instructions (assuming 256-bit YMM registers), we can process 0x10 (16) elements at the same time instead of checking them one by one.

Here is a basic implementation using the AVX2 extension on x86-64,

Rust
#[target_feature(enable = "avx2")]
unsafe fn simd_search(a: &[u16], target: u16) -> bool {
    let target_vec = _mm256_set1_epi16(target as i16);
    let mut i = 0;

    while i + 0x10 <= a.len() {
        let values =
            _mm256_loadu_si256(a.as_ptr().add(i) as *const __m256i);

        let cmp = _mm256_cmpeq_epi16(values, target_vec);

        if _mm256_movemask_epi8(cmp) != 0 {
            return true;
        }

        i += 0x10;
    }

    while i < a.len() {
        if *a.get_unchecked(i) == target {
            return true;
        }

        i += 1;
    }

    false
}
Linear search implementation using the AVX2 extension on x86-64

The SIMD implementation is O(N), but each iteration checks 0x10 values in parallel. It also has a simpler control flow, allowing the CPU to execute more independent work.

After benchmarking and profiling the implementation above,

Metric Value (per search)
Latency 49 ns (± 4)
CPU cycles 80.8 cycles/search
Instructions 158
Branches 38.11
Branch misses 2.9%
IPC 1.95
Benchmarking results of the AVX2 linear search with a randomly selected target in a vector containing 0x200 elements

The same search now takes around 49 nanoseconds, with about 158 instructions executed in 80 CPU cycles. This gives an IPC of almost 2, meaning the CPU executes close to two instructions per cycle.

The SIMD implementation is therefore about 1.33 times faster than binary search for this workload.

Conclusion #

Even though we are often told to choose the algorithm with the best asymptotic complexity, Big-O only describes how an algorithm scales as the input grows. It does not describe how efficiently the CPU can execute the code.

note
Once the vector grows beyond 0x400 (1024) elements, binary search wins consistently for already-sorted workload.

In real world scenarios, branch prediction, instruction-level parallelism, SIMD, and cache locality can all affect performance and not just Big-O.

task
Open the benchmark gist to find the complete code, machine information, benchmark setup, and profiling details.
#rust#algorithms#simd
last updated on Sep 16, 2026