Xbox One Memory Performance

Xbox Advanced Technology Group

Updated September 18th 2017

In this topic

Introduction

Memory Types

Write gather

Hardware units

The load/store process

Prefetch

The L2 cache

Lock prefix instructions

Virtual address system

Testing

Appendix A: Read-speed test results

Appendix B: Write-speed test results

Appendix C: Other tests

Resources

Introduction

Optimizing to reduce memory bandwidth is a key strategy for Xbox One. We strongly recommend that, where applicable, titles consider adopting data-oriented design because this can have the single largest effect on title performance. On Xbox One and Xbox One S, ESRAM is the single most effective means of reducing DRAM contention, but it is available only to the GPU. This paper presents a number of options available to the CPU that can be leveraged to optimize title performance.

We recommend that you become familiar with the Xbox One memory-system components, available bandwidth, and coherency models before reading this white paper. See the Resources section for more information.

Memory types

The Xbox One family CPU memory system support three sizes of memory page: 4 KiB, 2 MiB, and 1 GiB. Only 4-KiB and 2-MiB virtual-to-physical address translations are cached by the translation look-aside buffers (TLBs). The platform API exposes 4-KiB, 64-KiB, and 4-MiB pages, which represent the superset of the sizes supported by both the CPU and GPU. On the CPU, 4-MiB and 64-KiB pages are implemented as multiple smaller pages, 2MiB and 4KiB respectively.

Each of the pages exposed by the platform API can be one of three memory types: cacheable (default), write-combined, and non-cached. Titles are unlikely to find non-cached pages useful, so they will not be discussed further in this paper.

Loads from cacheable memory involve fetching data in aligned units of 64 bytes known as cache lines. A cache line is retired and written back to main RAM only when that cache line hasn’t been needed for some time or when another cache line needs the slot in the cache. When a function fails to make use of all bytes in a cache line, it is wasting memory bandwidth. Minimizing this waste is an important optimization strategy.

When a memory page is marked as write-combined instead of cacheable, all load/stores bypass the data cache, reducing pressure on the cache. Stores are batched into 64-byte write-gather buffers and later directly written to main RAM in bursts. There is no mechanism for accelerating loads from write-combined memory, they all are treated as cache misses and go straight to main memory. In our testing, the best speeds we have achieved is 0.045 GiB/s. Write-combined memory should only be used to write data that is not read by the CPU but is later read by either the GPU or DMA engines.

The Xbox One platform has two APIs that allow titles to allocate memory as either write-combined or cacheable and in different page sizes. These are:

  1. XMemAlloc (through the dwMemoryType and dwPageSize members)

    • XALLOC_MEMTYPE_*
    • XALLOC_PAGESIZE_*

See XMemAlloc in the XDK documentation for more details

  1. VirtualAlloc (through the flAllocationType and flProtect parameters)

    • MEM_LARGE_PAGES (64 KiB), MEM_4MB_PAGES (4 MiB)
    • PAGE_READWRITE PAGE_WRITECOMBINE

See VirtualAlloc in the XDK documentation for more details

Write gather

Each core’s bus unit (BU) maintains four 64-byte write-gather buffers used by streaming stores, non-temporal stores, or any store instruction issued to a write-combined memory page. These are used to accumulate sequential stores and write them to RAM when full.

The principal reasons to use write gather are that:

  1. Write gather bypasses the cache, freeing up cache lines for more useful work, not only for the current core but for other cores on the same module.
  2. Write gather uses half the memory bandwidth of cacheable memory. With cacheable memory, even if the CPU is going to write all the bytes in a cache line, it will still load the cache line first. When using write gather there is no load. In our tests, we’ve seen that using write gather improves GPU throughput when the GPU is accessing resources in DRAM. (ESRAM is not affected by CPU load on the Northbridge.)
  3. Stores are not stalled waiting on a cache-line load, improving store queue throughput.

There are some important considerations to keep in mind when using write gather for maximum performance:

  1. Each write must be 4 bytes or greater.
  2. Writes should be sequential and not random.
    • There should be no address gaps
    • No addresses should be written more than once.
    • The sequence of store operations should be ascending memory addresses.
  3. There are only four write-gather buffers per core.
  4. The buffers cannot be snooped; writes must have flushed before any read is attempted by another device.

    NOTE: Failure to do this might result in incorrect data being read.

  5. Ensure that the compiler is not introducing a read.

    a. For example, when manipulating a bit-field, if the compiler spots that a routine fails to set all bits, it must issue a read to preserve the unmodified portion of the bit-field.

