Skip to main content

Abstract

Building on the foundations established in XBOX One vs XBOX Series X|S this entry addresses the performance issues and costs associated with different read/write operations to shared memory between processor cores. For a refresher on the architecture, features, and performance of the XBOX One family and XBOX Series consoles CPUs, we suggest that you take a look at the mentioned entry. Introduction Shared Data The Costs of Data Sharing Recommendations Appendix: Code

Introduction


XBOX One vs XBOX Series X|S describes the cost associated with an L1 cache miss. One of the most common patterns for an L1 cache miss is writing to an address shared between CPU cores — for instance, data shared between multiple threads. The purpose of this entry is to provide relative numbers for the penalty associated with sharing writable data between cores. The main takeaway from our tests is that it’s preferable to confine data in read/write operations to each individual core, rather than sharing data between cores and threads. Sharing data across cores will slow down your code’s processing rather than speeding it up. The XBOX One family uses a Jaguar processor and the XBOX Series consoles uses a Hercules processor. Both of these processors use the MOESI protocol - Wikipedia to maintain a strong memory model and cache coherency. Each cache line can be in one of five states.
  • Modified
    • This processor has the only valid copy of the cache line and has made changes.
    • The cache line does not match memory.
  • Owned
    • This processor has the only valid copy and has made changes.
    • Other processors may have a read-only copy.
  • Exclusive
    • The processor has the only copy of the cache line.
    • The contents of the cache line match memory.
  • Shared
    • This processor has one of several copies of the cache line.
    • Another processor may have it in the Owned state if changes have been made
  • Invalid
    • This cache line is not valid and must be fetched before access.
See the AMD64 Architecture Programmer’s Manual Volume 2: System Programming found on AMD Developer Central for more detailed information on the AMD implementation.

Shared Data


Data sharing can happen in several ways. One of the more obvious is the direct sharing of variables, such as a reference count on a structure. This can easily result in the data being modified as each thread makes copies through a shared_ptr, for instance, as a parameter in a function call. That operation alone will result in resource contention. Another kind of data sharing, known as false sharing, is harder to detect. False sharing occurs when two memory addresses resolve to the same cache line. Both the Jaguar and Hercules processor’s L1 cache line is 64 bytes in size and is aligned to 64 bytes, so two integers in the same 64-byte block are considered to be shared. Consider, for example, the following two data structures:
The threadOne, threadTwo, threadThree, and threadFour data are unique per thread. Thread one will only operate on threadOne data, Thread two only on threadTwo data, and so forth. Between these two data structures, OuterClassSlow could operate up to twenty times slower than OuterClassFast when switching to multithreaded processing for a standard write operation alone. This is because the data in OuterClassSlow all share the same cache line as the result of false sharing. They are adjacent to each other in memory. This is an example of the difference between an array of structures (AoS) and a structure of arrays (SoA).

The costs of data sharing


Test profile

The testing profile followed these criteria:
  • The instructions tested were a raw read, a raw write, an atomic load, an atomic store, and an atomic Compare and Store (CAS) operation. The code used for each set of instructions is provided in the Appendix: Code.
  • Tests were performed with an 8-byte memory location shared by all threads (shared cache line) or with each thread having a unique 8-byte memory location (unique cache line).
  • Forty-thousand operations were executed in a tight loop.
  • The optimizer was set to minimize size to disable loop unrolling.
  • A total of 100 runs were performed; the median from all the runs is shown in the tables that follow.

Testing on a single core

