Cache-Friendly Code: Principles, Practices, and Performance Optimization

Nov 27, 2025 · Programming · 10 views · 7.8

Keywords: Cache-Friendly Code | Memory Hierarchy | Locality Principle | Performance Optimization | Data Structure Design

Abstract: This article delves into the core concepts of cache-friendly code, including memory hierarchy, temporal locality, and spatial locality principles. By comparing the performance differences between std::vector and std::list, analyzing the impact of matrix access patterns on caching, and providing specific methods to avoid false sharing and reduce unpredictable branches. Combined with Stardog memory management cases, it demonstrates practical effects of achieving 2x performance improvement through data layout optimization, offering systematic guidance for writing high-performance code.

Memory Hierarchy and Cache Fundamentals

In modern computer architectures, significant differences exist in memory access performance. Registers, as the lowest-level memory structures, can move data within single clock cycles, but their manufacturing cost is extremely high, with most computing cores equipped with only a few registers. In contrast, Dynamic Random-Access Memory (DRAM) is inexpensive (millions of times cheaper than registers) but suffers from access latency of hundreds of clock cycles. To bridge this performance gap, modern systems employ multi-level cache structures (L1, L2, L3), with speed and cost decreasing at each level.

The core idea of caching is based on the locality characteristics of program access: most execution time concentrates on a small set of frequently used variables, while the remaining variables are accessed less frequently. When the processor cannot find required data in the L1 cache, it sequentially queries L2 and L3 caches, ultimately accessing main memory. Each cache miss results in significant time overhead, analogous to the relationship between cache memory and system memory being similar to that between system memory and hard disk storage.

Caching is the primary means to reduce the impact of latency. As Herb Sutter stated: "Increasing bandwidth is easy, but we can't buy our way out of latency." Data is always retrieved through the memory hierarchy (from fastest to slowest), and cache hit/miss typically refers to the state in the highest-level CPU cache. Cache hit rate is crucial for performance because each cache miss requires fetching data from RAM (or worse storage devices), taking hundreds of clock cycles (RAM) or even tens of millions of cycles (hard disk).

Core Principles of Cache-Friendly Code

The key to cache-friendly code lies in fully utilizing the principle of locality, which involves placing related data closely in memory to enable efficient caching. Understanding how cache lines work is essential for optimization.

Temporal Locality Optimization

When a memory location is accessed, it is likely to be accessed again in the near future. Ideally, this data remains in the cache. For example, in Stardog's hash set tests, the case repeatedly accessing the same element (aElement=0) took only 1810 milliseconds, while accessing different elements required 3573 milliseconds, achieving approximately 2x performance improvement, thanks to the processor's ability to consistently retrieve data from the cache.

Spatial Locality Optimization

Storing related data in adjacent memory locations. When reading from RAM, typically a larger chunk of memory than requested is fetched automatically because the program is likely to need adjacent data soon. Hard disk caches employ the same strategy. For CPU caches, special attention must be paid to the concept of cache lines.

The choice of C++ standard library containers fully demonstrates the importance of spatial locality. std::vector elements are stored in contiguous memory, with access efficiency far superior to std::list (elements scattered in storage). Bjarne Stroustrup demonstrated this performance difference through experiments.

Cache Optimization in Data Structure and Algorithm Design

When designing and implementing data structures and algorithms, cache utilization should be fully considered. Cache blocking is a common technique in high-performance computing, as demonstrated by Automatically Tuned Linear Algebra Software (ATLAS).

Leveraging Implicit Data Structure

Row-major (C/C++) versus column-major (Fortran/MATLAB) storage orders significantly impact cache performance. Consider a 2x2 matrix:

1 2
3 4

Row-major storage is 1 2 3 4, while column-major storage is 1 3 2 4. Properly utilizing storage order can reduce memory access times.

Assuming a cache line holds 2 matrix elements, with automatic prefetching of the next element upon access. When calculating the sum of matrix elements:

Utilizing storage order (changing column index first in C++):

M[0][0] (memory access) + M[0][1] (cache hit) +
M[1][0] (memory access) + M[1][1] (cache hit)
= 1 + 2 + 3 + 4
→ 2 cache hits, 2 memory accesses