Each store using write gather first attempts to append to any of the in-use write-gather buffers. If this attempt fails, the store will grab an unused buffer or, if all buffers are in use, it will flush a buffer and reuse it.

There is no mechanism for snooping data from the write-gather buffers. The operation of a load that accesses data currently waiting for write in a write-gather buffer depends on the memory type being used.

In such instances, the load-hit-store stall will be severe. The write-gather buffer must be flushed and the data reloaded from main RAM.

Flushing

A single write-gather buffer is written to RAM if:

  1. All 64 bytes in its cache line have been written.
  2. No new writes have been appended to the buffer for 512 cycles.
  3. The memory is cacheable, and a cache request references the memory range of an open buffer.
  4. A streaming store cannot append to any of the four open buffers. Note that the store will stall until the buffer has flushed.
  5. The title issues a store fence.

Unless a CPU thread is tightly lock-stepped with the GPU or DMA, it won’t generally be necessary to manually flush the write-gather buffers. The buffers will self-flush after 512 cycles of inactivity. However, we do recommend implementing a “paranoid” mode that performs excessive flushing and synchronization to help track down elusive glitching issues when stale data is used.

Unless a CPU thread is tightly lock-stepped with the GPU or DMA, it won’t generally be necessary to manually flush the write-gather buffers. The buffers will self-flush after 512 cycles of inactivity. However, we do recommend implementing a “paranoid” mode that performs excessive flushing and synchronization to help track down elusive glitching issues when stale data is used.

Be careful not to saturate the four write-gather buffers with more than four array accesses. Similarly, a non-sequential series of stores will cause context thrashing and severely degrade performance. For examples of timings associated with different write scenarios see Appendix B.

There are several methods that flush the write-gather buffers and wait for completion. They are:

  1. The SSE intrinsics, _mm_sfence(), and _mm_mfence
  2. The x64 intrinsic, __faststorefence()
  3. Any Interlocked operation

The interlocked instructions are all implemented by using the lock prefix (sometimes called a load-op-store or read-modify-write operation). This causes all the write-gather buffers to flush before the bus and cache are locked for the duration of the instruction. These are issued by the Interlocked* API, std::atomic, critical sections, and so forth. On Xbox One, the compiler implements __faststorefence() as a lock OR instruction, performing a bitwise OR of zero with the stack pointer.

The 64-byte write-gather buffers are always written as two non-atomic 32-byte operations. Both are issued even if the buffer is half-full or less. Writes are masked, which means that memory isn’t corrupted by this process.

Streaming stores

Streaming stores are issued by non-temporal instructions, like VMOVNT* (which corresponds to the _mm_stream_si128 intrinsic). These use the write-gather buffers even when writing to cacheable memory. Using streaming stores with cacheable memory is an important optimization strategy, halving the amount of memory bandwidth consumed. However, if the memory will be accessed within a short amount of time (512-1500 cycles), it’s generally preferable to use caching writes to avoid the cache miss penalty.

Hardware units

Each core of the CPU has a load store and data cache (LSDC) unit and a bus unit (BU). The LSDC is responsible for servicing all memory accesses made by the core except instruction fetch and write gather. The L1 data cache (L1DC) and translation look-aside buffers (TLBs) ensure that many memory operations can be serviced by core local resources. The bus unit is the interface between the L1 data and instruction caches and the L2 caches. In general, the BU will arbitrate in favor of data rather than instructions. The BU also maintains the write-gather buffers. The most important components of the LSDC to understand are shown in Table 1.

Table 1. Load store and data cache components.