For our baseline, we started by testing each operation on a single core. | Operation | XBOX One | XBOX One X | XBOX Series S - 3.4 GHz | XBOX Series X|S - 3.6 GHz | XBOX Series X - 3.8 GHz | |--------------|----------:|----------:|----------:|----------:|----------:| | Raw Read | 68.78 us | 52.34 us | 11.81 us | 11.16 us | 10.56 us | | Atomic Load | 68.78 us | 52.34 us | 11.81 us | 11.16 us | 10.56 us | | Raw Write | 114.61 us | 104.64 us | 11.81 us | 11.16 us | 10.56 us | | Atomic Store | 369.55 us | 278.99 us | 207.11 us | 195.61 us | 185.31 us | | Atomic CAS | 668.79 us | 509.02 us | 203.43 us | 192.13 us | 182.03 us | The first is the difference in timings between the Jaguar processor and the Hercules processor. There is up to a four fold increase in performance on Hercules. This is due to a combination of the higher clock speed, higher bandwidth, and support for more simultaneous memory operations in flight Comparing 40,000 straight reads with 40,000 atomic loads, there is zero difference in the times. This is due the strong memory model of the CPU and the MOESI protocol. The compiler is able to generate the same instructions for both operations. The raw write and atomic store operations in this test attempt to increment a value in memory. In this test there is a two-fold increase in the cost between a raw write and an atomic store. This is because the atomic store is using the xchg instruction which implicitly includes the lock prefix. This has the possibility to stall the processer until the results have been flushed to the cache because it cannot reorder operations across instructions using the lock prefix. The raw write uses the mov instruction. This will not stall the processor, it is freely allowed to reorder operations across the mov instruction. This brings us to the atomic Compare and Store (CAS) operation. The operation for this test is similar to the raw write and atomic store tests, attempt to increment a value in memory. The lock prefix is used with the cmpxchg instruction for this test to make it comparable to the atomic store test. On the Jaguar processor the CAS operation is more expensive than the previously mentioned xchg due to the compare. However the Hercules processor has an improved implementation which keeps the cost similar to an atomic store.

Testing on multiple cores

The multiple-core tests all used two threads in several core configurations.
  • Two threads on separate physical cores, but in the same cluster.
  • Two threads on separate physical cores and in separate clusters.
  • Where SMT was enabled, two threads running on the two logical cores on a single physical processor.
We did two different runs for each test. In the first run, the two threads shared the same uint64_t address. In the second run, each thread had their own unique uint64_t address that did not share cache lines. The reason we did this was to make it easier to see the performance penalty when cache lines are shared between processors. For each test, the threads were started at the same time, with the timing coming just from the loop iteration. The threads were also set to high priority to lower the chance of a context switch as much as possible. One-hundred runs were performed for each test; the median results are shown in the tables that follow. The numbers given below are all relative to the base single core test, how much more expensive was the operation when running across several cores.

Raw read/Atomic Load

This is a straight read from a memory location. There is no contention between the threads, because nothing is changing the data: each thread has its own copy of the data in its L1 cache. As a result, the times between reading shared data and reading unique data are the same. Due to the strong memory model the compiler is allowed to generate the same code for an atomic load as a normal read. This means the times are the same for each operation. | Test | XBOX One | XBOX One X | XBOX Series X|S - SMT | XBOX Series X|S - no SMT | |--------------|----------:|----------:|----------:|----------:|----------:| | Single Core | 1.00 | 1.00 | 1.00 | 1.00 | | Same Physical Shared | N/A | N/A | 1.97 | N/A | | Same Cluster Shared | 1.00 | 1.00 | 1.00 | 1.00 | | Cross Cluster Shared | 1.00 | 1.00 | 1.00 | 1.00 | | Same Physical Unique | N/A | N/A | 1.97 | N/A | | Same Cluster Unique | 1.00 | 1.00 | 1.00 | 1.00 | | Cross Cluster Unique | 1.00 | 1.00 | 1.00 | 1.00 | There are two main takeaways from this table. The first is that only reading from memory has no effect on performance even when multiple cores are all reading from the same location. Each processor has a valid copy in their cache and their cache line is in the Shared state since more than one processor has a copy. The second is the doubling in time for the configuration where both threads are running on the same physical core. When SMT is enabled the resources of the core are shared between two threads. This can result in a performance gain because frequently one thread is not able to utilize all the available resources. However, this test is a very tight loop dominating the Load/Store unit and the L1 cache. A single thread is able to use all the slots in the Load/Store unit, therefore the two threads must share them in a round-robin format. The net effect is each thread take twice as long, however twice as much work is being done by the core. Overall the same amount of work is being done for the same amount of time by the core.