Not utilizing storage order (changing row index first):

M[0][0] (memory access) + M[1][0] (memory access) +
M[0][1] (memory access) + M[1][1] (memory access)
= 1 + 3 + 2 + 4
→ 0 cache hits, 4 memory accesses

In this simple example, utilizing storage order approximately doubles execution speed. In practical applications, the performance difference can be more significant.

Stardog Memory Management Optimization Cases

Sorted Collection Optimization

Stardog provides two types of sorted collections: standard sorted arrays and long-optimized sorted arrays. Standard sorted arrays store data pointers in address blocks, requiring alternating access between address blocks and data blocks during sorting, leading to poor cache efficiency. Long-optimized sorted arrays embed long values directly into address blocks, requiring only reading data from the same memory block during sorting, fully utilizing cached data.

Performance comparison shows that when sorting 5,000,000 data entries, the standard sorted array takes 11,619 milliseconds, while the long-optimized version requires only 4,759 milliseconds, achieving over 2x performance improvement. This optimization improves both spatial locality (compact data format) and temporal locality (avoiding memory region jumps).

Hash Set Optimization

Traditional DefaultHashSet stores hash codes in address blocks, requiring comparison of user elements with elements in data blocks during lookup, leading to jumps between different memory regions and destroying temporal locality. The optimized LongHashSet stores long values directly in address blocks, completely avoiding data block access, achieving good temporal and spatial locality.

Performance tests show that when inserting 10,000,000 data entries and performing 10,000,000 lookups, the traditional hash set takes 6,428 milliseconds total, while the optimized version requires only 3,929 milliseconds, demonstrating significant performance improvement.

Hash Table Partition Optimization

The original hash table stored address data in partitions, with each partition potentially containing multiple memory blocks. By reducing element slot size from 32 bytes to 16 bytes and adopting dynamic partition counts to maintain single memory blocks per partition, spatial locality was significantly improved.

Pre-optimization data layout was scattered across multiple memory blocks, with insertion of 10,000,000 data entries taking 8,123 milliseconds and lookup taking 6,321 milliseconds, totaling 14,444 milliseconds. Post-optimization with compact layout reduced insertion time to 2,883 milliseconds and lookup time to 3,545 milliseconds, totaling 6,428 milliseconds, achieving over 2x performance improvement.

Common Issues and Avoidance Strategies

Avoiding Unpredictable Branches

Modern architectures employ pipeline designs, with compilers excelling at reordering code to minimize delays due to memory access. When critical code contains unpredictable branches, data prefetching becomes difficult or impossible, indirectly causing more cache misses. This explains why processing sorted arrays is faster than unsorted arrays.

Considerations for Virtual Functions

In C++, virtual methods are controversial regarding cache misses. Virtual functions may cause cache misses during lookup, but if specific functions are called frequently (likely cached), this issue has minor impact. Virtual functions should be used cautiously in performance-sensitive scenarios.

Preventing False Sharing

A common issue in multi-processor cache systems. When different processors attempt to use data from different memory regions but these data map to the same cache line, it causes the cache line to be repeatedly overwritten. Effective avoidance strategies include cache line alignment and data structure redesign.

Preventing Thrashing

An extreme manifestation of poor caching performance in RAM (not specifically referring to CPU cache). Occurs when processes continuously generate page faults (accessing memory not in the current page) requiring disk access. Can be effectively prevented by optimizing memory access patterns and data structure layouts.

Conclusion and Best Practices

Cache-friendly code is a core element of modern high-performance computing. By deeply understanding memory hierarchy, fully utilizing locality principles, and optimizing data structure and algorithm design, program performance can be significantly enhanced. Stardog's practical cases prove that through careful design of data layout and access patterns, achieving over 2x performance improvement is entirely feasible. Developers should always pay attention to cache behavior and consider cache efficiency as an important factor in code design and optimization processes.

Copyright Notice: All rights in this article are reserved by the operators of DevGex. Reasonable sharing and citation are welcome; any reproduction, excerpting, or re-publication without prior permission is prohibited.