Component Features Function
L1 data cache (L1DC) • 32-KiB, eight-way set associative Caches recently accessed memory locations.
L1 data translation look-aside buffer (L1DTLB) • Fully associative
• 4-KiB and 2-MiB page sizes
• Dual-ported
• Forty-eight entries (40 4KiB, 8 2MiB)
Caches virtual-to-physical memory translations.
L2 data translation look-aside buffer (L2DTLB) • Fully associative
• 4-KiB and 2-MiB page sizes
• Single-ported
• Xbox One and Xbox One S: 768 entries (512 4-KiB, 256 2 MiB)
• Xbox One X: 2,304 entries (2048 4-KiB, 256 2-MiB)
Caches virtual-to-physical memory translations.
Table walker (TWC)   Populates the TLBs on misses.
L1 data prefetcher • Eight entries Recognizes data-cache miss patterns and issues prefetch requests ahead of time.
Missed-address buffer (MAB) • Eight entries Holds the physical addresses of L1DC misses.
Unified store queue (USQ) • Twenty entries, 128 bits each Holds all pending stores, handles blocking and store-to-load forwarding.

The LSDC supports:

  1. 48-bit virtual addresses and 40-bit physical addresses.
  2. 128-bit floating-point and 64-bit integer-aligned loads or stores per cycle.
  3. 256-bit Advanced Vector Extension (AVX) load/stores are broken into two 128-bit memory operations. These are not atomic—that is, 32-byte alignment offers no advantage over 16-byte alignment.
  4. Misaligned load/stores are also broken into two non-atomic memory operations; a minimum delay of one cycle occurs between each high and low pair.
  5. Up to 16 pending load operations (these might be processed out of order).
  6. Up to 20 pending store operations (not including writer gather).
  7. Up to eight pending data-cache misses before the core will stall.

In the event of an L1DTLB and L1DC hit, the load-to-use latency for an integer is three cycles. For floating-point data, the load-to-use latency is five cycles.

The load/store process

The load/stores process is complex, and there are many stages that can potentially stall for a variable number of cycles, depending on many external factors.

Loads

The process is illustrated in Figure 1 (see next page).

Figure 1. Load process.

There are eight entries in the missed-address buffer (MAB). These are serviced by the BU, which requests cache lines from the L2. The MAB detects and eliminates duplicate entries. A misaligned load/store that spans a cache line requires two MAB entries to service. The MAB can only accept a single request per cycle. If both load and store requests are generated in the same cycle, up to four store requests can be deferred in favor of a load request.

The MAB does not handle write-gather operations; these operations have their own special interface to the bus.

A cache line can be out of date if:

  1. A streaming instruction has modified the mapped area of main RAM and the memory type is cacheable.
  2. The core is attempting to write to a line and another core has written to a copy of the same line—a load will not block if the cache line is out of date, whereas a store will.

An L1DC miss but L2 cache hit takes a minimum of 17 cycles to service. Achieving this 17-cycle latency requires that:

  1. There is a free MAB entry available.
  2. No other MAB buffer entries are serviced ahead of the current miss.
  3. The required cache line is in the L2.
  4. There is no contention for the L2 bank (discussed in the L2 cache section).

That is, a single MAB entry is occupied for a minimum of 17 cycles. It is most easily saturated by either excessive memory prefetching or many successive L2 and/or TLB misses.

Load guarantee

Although the LSDC supports out-of-order loads, a guarantee is made that the data read will be temporarily correct. Consider an example in which core 0 reads the same address twice, and core 1 writes to that address:

  1. Both loads get old data: legal.
  2. Both loads get new data: legal.
  3. The first load gets old data and the second load gets new data: legal.
  4. The first load gets new data and the second load gets old data: illegal.

To avoid the illegal case, when loads have been processed out of order, a cache-line refill will be deferred until all affected loads have completed.

Lock prefix instructions and reads from write-combined memory act as fences over which loads might not be reordered.

Streaming loads

Streaming SIMD Extensions 4 (SSE4) introduced streaming loads with the VMOVNTDQA instruction (which corresponds to the _mm_stream_load_si128 intrinsic). Xbox One cores have no specific hardware acceleration for this instruction; however, the instruction does offer more opportunity for instruction reordering and this can sometimes produce a small speed increase.

An aligned 128-bit integer load can be done as an instruction operand:

vpaddd      xmm0,xmm1,xmmword ptr [rax] 

This is not the case with VMOVNTDQA, however:

vmovntdqa   xmm0,xmmword ptr [rax]  
vpaddd      xmm0,xmm0,xmm1 

Stores

