In modern SMP(multicore) systems, any processor can write to a memory location. The other processors have to update their caches immediately. For that reason, SMP systems implement the concept of "cacheline bouncing" to move "ownership" of cached-data between cores. This is effective but expensive.
Individual cores have private L1 caches which are extremely faster than the L2 and L3 caches that are shared between multiple cores. Typically, when a memory location is going to be ONLY read repeatedly, but never written to (for example, a variable tagged with the
const modifier), each core on the SMP system can safely store its own copy of that variable in its private (non-shared) cache. As the variable is NEVER written, the cache-entry never gets invalidated or "dirty". Hence, the cores never need to get into "cache line bouncing" for that variable.
Take the case of the x86 architecture:
An Intel core i5 die showing the various caches present
- [NON-SMP] Intel Pentium 4 processor has to communicate between threads over the front-side bus, thus requiring at least a 400-500 cycle delay.
- [SMP] Intel Core processor family allowed for communication over a shared L2 cache with a delay of only 20 cycles between pairs of cores and the front-side bus between multiple pairs on a quad-core design.
- [SMP] The use of a shared L3 cache in the Intel Core i7 processor means that going across a bus to synchronize with another core is NEVER required unless a multiple-socket system is being used.
The copies of "read-only" locations usually end-up being cached in the private caches of the individual cores, which are several orders of magnitude faster than the shared L3 cache.
How __read_mostly is Supposed to Work
When a variable is tagged with the
__read_mostly annotation, it is a signal to the compiler that accesses to the variable will be mostly reads and rarely (but NOT never) a write.
All variables tagged
__read_mostly are grouped together into a single section in the final executable. This is to improve performance by allowing the system to optimize access time to those variables in SMP systems by allowing each core to maintain its own copy of them variable in its local cache. Once in a while when the variable does get written to, "cacheline bouncing" takes place. But this is acceptable as the time spent by the cores constantly synchronizing using locks and using the slower shared-cache would be far more than the time it takes for the multiple cores to operate on own copies in their independent caches.
What Actually Happens
The problem with the above approach is that once all the
__read_mostly variables are grouped into one section, the remaining "non-read-mostly" variables end-up together too. This increases the chances that two frequently used locations (in the "non-read-mostly" region) will end-up competing for the same position (or cache-line, the basic fixed-sized block for memory<-->cache transfers) in the cache. Thus frequent accesses will cause excessive cache thrashing on that particular cache-line thereby degrading the overall system performance.
This situation is slightly alleviated by the fact that modern CPU caches are mostly 8way or 16way set-associative. In a 16way associative cache, each location has a choice of 16 different cache-slots. This means that two very frequently accessed memory-locations, though closely located in memory, can still end-up in 2 different slots in the cache, thereby preventing cache-thrashing (which would have occurred had both continued competing for the same cache-slot). In other words, a minimum of 17 elements frequently accessed and closely located in memory are required for 2 of them to begin competing for a common cache-slot.
While this is true in the case of INTEL and its x86 architecture, ARM still sticks to 4way & 2way set-associative caches even in its latest CortexA9 & A15 offerings; which means that just 3 or 5 closely located, frequently accessed elements can result in cache-thrashing on an ARM system.
This is probably why __read_mostly is NOT implemented for the ARM architecture in the linux-kernel. UPDATE: The patch daf8741675562197d4fb4c4e9d773f53494203a5 enables support for
__read_mostly in the linux kernel for ARM architecture as well.
With the number of cores increasing rapidly and the on-die cache size growing slowly, one must always aim to:
- Minimize access to the last level of shared cache to improve performance on multicore systems.
- Increase associativity of private caches (of individual cores) to eliminate cache-slot contention and reduce cache-thrashing.