Оптимизация конвейерной обработки процессора и доступа к кешу - PullRequest
1 голос
/ 06 апреля 2019

Я пытаюсь создать интуитивное представление о том, как написать эффективный код, который минимизирует CPI (количество циклов на инструкцию) и минимизирует потери в кэш-памяти и производительность с привязкой к серверу. Я хочу понять, как взаимодействуют локальность данных и конвейерная обработка.

Я понимаю, что многие из этих вещей зависят от конкретного оборудования, и невозможно ответить с уверенностью. Тем не менее, я надеюсь, что некоторые разумные рекомендации о том, что «вероятно» произойдет на «типичном» настольном компьютере, используют программу, скомпилированную с помощью общего компилятора, такого как gcc или icpc и -O2.

Рассмотрим следующий (надуманный) код. Целью этого кода является создание различных сценариев для иллюстрации вопроса. Давайте предположим, что строка кэша составляет 64 байта. (edit) - Для пояснения, давайте предположим, что ни одна из этих переменных не находится ни в одном уровне кэша во время выполнения calc. В ответе правильно указано, что если они уже кэшированы, это повлияет на результат.

class MyClass {
public:
    MyClass() {};
    inline void calc(const double in);
private:
    double x,y[10],z[32],a,b;
};

inline void MyClass:calc(const double in) 
{
    x = 5 + in;
    y[0] = 10 + in;
    z[0] = 25 + in;
    a = 50 + in;
    q = 100 + in;//q is a variable from global scope that is not already in the cache
    *pq = 200 + in;//*pq is a pointer from global scope that is not already in the cache
    q2 = 300 + in;//q2 is a variable from global scope that is not already in the cache
    b = 400 + in;
    cout << x << ", " << y[0] << ", " << z[0] << ", " << a << ", " << q << ", " << *pq << ", " << q2 << "," << b;
}

Когда запустится calc, x и y [0], вероятно, находятся в одной строке кэша, поэтому y [0] будет доступен с попаданием в кэш? z [0] находится на следующей строке кэша. Тем не менее, он может извлечь выгоду из предварительной выборки «следующая строка кэша», а также стать хитом кэша? a находится на расстоянии нескольких строк кэша, а затем q является переменной из глобальной области видимости, которая находится в некотором удаленном месте в памяти. Даже если a - это несколько строк кэша от z [0], следует ли ожидать, что он будет загружен в процессор быстрее, чем q? Возможно, будет какая-то предварительная выборка на более высоком уровне кеша, которая, возможно, предотвратит полное отсутствие кеша? q, безусловно, потребует извлечения из основной памяти, так как это из удаленного места в памяти. * pq и q также потребуют отдельного извлечения из основной памяти.

Так что я ожидаю, что что-то подобное произойдет: y [0] будет загружаться с попаданием в кэш L1, z [0] может загружаться с попаданием в кэш L1 или L2, может быть или не быть попаданием в кэш L2, и q определенно будет отсутствовать в кеше. Что если q настолько далеко, что это также приводит к пропаданию кэша TLB? Тогда это будет еще медленнее? Правильно ли мое понимание всего этого?

Как конвейер влияет на это? Процессор может передавать серию загрузок памяти, передавая q из основной памяти в кэш до завершения предыдущей строки кода. Таким образом, на практике, мы бы наблюдали замедление от использования переменной q, которая находится в удаленном месте в памяти?

Обратите внимание, что calc встроен, поэтому его инструкции могут составлять часть большей цепочки операций в вызывающей его функции, которая, как я полагаю, помогла бы с конвейерной обработкой.

Как переменная * pq влияет на конвейеризацию? Компилятор не знает, является ли * pq указателем, указывающим на q2 или на b. Повлияет ли это на эффективность конвейерной обработки?

Наконец, мы приходим к б. Он находится на той же строке кэша, что и. Нам пришлось сделать несколько вещей с тех пор, как мы в последний раз использовали a, но, надеюсь, он все еще находится в кеше L1 и является хитом? Опять же, может ли использование указателя * pq (которое может указывать на b) повлиять на оптимизацию здесь?

1 Ответ

1 голос
/ 06 апреля 2019

Я постараюсь ответить на ваши вопросы.

Компилятор может выравнивать объекты MyClass более чем на 8, особенно если они находятся в статической памяти, поэтому x и y [0] могут находиться в одной строке кэша. Большинство компиляторов будут выравнивать большие объекты больше, чем маленькие.

Если объект MyClass объявлен локально, он будет сохранен в стеке. В этом случае вполне вероятно, что весь объект находится в кэше L1.

z [0] может быть предварительно выбран аппаратно, но, возможно, недостаточно рано.

Первые пять строк могут быть выполнены не по порядку, потому что они независимы. Это означает, что любая потеря кэша в одной строке не будет замедлять следующие строки.

Вы правы в том, что * pq = что-то предотвращает выполнение не по порядку, потому что (в общем случае) компилятор не знает, является ли * pq псевдонимом некоторых других переменных.

'a' не обязательно загружается быстрее, чем 'q'. Например, если они оба находятся в кэше уровня 2, они будут загружаться одинаково быстро. Это зависит не от расстояния, а от времени с момента последнего прикосновения. Мисс TLB или граница страницы могут, конечно, влиять на время выборки, если оба находятся в основном ОЗУ, а q далеко.

b останется в кэше, если он находится в той же строке кэша, что и a, но вы не сможете получить доступ к b, пока адрес * pq не будет разрешен и не найден псевдоним на b.

Вставка функции calc здесь не имеет значения, если мы предположим, что кеширование данных является узким местом, а кеширование кода - нет.

...