Stores are queued in the Unified Store Queue (USQ). Unlike loads, stores are committed in strict age order. Up to two stores can be dispatched per cycle. Two halves of a misaligned store will be committed back to back, but these are not atomic: a minimum delay of one cycle will occur between storing each half.

The process of mapping a virtual address to a physical address is the same for a store as it is for a load.

When a store reaches the head of the queue, it will block if any of the following conditions apply:

  1. The physical address hasn’t yet been retrieved.
  2. The store is still speculative; that is, the core has not yet determined whether the correct code path was taken.
  3. The store is being made to cacheable memory and the cache line is not present in L1.
  4. The cache line is in L1, but another core has made a write to the same line and a cache-line refill is pending.

To provide protection before the physical address has been obtained, a load will block if the least-significant 11 bits of its virtual address match a store’s least-significant 11 bits. In practice, we have seen that aliasing array access on 2-KiB alignment can halve the throughput of the memory operations.

Stores can potentially block for a considerable amount of time. The size of the USQ is 20 entries, and when the USQ is full, a store will stall the core. The USQ can accommodate only one misaligned store crossing a page boundary at a time. The core must block on store if attempting to add a second.

Store-to-load forwarding

Store-to-load forwarding allows a load to pick up data that is still in the store queue and not yet committed to the data cache. This is a particularly important optimization in register-overspill scenarios. Many conditions must be satisfied for forwarding to work:

  1. The store is being made to cacheable memory and is not streaming.
  2. The physical addresses of both the load and the store have been obtained.
  3. The physical addresses match exactly.
  4. The store size is greater than or equal to the load size.
  5. There are no newer pending stores that alias the memory.
  6. The load and store are both aligned.
  7. The store is not speculative; that is, the CPU has determined that the correct code path was taken.
  8. The store is not part of an atomic operation.

If the above conditions are not met, a load-hit-store stall is generated and the store must either complete or become non-speculative before the load is unblocked.

Register overspill of temporary variables on the stack is likely to satisfy all conditions except perhaps point 7, the speculative condition. Despite store-to-load forwarding, register overspill is still a situation to be avoided whenever possible. With register overspill, the CPU must issue the store instruction and then a load instruction, both of which have latency, as does the store-to-load forwarding mechanism.

Prefetch

In addition to the manual memory prefetch, Xbox One implements hardware memory prefetch. Leveraging the hardware prefetch can be useful as an optimization strategy in certain scenarios. Cache lines are 64 bytes.

Manual prefetch

Titles can issue an instruction to fetch a cache line through the _mm_prefetch() intrinsic. This instruction has a certain cost and might even be detrimental to performance, particularly if:

  1. The hardware prefetcher would have fetched the cache line anyway.
  2. The MAB has no free entries and the prefetch instruction then stalls the core.

Care must be taken to ensure that the compiler does not reorder prefetch instructions and, in so doing, cause MAB saturation that stalls the core.

Xbox One has no mechanism for loading data into the L1DC but not the L2, because the L2 always includes the L1 caches.

The _mm_prefetch() intrinsic will accept the following hints:

  1. _MM_HINT_T0: loads the line into all levels of the data cache.
  2. _MM_HINT_T1: brings the data into L2, but not L1.
  3. _MM_HINT_T2: does the same as NTA (non-temporal access).
  4. _MM_HINT_NTA: hints to the cache that the line is temporary and might be evicted early.

Non-temporal prefetch is implemented by restricting the cache line to a single set of the L2. The L2 is a 2-MiB, 16-way set associate cache; that is, each set is 128 KiB. In effect, therefore, non-temporal prefetch constrains the cache line to a 128-KiB direct-mapped L2 cache. This setup is advantageous because direct-mapped caches are faster to access, and the other 15 sets in the L2 will be entirely unaffected, freeing up cache for other operations. The disadvantage is that each cache line can only be mapped to a single line in the 128-KiB set. The line used is determined by bits 7 to 17 of the address. Collisions result in evictions, which will cause considerable performance penalties if the cache line is needed again. Titles must take care to ensure that aliasing cache lines are not loaded before the previous line has been finished with. It can be particularly difficult to avoid collisions when multiple threads are concurrently issuing non-temporal prefetches.