Raw write

A simple write to memory shared between cores will be significantly slower than a write by a single core to a non-shared memory location. The write operation causes the core to update the cache line and invalidate any copies on other cores. This particular test is an increment of a value in shared memory. This means if the core does not have a valid copy in it’s cache it needs to request the data from either memory or the last writer to the address. This can create a ping-pong effect between the two cores where one core updates the data, the second core reads the data from the first core and updates it, then the first core has to read the data from the core before it can update. This continues back and forth through each iteration through the test loop.. The overhead of the ping-pong effect can be seen in this table. | Test | XBOX One | XBOX One X | XBOX Series X|S - SMT | XBOX Series X|S - no SMT | |--------------|----------:|----------:|----------:|----------:|----------:| | Single Core | 1.00 | 1.00 | 1.00 | 1.00 | | Same Physical Shared | N/A | N/A | 10.69 | N/A | | Same Cluster Shared | 10.54 | 8.73 | 24.36 | 23.59 | | Cross Cluster Shared | 13.61 | 12.78 | 18.89 | 23.57 | | Same Physical Unique | N/A | N/A | 1.98 | N/A | | Same Cluster Unique | 1.05 | 0.93 | 1.03 | 1.03 | | Cross Cluster Unique | 1.01 | 0.84 | 1.04 | 1.03 | There are several takeaways from this table. The first is the difference in the relative impact on performance between the Jaguar and Hercules processor. On Hercules the cost to share a cache line is more expensive than on Jaguar. In this case Jaguar has a cost of 17 cycles to request the data from the other core, the same operation on Hercules costs 90 cycles. This is due to the deeper cache on Hercules, the existence of an L3 cache that is missing from Jaguar. The second is the difference between using a shared memory location and a unique location. In this case the operation is an increment of the current value, this is a read/modify/write operation. The read operation has to be fulfilled by the core that most recently wrote to that memory location. If this is not the same core the cost for the read is drastically higher. As can be seen in the table this could be up to twenty five times more expensive. The third is the smaller relative difference when the two threads are sharing a single physical core with SMT enabled. The updated cache line doesn’t need to be fetched from another core, it is already local. However the store-to-load forwarding cannot be applied. When thread A updates the value it needs to be flushed to the cache before thread B can use the value. The fourth is the doubling in time for the configuration where both threads are running on the same physical core but operating on unique memory. When SMT is enabled the resources of the core are shared between two threads. This can result in a performance gain because frequently one thread is not able to utilize all the available resources. However, this test is a very tight loop dominating the Load/Store unit and the L1 cache. A single thread is able to use all the slots in the Load/Store unit, therefore the two threads must share them in a round-robin format. The net effect is each thread take twice as long, however twice as much work is being done by the core. Overall the same amount of work is being done for the same amount of time by the core.

Atomic store

