- Оригинальный адрес:medium.com/@говорю, чтобы съесть оленя/я…
- Оригинальный автор:Genchi Lu
- Адрес перевода:GitHub.com/вода Мел О/…
- Переводчик: хаки хаки
- Уровень переводчика ограничен, если есть ошибка в переводе или понимании, помогите указать
Прежде чем объяснять ложное совместное использование кеша, необходимо кратко описать, как работает кеш в архитектуре ЦП.
Наименьшая единица кеша в ЦП — это строка кеша (общий размер строки кеша в ЦП сегодня составляет 64 байта). Таким образом, когда ЦП считывает переменную из памяти, он будет считывать все переменные рядом с этой переменной. Рисунок 1 представляет собой простой пример:
Когда core1 считывает переменную a из памяти, он также считывает переменную b в кэш. (Кстати, я думаю, что основная причина, по которой ЦП считывает переменные пакетами из памяти, основана на теории пространственной локальности: когда ЦП обращается к переменной, он может вскоре прочитать переменную рядом с ней.) : Для теории пространственной локализации см.эта статья)
В этой архитектуре кэша есть проблема: если переменная существует в двух строках кэша на разных ядрах ЦП, как показано на рисунке 2:
Когда Core1 обновляет переменную A:
Когда core2 читает переменную b, кэш core2 пропускается, даже если переменная b не была изменена. Таким образом, core2 перезагружает все переменные в строке кэша из памяти, как показано на рисунке 4:
Это ложное совместное использование кеша: одно ядро ЦП, обновляющее переменную, заставляет другие ядра ЦП обновлять кеш. И все мы знаем, что чтение переменной процессора из кеша намного быстрее, чем чтение переменной из памяти. Таким образом, хотя эта переменная всегда присутствует в многоядерных процессорах, это может существенно повлиять на производительность.
Распространенным решением этой проблемы является заполнение кеша: заполнение бессмысленных переменных между переменными. Сделайте так, чтобы переменная занимала строку кэша ядра ЦП сама по себе, чтобы при обновлении других ядер другие переменные не заставляли это ядро перезагружать переменную из памяти.
Мы используем следующий код Go, чтобы кратко представить концепцию ложного совместного использования кеша.
Это структура с тремя переменными uint64,
type NoPad struct {
a uint64
b uint64
c uint64
}
func (myatomic *NoPad) IncreaseAllEles() {
atomic.AddUint64(&myatomic.a, 1)
atomic.AddUint64(&myatomic.b, 1)
atomic.AddUint64(&myatomic.c, 1)
}
Вот еще одна структура, я использую [8]uint64 для заполнения кеша:
type Pad struct {
a uint64
_p1 [8]uint64
b uint64
_p2 [8]uint64
c uint64
_p3 [8]uint64
}
func (myatomic *Pad) IncreaseAllEles() {
atomic.AddUint64(&myatomic.a, 1)
atomic.AddUint64(&myatomic.b, 1)
atomic.AddUint64(&myatomic.c, 1)
}
Затем напишите простой код для запуска теста:
func testAtomicIncrease(myatomic MyAtomic) {
paraNum := 1000
addTimes := 1000
var wg sync.WaitGroup
wg.Add(paraNum)
for i := 0; i < paraNum; i++ {
go func() {
for j := 0; j < addTimes; j++ {
myatomic.IncreaseAllEles()
}
wg.Done()
}()
}
wg.Wait()
}
func BenchmarkNoPad(b *testing.B) {
myatomic := &NoPad{}
b.ResetTimer()
testAtomicIncrease(myatomic)
}
func BenchmarkPad(b *testing.B) {
myatomic := &Pad{}
b.ResetTimer()
testAtomicIncrease(myatomic)
}
Результаты тестов с использованием MacBook Air 2014 года следующие:
$> go test -bench=.
BenchmarkNoPad-4 2000000000 0.07 ns/op
BenchmarkPad-4 2000000000 0.02 ns/op
PASS
ok 1.777s
Результаты тестов показывают, что он повышает производительность с 0,07 нс/операцию до 0,02 нс/операцию, что является большим улучшением.
Вы также можете проверить это на других языках, таких как Java, и я уверен, что вы получите такие же результаты.
Прежде чем применять это к своему коду, вы должны знать два важных момента:
- Убедитесь, что размер строки кэша ЦП в вашей системе: это связано с размером заполнения кэша, который вы используете.
- Заполнение большего количества переменных означает потребление большего количества ресурсов памяти. Запустите тесты в своем сценарии, чтобы убедиться, что такое потребление памяти того стоит.
Весь мой пример кодаGitHubначальство.