It is possible to prefetch the address of functions into the L2 cache (which is a unified instruction/data cache) but not the L1 instruction cache. Doing this will pollute the L1DC with instructions, although the overall effect can be a performance increase.

It should be noted that prefetch does nothing when it is applied by write-combined memory; it simply wastes execution resources.

Hardware prefetch

There are two hardware prefetch units per core, one for the L1 and one for the L2.

The L1 prefetcher monitors the MAB to determine patterns of missed cache lines. The prefetcher can spot both forward and backward array access. When an access occurs, it is compared against the currently active streams to see whether it fits one (or more) of them. If it does, the access is ignored by the prefetcher. Each time two memory accesses miss all existing active streams, a new stream is initialized. The prefetcher can maintain up to eight access streams at once and prefetch between one and four lines ahead. Each stream can only have one prefetch in flight at once.

The L2 prefetcher works in the same way. For each core, there is a prefetch unit that monitors the L1DC misses, ignoring instruction-cache misses. Each L2 prefetcher has 10 streams, each of which is associated with a single page.

Prefetching stops if either:

  1. A 4-KiB page boundary is reached.
  2. A lock prefix instruction has the cache locked.

Because the hardware prefetch units cannot fetch from a different page, they will not trigger a TLB miss and will start priming the TLB ahead of time.

Manual prefetch can be used to kick off a new hardware prefetch stream.

The L2 cache

The L2 cache has the following characteristics:

  1. Each module’s 2-MiB L2 cache includes the L1 caches for each core.
  2. The minimum L2 hit-to-use latency is 17 cycles.
  3. Internally, the L2 is composed of four 512-KiB banks. Bits 6 and 7 of the cache-line address determine which bank holds the line. That is, contiguous memory is spread over all banks.

Each L1 can receive 16 bytes per cycle, and each bank can transmit 16 bytes per cycle. The maximum L2 to L1 throughput, therefore, requires that all banks upload to a different L1.

Lock prefix instructions

Lock prefix instructions (also known as atomic, load-op-store, or read-modify-write instructions) are issued through the Interlocked* API, std::atomic, CriticalSection, and so on. These pass down a special flag that locks the cache and bus on load and unlocks it after the store completes. Lock prefix instructions are added to the store queue like any other store operation. That is, the load will lock the cache and bus, but the pending stores must all be processed before the atomic store unlocks the cache and bus again. Reducing the chance that preceding stores will block will decrease the time it takes to unlock the cache and bus.

Lock prefix instructions cannot be executed speculatively and cannot be executed out of order. Executing a lock prefix load operation forces a series of actions to complete before the lock is granted:

  1. All active write-gather buffers are flushed to RAM prior to the lock.
  2. All data-cache operations required by instructions that are encountered before the lock instruction will be serviced, including page-table walks.

These conditions ensure that the head of the store queue can be processed until the atomic store is encountered without having to make any changes to the cache or having to flush the write-gather buffers while the cache and bus are both locked.

There are several factors that make atomic instructions slow:

  1. Memory-concurrency features of the LDSC that are designed to improve throughput are disabled or prematurely flushed.
  2. Pending stores must all be processed in sequence before the atomic store that unlocks the cache and bus is encountered.
  3. The round-trip time to load a value, modify it, and store it at the globally visible level can be lengthy.
  4. Other cores are blocked from accessing shared cache lines while the load-op-store completes.

Locks—CriticalSection and mutex—are internally implemented with multiple atomic instructions and kernel calls. A single atomic instruction is therefore faster than a lock, but that still does not make it a fast operation. The atomic (indivisible) guarantee of a lock prefix instruction breaks when the data being acted on is not on its natural alignment. In such cases the load and store operations are broken into a high and low operation, and other concurrent accesses to that memory also made through lock prefix instructions can end up with incorrect data.

Virtual address system

On Xbox One all memory addresses are virtual. The CPU and GPU’s virtual address spaces are kept in sync, meaning that it is safe to pass pointers between them.

Xbox One runs multiple concurrent operating systems. To provide security, each operating system has its own virtual address space, and physical RAM is managed by a hypervisor. This means that the process of mapping from virtual to physical memory is a nested one.

Data translation look-aside buffers

