Andrew Farrier, Advanced Technology Group
Updated September 18th, 2017
What are the main data cache issues?
What are the main branch mispredict issues?
Performance Monitor Counters (PMCs)
Function Summary/Callgraph Capture
This paper covers the high-level steps needed to find common cache-related issues and branch mispredict issues within an Xbox One title. Useful related documentation that’s available on the Game Developer Network (GDN) includes the following white papers:
It is vital that data cache and branch mispredict issues are located in title code so the costs can be mitigated.
The Xbox One CPU Introduction white paper lists the cost of a cache miss at >120 cycles versus the cost of a cache hit at 3 cycles. During these cycles the CPU core is not performing dependent work from those accesses. The speculative execution and Reorder Buffer can hide some latency, but not all, depending on cost.
The Xbox One CPU: Branch Prediction white paper lists the cost for a branch mispredict at a minimum of 14 cycles. This is due to the CPU core having to cancel all speculative execution and start filling the pipeline from scratch.
Depending on your title, each of these issues could be the difference between running at 30 frames per second versus running at 60 frames per second.
Each cache line is treated as a single 64-byte block of memory. Because of this, a title can have either real sharing or false sharing between data.
In real sharing, the plan is for the title to share a piece of data between cores. The shared data is known ahead of time and work can be done to mitigate the performance impact. The main way to mitigate the cost is careful control on when the data is accessed.
False sharing can be more insidious and harder to detect. Two different pieces of data sit next to each other in memory and thus will share a cache line. Because of this, any write to one will automatically invalidate the cache line for the other object. This is acceptable and beneficial when the code is executing on a single core. However, when code is executing on different CPU cores, you will get the data-contention issue mentioned previously.
You should very carefully map out what data is going to be shared between CPU cores and what data is not. For data that is shared between CPU cores, we highly recommend that each data item is unique per cache line to avoid any false sharing with other data.
Each core on the Xbox One CPU has a set of built-in counters called Performance Monitor Counters (PMCs) that track a wide variety of statistics. The following four counters supply the information needed to locate cache and branch issues:
The easiest place to see any PMC value is through PIX. PIX contains the ability to display a real-time graph of the current values in System Monitor along specific values in various captures.
The primary PMC to use for a holistic idea on how the title is doing is a composite PMC called Clocks Per Instruction (CPI), the average number of clock cycles it takes to retire one instruction (the lower the number the better). A core can retire two micro-ops per clock cycle; most instructions are one micro-op. The other instructions will cause the CPI to rise above 0.5 because they are multiple micro-ops.
The first step is to enable PMC values in PIX and System Monitor by enabling Retired Instructions per second for each processor core. (See Figure 1.)
Figure 1. System Monitor showing Retired instructions per sec on each CPU core.

Each processor core runs at 1.75 GHz on Xbox One and Xbox One S, and at 2.3 GHz on Xbox One X. For each core, divide the frequency by the Retired Instructions per second. This number will be the Cycles Per Instruction (CPI) for the core. The average seen for shipping titles is approximately two. If the number is higher than two, the most likely cause is a data cache issue.
If the CPI is high only on one core, there may be a general cache miss, probably from a poor access pattern on data. If the CPI is high across multiple cores, there is a likelihood of data contention between threads. However, if the title has many threads that can float between cores, this could be a false positive of data-contention issues.
If the CPI numbers derived from System Monitor are high, the next step is to use either the Function Summary Capture command or the Callgraph Capture command. Function Summary Capture will show overall time spent per function along with a chosen set of PMC values. The Callgraph Capture command allows you to drill down through a function and its children.
This command is useful if problem areas are not known.
To locate general issues:
From System Monitor, select Options in the Function Summary Capture box to expand and see the options. Select PMCs for RetiredInstructions, DCacheMisses, BranchInstructions, and MispredictedBranch.
Start a Function Summary Capture. The default time of 15000 milliseconds is a good starting point. (See Figure 2.)
Figure 2. Function Summary Capture box from System Monitor.

Figure 3: Context menu showing composite counters

Figure 4. Counters view in both Function Summary and Callgraph displays.

This command is useful if the title has known locations in the code with issues.
To analyze known locations:
Figure 5. Callgraph Capture drop-down menu from System Monitor.

Figure 6. Context menu for a function in the Function Summary.

In the Callgraph window, select the counter display.
In the Counters drop-down list, start with the following options selected, both inclusive and exclusive: Duration (μs), ClocksPerInstruction, DataMiss_Weighted, InstructionsPerBranchMispredict, and Notes. (See Figure 7.)
Figure 7. Counters view in both Function Summary and Callgraph displays.

There are two very useful instruction trace strategies:
To capture instructions for a full frame or a specific function:
Make sure you are on a computer with at least 16 GB of memory; even more is preferable because instruction traces can generate a large amount of collected data.
From either the Function Summary or Callgraph window, select CPU Instruction Trace from the context menu of a high-level function that covers the entire frame. (See Figure 10.)
Figure 10. Starting an instruction trace from a function.

Another option: In System Monitor, select the Capture Specified Number of Instructions option in the CPU Instruction Trace box to indicate the approximate number of instructions per thread to capture. (See Figure 11.)
Figure 11. Full frame instruction trace started from the System Monitor.

The Trace Summary window is the main window showing overall statistics on how the title performed during the trace. The two blocks of data at the top give overall numbers. Below these blocks are quick links to the worst offending functions (See Figure 12.)
Figure 12. Trace Summary window showing overall cache statistics.

Clicking the address for any of the top offenders will display the assembly of the function in question. Columns for execution count and cache misses are shown per line. (See Figure 13.)
Figure 13. Instruction Table for myRand function from instruction trace.

The context menu of a function allows the option to view the source for the function. Columns for execution count and cache misses are shown per line of the source. (See Figure 14.)
Figure 14. Instruction Table with source overlaid for myRand function from instruction trace.

To locate issues at the instruction level:
Instruction trace supports the ability to view the areas of memory touched during the capture across the entire addressable range of your title.
Figure 15. Initial Memory Map screen.

Figure 16. Memory Map window with single function selected, showing highlight of addresses touched.

Figure 17. Memory Map window zoomed into the individual byte and cache line level.

Each block represents one byte of memory with larger blocks for each cache line.
You’re looking for areas where the full cache line is not being used.
For example, figure 17 shows many sections where parts of the cache line are not being used. This is a good indicator of poor layout of data within the program, which can lead to higher cache misses.
Select any function in the list to highlight which blocks are being accessed by this function and the corresponding source code.
Figure 18. Memory Map window showing individual bytes touched by a function.

In figure 18, you can see that only 12 bytes are being used, of the entire 64 bytes available in the cache line.
This might be a good spot to consider adjusting the packing of memory, especially if it was one of the poor cache-usage functions. Access that’s spread out across memory requires more cache lines to be touched, which directly relates to a higher cache miss rate.
Following these steps should put you well on your way to locating the cause of common data cache issues and branch misprediction issues. If any of your questions were not answered, please visit the Developer Forums. There is a wealth of information there along with people who can help answer your questions.