tgoop.com/unsafecsharp/228
Create:
Last Update:
Last Update:
Cache Line и Cache Miss
На самом деле это две разные штуки, хоть и имеют схожие названия.
Я постараюсь объяснить просто и понятно, но так, чтобы не опускаться на самый низкий уровень.
Cache Line. Давайте представим, что у вас есть 2 GameObject: на одном есть скрипт Processor, а на втором скрипт - RAM.
В скрипте RAM давайте объявим массив объектов:
public class RAM : MonoBehaviour {
public object[] objects;
}
А у скрипта Processor одно поле
public class Processor : MonoBehaviour {
public object currentObject;
}
Мы хотим, чтобы процессор обрабатывал какой-то объект, мы можем сделать это двумя путями:
1. Как в примере выше: просто объявляем ссылку на объект;
2. Процессор будет только хранить только информацию о том как этот объект получить, но сам хранить ничего не будет.
Какой вариант будет работать быстрее? Очевидно, что первый.
Вот примерно так и работает Cache Line, где currentObject - это не объект, а просто определенного размера кэш. Он один раз загружается из RAM и используется до тех пор, пока не потребуется какой-то другой участок памяти. Поэтому если читать последовательно (например, из массива), то будет задействован кэш процессора, а не оперативка. Отсюда в названии "Line".
Размер кэш линии зависит от процессора, в основном 32, 64 или 128 байт.
Cache Miss. Это вытекает из первого. По сути это момент, когда мы не попадаем в cache line. Рассмотрим пример:
var arr = new int[1000];
for (int i = 0; i < arr.Length; ++i) {
arr[i] // обращаемся к массиву
}
При первом обращении к массиву мы получаем Cache Miss и загрузку Cache Line, т.к. нам нужно загрузить данные из памяти. Когда мы доходим до N-го элемента, мы снова делаем новую загрузку и снова получаем Cache Miss. Каждая загрузка занимает существенное время, поэтому минимизация количества Cache Miss дает буст в производтельности.
Поэтому код вида:
var arr2 = new int[arr.Length];
for (int i = 0; i < arr.Length; ++i) {
arr[i] // обращаемся к массиву
arr2[i] // обращаемся ко второму массиву
}
Будет постоянно выдавать Cache Miss, т.к. мы загружаем Cache Line заново. На практике вы, конечно, вряд ли заметите разницу, т.к. все же этот процесс довольно быстрый, но при большом количестве обращений в хот частях может дать замедление.
Можно переписать код примерно так:
struct MyStruct {
int a1;
int a2;
}
var arr = new MyStruct[1000];
for (int i = 0; i < arr.Length; ++i) {
arr[i].a1 // обращаемся к массиву и получаем a1
arr[i].a2 // обращаемся к массиву и получаем a2
}
То есть мы просто положили 2 переменные рядом в памяти и теперь мы будем получать Cache Miss как и в первом примере, но в 2 раза чаще, т.к. данных на один элемент у нас теперь х2.
Многопоточность. С этим есть некоторые нюансы: каждый поток использует свою кэш линию (каждый честный поток). Наша задача сделать так, чтобы данные не пересекались.
Пример:
arr = new int[10]; // Допустим, мы делаем 10 потоков
Thread(int threadIndex) {
++arr[threadIndex]; // обращаемся к своему индексу для каждого потока
}
Такой код занимается увеличением счетчика без блокирования потока. Но тут и есть проблема, которая приведет к тому, что мы потеряем производительность на мердже Cache Line. То есть когда в проц загружается Cache Line - мы по сути делаем "лок" на эту область памяти. И когда второй проц заберет ту же кэш линию - будет конфликт интересов. По факту это, конечно, разрулится, и данные в итоге будут нужные, но вот производительность мы потеряем.
Что делать? Можно просто расширить массив arr с 10 до 10 * CacheLineSize / sizeof(int). Т.е. мы под каждый поток выделяем область памяти, которая никак не пересекается с другим потоком.
Таким образом, первый поток будет писать в arr[0], второй - в arr[1 * CacheLineSize / sizeof(int)] и т.д.
Т.е. при размере кэш линии в 128 байт, у нас размер массива для 10 потоков будет 320, а потоки будут использовать индексы: 0, 32, 64 и т.д. (с шагом 32, где 32 * sizeof(int) = 128 размер кэш линии).
#cache #memory #cachemiss #cacheline
BY Unity: Всё, что вы не знали о разработке
Share with your friend now:
tgoop.com/unsafecsharp/228