Welcome to the final lesson in our module on high-performance vector math. So far, you have built two crucial, high-performance components: a SIMD-accelerated dot product and a similarly optimized function for the squared L2 norm. You've mastered Fused Multiply-Add (FMA) instructions and loop unrolling, recognizing that the L2 norm calculation is simply a dot product of a vector with itself.
Today, we will assemble these pieces to achieve the primary goal of this module: creating a fully SIMD-accelerated function for Cosine Similarity. This will involve not just combining our previous work, but also learning a critical optimization strategy that distinguishes a good implementation from a great one by minimizing memory access and maximizing computational density.
The Cosine Similarity Formula
Let's begin with the definition of Cosine Similarity. For two non-zero vectors and , it is the cosine of the angle between them. Mathematically, it's defined as their dot product divided by the product of their magnitudes (their L2 norms):

As you can see, the formula requires three distinct sums:
- The dot product of and ().
- The sum of squares of 's elements (), which is the squared L2 norm of .
- The sum of squares of 's elements (), the squared L2 norm of .
A straightforward way to implement this would be to call your existing dot_product and l2_norm_squared functions three times. While simple and modular, this "three-pass" approach is inefficient. It would require reading the vector data from memory three separate times, which can quickly become a bottleneck, especially for large vectors that don't fit in the CPU cache.
For high-performance systems, a much better strategy is the single-pass approach.
The Superiority of the Single-Pass Method
The core principle of the single-pass method is to calculate all three required sums simultaneously within a single loop. By loading chunks of vectors and just once, we can perform all the necessary multiplications and additions while the data is hot in the CPU registers. This drastically reduces the pressure on memory bandwidth and improves cache utilization.
The following blog post from SereneDB explains this exact trade-off.
This article compares the performance of Cosine distance implementations. We are interested in its analysis of the single-pass versus multi-pass strategy.
Read the section titled one loop vs. three. Pay close attention to how it contrasts the SereneDB approach (one pass) with the Faiss approach (three passes). The key takeaway is that reading the data once and updating all accumulators together is significantly more efficient.
This single-pass strategy is the standard for high-performance vector libraries and is exactly what we will implement.
Implementing the Single-Pass Cosine Similarity
To implement the single-pass method, we will integrate the logic from our previous two lessons into a single function. Instead of one set of accumulators for the dot product, we will now maintain three sets: one for the dot product (), one for the squared norm of A (), and one for the squared norm of B ().

The following resource provides a C implementation that perfectly illustrates this technique.
Understanding SIMD: Infinite Complexity of Trivial Problems
This article from the author of the SimSIMD library provides a detailed walkthrough of an AVX2-accelerated Cosine Similarity function. It demonstrates the single-pass technique and other advanced optimizations. Note that the code uses bfloat16, but the logic for float32 is identical; just ignore the data type conversion parts.
Focus on the C code block under the heading Baseline implementation. Observe how it initializes three __m256 accumulator variables: ab_vec, a2_vec, and b2_vec. Inside the loop, after loading a_vec and b_vec, it performs three separate FMA operations—one for each accumulator. This is the core of the single-pass optimization.
The structure of your function will be:
-
Initialization: Create three
__m256registers,acc_ab,acc_a2, andacc_b2, and initialize them to zero using_mm256_setzero_ps(). If you were to apply the loop unrolling from our previous lessons, you would initialize 4 registers for each sum, for a total of 12 accumulator registers. -
Main Loop: Iterate through the vectors, processing 8 floats (one
__m256register) at a time.- Load data:
const __m256 a_chunk = _mm256_loadu_ps(a_ptr);andconst __m256 b_chunk = _mm256_loadu_ps(b_ptr);. - Update accumulators using FMA:
acc_ab = _mm256_fmadd_ps(a_chunk, b_chunk, acc_ab); acc_a2 = _mm256_fmadd_ps(a_chunk, a_chunk, acc_a2); acc_b2 = _mm256_fmadd_ps(b_chunk, b_chunk, acc_b2);
- Load data:
-
Horizontal Reduction: After the loop, you will have three
__m256registers containing partial sums. You need to perform the same horizontal sum procedure you learned for the dot product on each of these three registers to get three final scalarfloatvalues:sum_ab,sum_a2, andsum_b2. -
Final Calculation: Compute the final result:
sum_ab / (sqrtf(sum_a2) * sqrtf(sum_b2)). Remember to handle the edge case where a norm is zero to prevent division by zero.
Optimization: Reciprocal Square Root
The final division and square root operations can themselves be a performance bottleneck. Standard sqrtf() can be slow. A classic low-level optimization is to replace division by a square root with multiplication by its reciprocal.
Modern CPUs provide a dedicated instruction to compute the reciprocal of a square root, , which is much faster than a full square root followed by a division. In AVX, this is _mm_rsqrt_ps. This instruction is an approximation, so for better accuracy, it's standard practice to follow it with one or two rounds of Newton-Raphson refinement.
The "Understanding SIMD" article you just reviewed provides an excellent example of this.
Understanding SIMD: Infinite Complexity of Trivial Problems
This section details how to perform the final normalization step efficiently using the reciprocal square root instruction and a Newton-Raphson iteration for precision.
Read the section Reciprocal square root and examine the two code blocks. The first shows the basic use of _mm_rsqrt_ps. The second, more robust version, adds a Newton-Raphson step to improve the precision of the result. This is a powerful technique for performance-critical code.
Implementing this refinement shows a deep understanding of low-level optimization that is highly valued in performance engineering roles.
Conclusion
You have now assembled a complete, high-performance plan for calculating Cosine Similarity using SIMD. By moving from a naive three-pass approach to an efficient single-pass method, you've learned a critical pattern for optimizing data-intensive computations.
Key Takeaways:
- An efficient Cosine Similarity implementation calculates the dot product and both squared L2 norms within a single pass over the data.
- This is achieved by maintaining three sets of SIMD accumulator registers inside the main processing loop and updating them with FMA instructions.
- This approach minimizes memory bandwidth usage and maximizes cache efficiency.
- The final calculation can be further optimized by using the fast but approximate reciprocal square root instruction (
_mm_rsqrt_ps), refined with a Newton-Raphson iteration for accuracy.
In our next lesson, we will put this all to the test. You will benchmark the performance of your naive and SIMD-accelerated distance functions to quantify the impressive speed gains you've achieved.