Translation look-aside buffers (TLBs) are caches of virtual-to-physical address translations at the level of a memory page. The data TLB (DTLB) consists of a small, fast Level 1 DTLB backed up by a slower, larger Level 2 DTLB. When the L2DTLB misses, requests are referred to the table walker (TWC). The TWC performs a search to find the missing table. Instruction fetch has its own ITLBs, which are not discussed here. Each core has its own L1DTLB, L2DTLB, and table walker. L2DTLBs are not shared between cores.

The L1DTLB is a 48-entry, fully associative, dual-ported cache that holds 40 4-KiB translations and eight 2-MiB translations. It has separate load and store request ports. The L1DTLB will return hit/miss status to LS the cycle after LSDC posts a request to the TLB. If the request was a miss, then it might result in a request to the L2DTLB. The depiction of the load process that was shown in Figure 1 is a slight simplification: the L2DTLB doesn’t directly make requests of the TWC. Rather, the load is replayed and the second time it fails the L1DTLB, a request is issued to the TWC instead of to the L2DTLB.

On Xbox One and Xbox One S, the L2DTLB is a 768-entry, fully associative, single-ported cache that holds 512 4-KiB translations and 256 2-MiB translations. Xbox One X increases the number of 4-KiB entries to 2,048.

The L2DTLB will report a hit or miss four cycles after the original LS request was posted. The L2DTLB is single ported, and only one request can be serviced at a time. This means that if both the load and store requests miss the L1DTLB, then it will arbitrate between these for the request to the L2DTLB. In general, load requests have priority.

The D1TLB uses a mixture of least-recently used (LRU) and round-robin (RR) retirement schemes, and the D2TLB uses RR schemes only. Because of this difference in retirement schemes, the D1TLB does not necessarily include the D2TLB.

Misaligned load/stores over 4-KiB page boundaries increase TLB pressure. It is possible that each half of the memory operation has a different memory type or even disjointed physical addresses.

If a title is only using 4-KiB or 2-MiB pages, then a percentage of the TLB caches is going to waste. 2MiB pages are well worth using.

The difference between hitting and missing the TLBs can be observed by comparing a random walk of cache lines against a pseudo-random walk. The pseudo-random walk hits the TLB far more often.

The L1DTLB has a single-cycle latency, while the L2DTLB has a four-cycle latency.

Table walking

On Xbox One a virtual address consists of indices into several page tables and finally an offset into the physical page. Compared to 2-MiB pages, 4-KiB pages have one extra directory. The 48-bit virtual address space is divided as follows:

  1. For 4-KiB pages:

    a. Four 9-bit directories = 36 bits

    b. A 12-bit page offset = 4 KiB.

  2. For 2-MiB pages:

    a. Three 9-bit directories = 27 bits.

    b. 21-bit offset = 2 MiB.

Each core has a single hardware page table walker dictating the structure of the page tables. The table walker is shared and services both instruction and data TLBs.

Figure 2. Single-layer table walk on a 4-KiB page.

Note: Each directory is 9 bits, 512 entries large. In this illustration, it is shortened to two entries for the sake of brevity.

When performing a 4-KiB page-table walk, the hardware needs to issue five serial memory accesses to obtain the physical page. For 2-MiB pages, only four serial memory accesses are required. Each memory load is made through the L1DC and L2 cache, the same as regular memory requests. Speed therefore depends on the existing content of the caches and contention for the L2 and/or DRAM. Because TLB refill uses the data cache, 2-MiB pages also reduce cache pressure.

With the nested operating systems on Xbox One, what happens is that each page-table entry is another virtual address that needs a second page-table walk at the hypervisor level to find the actual physical address.

Note: The hypervisor uses 2-MiB pages.

Figure 3.

The 2-MiB TLB entries are shared between the hypervisor and the guest operating system, which is something to account for when deciding whether the L1TLBs might be churning.

In reality, many of the directory lookups will hit the cache. For example, unless the OS maps more than 512 GiB of virtual memory, the level 0 pointer will always be the same.

Testing

