Найти в Дзене
allaboutknit.ru

Как намеренно вызвать false sharing в Go — и почему ваш многопоточный код может тормозить в 2–20×

Покажу простой пример на Go: как сделать код, который специально страдает от false sharing, и как это быстро исправить. False sharing — это когда разные горутины работают с разными переменными, но эти переменные лежат в одной и той же cache line (обычно 64 байта). В результате процессоры тратят время на синхронизацию и инвалидирование кэш-линий — и программа неожиданно тормозит. Ниже — короткий и наглядный эксперимент на Go. Мы сделаем две версии: Затем сравним время выполнения. package main import (
"fmt"
"runtime"
"sync"
"sync/atomic"
"time" ) type Counters struct {
A int64
B int64 } func main() {
runtime.GOMAXPROCS(4) // ставим несколько ОС-потоков
var c Counters
var wg sync.WaitGroup
const goroutines = 4
const iters = 50_000_000
start := time.Now()
// Запускаем writers: половина пишет в A, половина — в B
for i := 0; i < goroutines; i++ {
wg.Add(1)
if i%2 == 0 {
go func() {
defer wg.Done()
for j := 0; j < iters; j++ {
atomic.AddInt64(&c.A, 1)
Оглавление

Покажу простой пример на Go: как сделать код, который специально страдает от false sharing, и как это быстро исправить.

False sharing — это когда разные горутины работают с разными переменными, но эти переменные лежат в одной и той же cache line (обычно 64 байта). В результате процессоры тратят время на синхронизацию и инвалидирование кэш-линий — и программа неожиданно тормозит. Ниже — короткий и наглядный эксперимент на Go.

Идея эксперимента (коротко)

Мы сделаем две версии:

  1. Проблемная — два счётчика расположены рядом в памяти (в одном cache line). Много горутин каждую итерацию увеличивают эти счётчики.
  2. Исправленная — между счётчиками вставлен паддинг, чтобы они оказались в разных cache line.

Затем сравним время выполнения.

Важные детали перед запуском

  • Запускай на многопроцессорной машине (GOMAXPROCS >= 2).
  • Для бенчей использую простую time-метрику (можно и testing.B, но тут нагляднее простой runnable-пример).
  • False sharing особенно заметен при большом числе итераций и нескольких горутинах-писателях.

Код — пример с false sharing (намеренно «сломанный»)

package main
import (
"fmt"
"runtime"
"sync"
"sync/atomic"
"time"
)
type Counters struct {
A int64
B int64
}
func main() {
runtime.GOMAXPROCS(4) // ставим несколько ОС-потоков
var c Counters
var wg sync.WaitGroup

const goroutines = 4
const iters = 50_000_000

start := time.Now()

// Запускаем writers: половина пишет в A, половина — в B
for i := 0; i < goroutines; i++ {
wg.Add(1)
if i%2 == 0 {
go func() {
defer wg.Done()
for j := 0; j < iters; j++ {
atomic.AddInt64(&c.A, 1)
}
}()
} else {
go func() {
defer wg.Done()
for j := 0; j < iters; j++ {
atomic.AddInt64(&c.B, 1)
}
}()
}
}

wg.Wait()
elapsed := time.Since(start)
fmt.Printf("A=%d B=%d time=%v\n", c.A, c.B, elapsed)
}

Что делает код: много горутин интенсивно увеличивают два поля A и B, которые, скорее всего, окажутся в одном cache line → будет false sharing.

Код — исправленный вариант с паддингом

package main
import (
"fmt"
"runtime"
"sync"
"sync/atomic"
"time"
)
// Размер кэш-линии обычно 64 байта. Структура ниже ставит поля в разные линии.
type PaddedCounters struct {
// Паддинг так, чтобы A занял свою линию
_ [56]byte
A int64
_ [56]byte
B int64
_ [56]byte
}
func main() {
runtime.GOMAXPROCS(4)
var c PaddedCounters
var wg sync.WaitGroup

const goroutines = 4
const iters = 50_000_000

start := time.Now()

for i := 0; i < goroutines; i++ {
wg.Add(1)
if i%2 == 0 {
go func() {
defer wg.Done()
for j := 0; j < iters; j++ {
atomic.AddInt64(&c.A, 1)
}
}()
} else {
go func() {
defer wg.Done()
for j := 0; j < iters; j++ {
atomic.AddInt64(&c.B, 1)
}
}()
}
}

wg.Wait()
elapsed := time.Since(start)
fmt.Printf("A=%d B=%d time=%v\n", c.A, c.B, elapsed)
}

Идея: вставляем паддинг (примерно 56 байт — т.к. int64 ещё 8 байт → в сумме 64) между полями, чтобы они оказались в разных 64-байтных блоках.

Как запускать и что ждать

1 Скомпилируй и запусти сначала «сломанный» пример, затем исправленный.

2 Пример:

go run broken.go
go run padded.go

3 На реальной машине исправленный вариант обычно работает значительно быстрее (разница зависит от CPU, числа ядер и нагрузки — от ~2× до десятков раз).

Почему это работает (коротко и по существу)

  • CPU оперирует cache line (обычно 64 байта) целиком.
  • Когда одно ядро записывает в cache line, другие ядра должны получить уведомление и/или инвалидировать свои копии → синхронизация по шине (MESI/MOESI).
  • Даже если горутины пишут в разные переменные внутри одной линии, аппаратная когерентность вынуждает делать «пересылку» всей линии — вот false sharing.
  • Решение: разнести часто записываемые поля так, чтобы они не попадали в одну cache line (паддинг) или уменьшить конкуренцию (менее частые обновления, шардинг, batching).

Дополнительные способы вызвать false sharing (намеренно)

  • Малые структуры с множеством полей-счетчиков, которые активно пишутся разными потоками.
  • Интенсивные атомарные операции (atomic.Add*, atomic.Store*) по соседним адресам.
  • Большое число одновременно работающих потоков/горутин (GOMAXPROCS > 1).
  • Компактные буферы/карты, где несколько горутин пишут в элементы, расположенные подряд.

Как избежать false sharing в реальном коде

  • Добавлять паддинг (как в примере).
  • Использовать []struct{ ... } с выравниванием так, чтобы элементы были на отдельных линиях.
  • Разделять данные по шардам (sharding): каждый поток пишет в свою часть.
  • Минимизировать частые атомарные операции на общих адресах (агрегировать).
  • Для Go: использовать runtime/pprof, perf, go test -bench и профилирование, чтобы найти горячие точки.

Небольшая оговорка

Паддинг — «грубый» инструмент: он увеличивает память. Применять его стоит там, где критична скорость и где профайлер показал проблему. False sharing — аппаратный эффект, и его влияние зависит от CPU/архитектуры и реальной нагрузки.

Заключение

False sharing — легко воспроизводимая и хорошо объяснимая проблема производительности. С простым примером на Go можно показать читателю «магическое» торможение и столь же простое решение. Такой материал хорошо заходить на Дзене: понятный заголовок, демонстрация «до/после», код и конкретные инструкции по запуску.