aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/arch/cache.md
blob: 8bda1052dbf7531a39c9dcd1489fd00708ff233e (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
# cache

Caches are organized by the following components
- `sets`
- `ways`
- `entries`

Each `set` consists of one or more `ways` and  a `way` is a single slot which
can hold an `entry`.
```
S-set / W-way cache

         +----------------- .. -----------+
SET 0    | WAY 0 | WAY 1 |      | WAY W-1 |
         +----------------- .. -----------+
SET 1    | WAY 0 | WAY 1 |      | WAY W-1 |
         +----------------- .. -----------+
..       |                                |
         +----------------- .. -----------+
SET S-1  | WAY 0 | WAY 1 |      | WAY W-1 |
         +----------------- .. -----------+
```

In general a cache is described by the number of `sets S` and the number of
`ways W`. Depending on the values for `S` and `W` caches can be further
classified.
- `W=1` is a `direct-mapped` cache, which means that each entry can be placed
  at exactly **ONE** location in the cache. It is also called a _one-way set
  associative_ cache.
- `S>1 & W>1` is a `W-way set associative` cache, which consists of S sets where
  each set consists of W ways. Each entry maps to a **UNIQUE** set, but to
  **ANY** way in that set.
- `S=1` is a `fully-associative` cache, which means that each entry can be
  placed at **ANY** location in the cache.

To determine which set an entry falls into, a `hash function` is applied on the
`key` which is associated with the entry. The set is then given by applying the
modulo operation to the hash value `hash % num_sets`.

The following figure illustrates the different cache classes and gives an
example which entries the given hash value `5` can map to.
```
direct-mapped      2-way set associative      fully-associative

HASH=5 (IDX=5%4)   HASH=5 (IDX=5%4)           HASH=5 (only one IDX)
|                  |                          |
|    S=4, W=1      |   S=4, W=2               |   S=1, W=4
|    +--------+    |   +--------+--------+    |   +--------+--------+--------+--------+
|   0|        |    |  0|        |        |    `->0| xxxxxx | xxxxxx | xxxxxx | xxxxxx |
|    +--------+    |   +--------+--------+        +--------+--------+--------+--------+
`- >1| xxxxxx |    `->1| xxxxxx | xxxxxx |
     +--------+        +--------+--------+
    2|        |       2|        |        |
     +--------+        +--------+--------+
    3|        |       3|        |        |
     +--------+        +--------+--------+
```

## CPU (hardware) caches
The number of sets in a hardware cache is usually a power of two. The `address`
acts as the key and some bits in the address are used to select the set in the
cache. The hash function in this case is simple, as it just extracts the bits
from the address which are used to select the set.

The `address` is usually split up into the `{ TAG, IDX, OFF }` bits which are
used to lookup an entry in the cache.

The `IDX` bits are used to index into the corresponding set, where the `TAG`
bits are then compared against the stored `TAG` bits in each way. If any way
holds an entry with the matching `TAG` bits, the lookup is a `HIT`, else a
`MISS`.

In case the entry is in the cache, the `OFF` bits are used to index into the
cache line. Hence, the number of offset bits available define the cache line
size.

The following gives an example for _64-bit addresses_ and a _direct-mapped_ cache.
```
        63                      0
        +-----------------------+
ADDR:   |  TAG  |  IDX  |  OFF  |
        +-----------------------+
            |       |       `------------------,
            |       |                          |
            |       |    CACHE                 |
            |       |    +----------------+    |
            |       |    | TAG | CACHE_LN |    |
            |       |    +----------------+    |
            |       |    | TAG | CACHE_LN |    |
            |       |    +----------------+    |
            |       |    | ..             |    |
            |       |    +----------------+    |
            |       `--> | TAG | CACHE_LN |    |
            |            +----------------+    |
            |               |     |            |
            |               v     v            |
            `-------------> =     + <----------`
                            |     |
                            v     v
                           HIT?  DATA


OFF bits: ln2 (cache_line_sz)
IDX bits: ln2 (num_sets)
TAG bits: 64 - IDX bits - OFF bits
```

The total size of a cache can be computed by `cache_line_sz * num_sets * num_ways`.
```
Example
  SETS: 64        => 6 IDX bits
  WAYS: 8
  LINE: 64 bytes  => 6 OFF bits

  SIZE: 64 sets * 8 ways * 64 bytes => 32k bytes
```

## Hardware caches with virtual memory
In the context of _virtual memory_, caches can be placed at different location
in the memory path, either _before_ or _after_ the `virtual address (VA)` to
`physical address (PA)` translation. Each placement has different properties
discussed in the following.

If the cache is placed _before_ the `VA -> PA` translation, it is called
`virtually indexed virtually tagged (VIVT)` cache, as it is indexed by a virtual
address and data in the cache is tagged with the virtual address as well.

The benefit of VIVT caches is that lookups are very fast as there is no need to
wait for the result of the address translation. However, VIVT caches may suffer
from the following problems.
- `synonyms`: different VAs map to the same PA. This can happen in a single
  address space (same page table), if for example a process maps the same file
  at different VAs (also commonly referred to as _aliasing_ or _cache-line
  sharing_). This can also happen in different address spaces (different page
  tables), if for example pages are shared between two processes.
  ```
  PT1
  +-------+
  |       |        PHYSMEM          PT2
  +-------+        +-------+        +-------+
  |  VA1  |---,    |       |        |       |
  +-------+   |    +-------+        +-------+
  |       |   +--->|  PA1  |<-------|  VA3  |
  +-------+   |    +-------+        +-------+
  |  VA2  |---`    |       |        |       |
  +-------+        +-------+        +-------+
  |       |
  +-------+

  Assume VA1 != VA2 != VA3

  CACHE
   TAG     DATA
  +-------+-------------+        Problems:
  |  VA1  | Copy of PA1 |        * multiple copies of the same data.
  |  VA3  | Copy of PA1 |        * write through one VA and read through a
  |       |             |          different VA results in reading stale data.
  |  VA2  | Copy of PA1 |
  +-------+-------------+
  ```
- `homonyms`: same VA corresponds to different PAs. This is the standard case
  between two different address spaces (eg in a multi-tasking os), for example
  if the same VA is used in two different processes, but it maps to a different
  PA for each process.
  ```
  PT1              PHYSMEM          PT2
  +-------+        +-------+        +-------+
  |  VA1  |------->|  PA1  |    ,---|  VA2  |
  +-------+        +-------+    |   +-------+
  |       |        |       |    |   |       |
  |       |        +-------+    |   |       |
  |       |        |  PA2  |<---`   |       |
  +-------+        +-------+        +-------+

  Assume VA1 == VA2

  CACHE
   TAG     DATA
  +-------+-------------+        Problems:
  |  VA1  | Copy of PA1 |        * same VA from different address spaces map to
  |       |             |          different PA
  |       |             |        * read thorugh VA2 returns data from PA1
  +-------+-------------+          rather than from PA2
  ```

While `synonyms` may lead to accessing _stale_ data, if there is no hardware to
guarantee coherency between aliased entries, `homonyms` may lead to accessing
the _wrong_ data.

On one hand there are multiple counter measures to avoid `homonyms`, for example
physical tagging, tags could contain an address space identifier (ASID), or the
cache could be flushed on context switches (changing the page table).
Approaches like physical tagging and ASIDs work, as the same VA always maps to
the same index in the cache, which would then result in a cache miss in case of
the homonym.

Preventing `synonyms` on the other hand is harder, as neither physical tagging
nor ASIDs help in this case. Flushing the cache during context switches only
helps with the case where different address spaces alias shared pages, but it
won't help if the same PA is aliased by different VAs in a single address space.
There are to alternative approaches, one is to have hardware support to detect
synonyms and the other one is to have the operating system only allow shared
mappings with VAs that have the same index bits for the cache. However, the
latter only works for direct-mapped caches, as there is only a single location
where those VAs could map to in the cache.

If the cache is placed _after_ the `VA -> PA` translation, it is called
`physically indexed physically tagged (PIPT)` cache, as it is indexed by a
physical address and data in the cache is tagged with the physical address as
well.

Compared to VIVT caches, PIPT caches do not suffer from `synonyms` or
`homonyms`. However, their major drawback is that the lookup depends on the
result of the address translation, and hence the translation and the cache
lookup happen sequentially which greatly decreases access latency.

Between VIVT and PIPT caches there is also a hybrid approach called `virtually
indexed physically tagged (VIPT)` cache, where the cache lookup is done with a
virtual address and the data is tagged with the physical address.

The benefit of this approach is that the cache lookup and the address
translation can be done in parallel, and due to the physical tagging, `homonyms`
are not possible.

For VIPT caches, `synonyms` may still happen depending on how the cache is
constructed.
- if the `index` bits for the cache lookup, exceed the `page offset` in the
  virtual address, then `synonyms` are still possible.
- if all the `index` bits for the cache lookup fall into the `page offset` of
  the virtual address, then the bits used for the cache lookup won't change
  during the `VA -> PA` translation, and hence the cache effectively operates as
  a PIPT cache. The only downside is that the number of sets in the cache is
  limited by the page size.

### VIPT as PIPT example
The following example shows that for a system with `4k` pages and cache lines of
`64 bytes` a VIPT cache can have at most `64 sets` to still act as PIPT cache.
```
      63      12              0
      +-----------------------+
VA:   |       |     PG_OFF    |
      +-----------------------+
CACHE BITS:   | C_IDX | C_OFF |
              +---------------+

PAGE SIZE  : 4k
PAGE OFFSET: ln (PAGE SIZE) = 12 bits

CACHE LINE  : 64 bytes
CACHE OFFSET: ln (CACHE LINE) = 6 bits

CACHE INDEX: PG_OFF - C_OFF = 6 bits
CACHE SETS : 2^CACHE INDEX = 64 sets
```
The total cache size can be increased by adding additional ways, however that
also has a practical upper limit, as adding more ways reduces the latency.

## Cache info in Linux
```sh
# Info about different caches (size, ways, sets, type, ..).
lscpu -C
# NAME ONE-SIZE ALL-SIZE WAYS TYPE        LEVEL SETS PHY-LINE COHERENCY-SIZE
# L1d       32K     128K    8 Data            1   64        1             64
# L1i       32K     128K    8 Instruction     1   64        1             64
# L2       256K       1M    4 Unified         2 1024        1             64
# L3         6M       6M   12 Unified         3 8192        1             64

# Info about how caches are shared between cores / hw-threads. Identified by
# the same cache ids on the same level.
lscpu -e
# CPU  CORE  L1d:L1i:L2:L3  ONLINE
#   0     0  0:0:0:0           yes
#   1     1  1:1:1:0           yes
#   4     0  0:0:0:0           yes
#   5     1  1:1:1:0           yes
#
# => CPU 0,4 share L1d, L1i, L2 caches (here two hw-threads of a core).
```