To test the performance characteristics of the Xbox One memory system, we constructed test code that timed a variety of combinations.

  1. Reads and writes of entire cache lines using either regular SSE integer load/stores or streaming load/stores (_mm_stream_load_si128, _mm_stream_si128), both of which are 128-bit.
  2. Different-sized data sets on a log2 scale:
    • Linear progression, both forward and backward.
    • Pseudo-random; that is, each cache line in the data set was visited once but in random order.
    • A small random selection of an extremely large set.
    • In the random cases, a second array was used to control which cache line to read next; this array was always read linearly.
  3. Either not using prefetch or using prefetch with hints: _MM_HINT_T0 and _MM_HINT_NTA.
    • We prefetched four cache lines in advance.
  4. Using both write-combined and cacheable memory.
  5. With either zero, one, or five worker threads hammering the data cache:
    • Each worker thread looping a thread-local 4-MiB buffer reading and writing from each cache line. Linear rather than random access.

We ran multiple iterations of each combination, flushing out the data cache and TLBs between runs. This meant that the speeds for a low number of cache lines was very poor, and the work done was low for the cost of the initial table walks/TLB population.

We graphed the results of our testing, many of which are depicted in the appendices. Because testing every combination generated a huge amount of data, we chose not to reproduce the following:

  1. Almost all write-combined memory tests: Reading from write-combined memory was awful in every combination, achieving a maximum speed of 0.045 GB/s, often much less. This figure is far too low to be useful.
  2. Traversing memory backward, which was no different from traversing it forward.

Observations

Here is a short summary of the observations we made during testing.

Recommendations

Appendix A: Read-speed test results

Xbox One X has a higher memory bandwidth than Xbox One or Xbox One S. However, the CPU core works identically and at the same cycle counts. The relative differences between the memory operations are the same across the Xbox One family. The recommended optimization strategies for Xbox One also apply to Xbox One X. Because of this, we don’t provide separate numbers specific to Xbox One X. Follow these links to see graphic depictions of our different read-speed test results:

  1. Read speed, cacheable, forward traversal
  2. Read speed, cacheable, looping dataset
  3. Read speed, cacheable, random traversal
  4. Read speed, cacheable, pseudo-random traversal

Read speed, cacheable, forward traversal

With five worker threads all accessing memory:

Read speed, cacheable, looping dataset

With five worker threads all accessing memory:

Read speed, cacheable, random traversal

With five worker threads all accessing memory:

Read speed, cacheable, pseudo-random traversal

Pseudo-random tests involved visiting each cache line in a fixed-size dataset just once.

With five worker threads all accessing memory:

Appendix B: Write-speed test results

Xbox One X has a higher memory bandwidth than Xbox One or Xbox One S. However, the CPU core works identically and at the same cycle counts. The relative differences between the memory operations are the same across the Xbox One family. The recommended optimization strategies for Xbox One also apply to Xbox One X. Because of this, we don’t provide separate numbers specific to Xbox One X. Follow these links to see graphic depictions of our different write-speed test results:

  1. Write speed, cacheable, forward traversal
  2. Write speed, cacheable, looping dataset
  3. Write speed, cacheable, random traversal
  4. Write speed, cacheable, pseudo-random traversal
  5. Write speed, write-combined, forward traversal

Write speed, cacheable, forward traversal

With five worker threads all accessing memory:

Write speed, cacheable, looping dataset

With five worker threads all accessing memory:

Write speed, cacheable, random traversal

With five worker threads all accessing memory:

Write speed, cacheable, pseudo-random traversal

With five worker threads all accessing memory:

Write speed, write-combined, forward traversal

We tested writing out the same amount of data using one pointer or four pointers to one-quarter of the same array. We also tested writing out only each alternate four bytes and skipping four bytes (so writing less); these are the restarting cases in the following graphs. We also tried using five streams but without any restarts.

With five worker threads all accessing memory:

Appendix C: Other tests

Xbox One X has a higher memory bandwidth than Xbox One or Xbox One S. However, the CPU core works identically and at the same cycle counts. The relative differences between the memory operations are the same across the Xbox One family. The recommended optimization strategies for Xbox One also apply to Xbox One X. Because of this, we don’t provide separate numbers specific to Xbox One X.

Fence instruction speed

We timed four streams writing out sequential data. After each four bytes written we inserted a fence instruction to flush all four write-gather buffers.

Resources

The resources listed below are recommended as prerequisites before reading this white paper. Both are available for download from Game Developer Network.