Atomic store uses the xchg instruction which implicitly signals the lock flag. The xchg instruction is required to read and then write to the memory address in question. During this time the cache line is locked, preventing the other cores from accessing that cache line. This can stall a pending operation until the xchg instruction has written the data to the cache. Another cost is that the core cannot reorder operations across the xchg instructions. The final cost happens if another core has modified the data. In the case the data will need to be fetched from that core. Any other cores will need to wait during this entire sequence when operating on the shared cache line. As with the Raw Write test the Atomic Store test can have a similar ping-pong effect, however in this case it is more pronounced and even effects the single core test. The processor cannot speculatively execute further than one iteration through the loop. Thread A performs the xchg instruction while thread B waits for access. As soon as thread A is finished thread B can continue and immediately requests a copy before thread A can request access from the next iteration through the loop. This table shows the relative cost for the xchg instruction compared against a single core performing the operation. | Test | XBOX One | XBOX One X | XBOX Series X|S - SMT | XBOX Series X|S - no SMT | |--------------|----------:|----------:|----------:|----------:|----------:| | Single Core | 1.00 | 1.00 | 1.00 | 1.00 | | Same Physical Shared | N/A | N/A | 2.38 | N/A | | Same Cluster Shared | 6.84 | 6.85 | 4.17 | 4.04 | | Cross Cluster Shared | 10.64 | 13.40 | 3.72 | 3.56 | | Same Physical Unique | N/A | N/A | 0.98 | N/A | | Same Cluster Unique | 1.00 | 1.00 | 1.00 | 1.00 | | Cross Cluster Unique | 1.01 | 1.00 | 1.00 | 1.00 | There are two takeaways from the data in this table. The relative times when using a shared address are not as extreme for an atomic store as opposed to a raw write. The reason for this is that a large amount of the cost for this test has already been paid with the single core test. However the overall cost for the Atomic Store is still two to four times more expensive then a raw write on Hercules and up to thirteen times more expensive on Jaguar. The second is that Hercules has an improved implementation for lock operations which in this case causes the same cluster and cross cluster tests to take approximately the same amount of time. The Jaguar processor can be up to twice as expensive when data is shared between clusters and lock operations.

Atomic Compare and Store (CAS)

The atomic compare and store (CAS) operation in this test uses the cmpxchg instruction with the lock prefix, this means while operation is being performed the cache line is locked and cannot be accessed by other processors. This is the same way the xchg instruction works in the Atomic Store test. In this test the CAS operation performs the same math as the Atomic Store operation. It attempts to increment the value in memory, either shared or unique. The only difference is that the CAS operation will only write to the memory location if it’s equal to a specified third value. | Test | XBOX One | XBOX One X | XBOX Series X|S - SMT | XBOX Series X|S - no SMT | |--------------|----------:|----------:|----------:|----------:|----------:| | Single Core | 1.00 | 1.00 | 1.00 | 1.00 | | Same Physical Shared | N/A | N/A | 2.91 | N/A | | Same Cluster Shared | 4.32 | 4.32 | 4.01 | 4.24 | | Cross Cluster Shared | 10.12 | 14.80 | 3.52 | 3.45 | | Same Physical Unique | N/A | N/A | 0.91 | N/A | | Same Cluster Unique | 1.00 | 1.00 | 1.00 | 1.00 | | Cross Cluster Unique | 1.00 | 1.00 | 1.00 | 1.00 | Because both the Atomic Store and the Atomic Compare and Store tests use the lock prefix their relative costs are the same and for the same reasons. The relative times between single and multi-threaded operations using a shared address are not as extreme because a large amount of the cost for this test has already been paid with the single core test. However the overall cost for the CAS is still two to four times more expensive when the data is shared on Hercules and up to fourteen times more expensive on Jaguar. The second is that Hercules has an improved implementation for lock operations which in this case causes the same cluster and cross cluster tests to take approximately the same amount of time. The Jaguar processor can be up to twice as expensive when data is shared between clusters and lock operation are used.

Recommendations


There are several key patterns you can follow to avoid sharing writable data between cores:
  • Data structure: switch from using an array of structures (AoS) to a structure of arrays (SoA) where each thread is operating on a unique structure.
  • Separate out read-only data from read/write data.
  • Padding: you can pad a structure by a multiple of a cache line in size (64 bytes). This will remove false sharing if each thread operates on a structure.
  • Reference counting (shared_ptr, weak_ptr, and so on): they both have a single reference count variable which is shared among all instances of the object.
  • Be very clear on ownership of data. When data is passed off to another thread for processing, that thread now owns the data. Avoid doing anything on the data until that thread is done with it.
  • Shared job queue: try to avoid using one job queue that is shared by multiple threads, especially if the jobs are small. Look instead at having a unique job queue for each thread. You can use the work stealing algorithm to help balance the load between the threads.

Appendix: Code


Raw read

Atomic load

Raw write

Atomic store

Atomic CAS

Last modified on August 20, 2026