Оптимизация программ для современных процессоров и...

57
Оптимизация программ для современных процессоров и Linux Крижановский Александр [email protected]

Upload: ontico

Post on 15-Jun-2015

465 views

Category:

Internet


9 download

DESCRIPTION

Доклад Александра Крижановского с HighLoad++ 2014.

TRANSCRIPT

Page 1: Оптимизация программ для современных процессоров и Linux, Александр Крижановский (NatSys Lab)

Оптимизация программ для современных процессоров и Linux

Крижановский Александр[email protected]

Page 2: Оптимизация программ для современных процессоров и Linux, Александр Крижановский (NatSys Lab)

Окружение

● Много ядер => много потоков (или процессов)(общая тенденция к росту числа CPU)

● 2 типа памяти: быстрый RAM и медленный диск=> используется кэширование

● NUMA: доступ к памяти другого процессора сильно дороже(кластер внутри машины)

● Между потоками разделяются одна или более структур данных

Page 3: Оптимизация программ для современных процессоров и Linux, Александр Крижановский (NatSys Lab)

Пример

Page 4: Оптимизация программ для современных процессоров и Linux, Александр Крижановский (NatSys Lab)

Декомпозиция

● Функциональная декомпозиция● Декомпозиция по данным

Page 5: Оптимизация программ для современных процессоров и Linux, Александр Крижановский (NatSys Lab)

Декомпозиция по данным

Original:

for (int i = 0; i < 10; ++i)

foo(i);

Thread 1: Thread 2:

for (int i = 0; i < 5; ++i) for (int i = 5; i < 10; ++i)

foo(i); foo(i);

● Пример: рабочие потоки, “разгребающие” очередь задач● Хорошо масштабируется с ростом CPU и на NUMA.

Page 6: Оптимизация программ для современных процессоров и Linux, Александр Крижановский (NatSys Lab)

Функциональная декомпозиция

Original:

foo();

bar();

Thread 1: Thread 2:

boo(); bar();

● Пример: поток ввода, поток вывода, рабочий поток, поток реконфигурации и пр.

● Код – тоже данные (декомпозиция по данным)● Не масштабируется.

Page 7: Оптимизация программ для современных процессоров и Linux, Александр Крижановский (NatSys Lab)

Процессы vs Потоки

● Один и тот же task_struct, один и тот же do_fork()● Потоки разделяют память, а процессы – нет.● Процессы:

● pros: меньше раздедяемых данных (выше параллельность), выше отказоустойчивость

● cons: тяжелее программировать

PostgreSQL: процессы, shared memory для buffer pool, блокировки тоже в shared memory (еще примеры: Apache, Nginx)

Page 8: Оптимизация программ для современных процессоров и Linux, Александр Крижановский (NatSys Lab)

Потоки – много или мало?

● Современные планировщики ОС обрабатывают N kernel-thread'ов за O(1) или O(log(N)) время

● Kernel-thread ~ process: тяжелое создание и ~6 страниц памяти => нужен пул тредов

● M потоков на один CPU => ухудшается cache hit(поток должен завершить текущую задачу и только потом перейти к следующей)

Page 9: Оптимизация программ для современных процессоров и Linux, Александр Крижановский (NatSys Lab)

Cache hit(X – single thread, Y - multi-thread)

● Multi-thread cache hit всегда <= single thread

● Single thread cache hit < 55% => много-поточность всегда имеет смысл

● Чем страшен context switch?

Ulrich Drepper, “What Every Programmer Should Know about Memory”

Page 10: Оптимизация программ для современных процессоров и Linux, Александр Крижановский (NatSys Lab)

Синхронизация

● pthread_mutex_lock() - только один поток владеет ресурсом● pthread_rwlock_wrlock()/pthread_rwlock_rdlock() - один писатель

или много читателей● pthread_cond_wait() - ожидать наступления события● pthread_spin_lock() - проверяет блокировку в цикле● (первые 3 работают через futex(2))

Page 11: Оптимизация программ для современных процессоров и Linux, Александр Крижановский (NatSys Lab)

Pthreads & futex(2)

static volatile int val = 0;

void lock() {

int c;

while ((c = __sync_fetch_and_add(&val, 1))

lll_futex_wait(&val, c + 1);

}

void unlock() {

val = 0;

lll_futex_wake(&val, 1);

}

Page 12: Оптимизация программ для современных процессоров и Linux, Александр Крижановский (NatSys Lab)

pthread_cond_broadcast:гремящее стадо

● Cache line bouncing для разделяемых данных● Атомарные операции и, тем более, системный вызов – дорогие

операции● Поэтому лучше использовать pthread_cond_signal()

Page 13: Оптимизация программ для современных процессоров и Linux, Александр Крижановский (NatSys Lab)

Инвалидация кэшей на мьютексе

● Если поток не может захватить мьютекс, то он уходит в сон● => на текущем процессоре будет запущен другой поток● => этот поток “вымоет” L2/L3 кэш и инвалидирует L1● Когда мьютекс будет отпущен, то поток продолжит выполнение с

“вымытым” кэшем или на другом процессоре

Page 14: Оптимизация программ для современных процессоров и Linux, Александр Крижановский (NatSys Lab)

pthread_spin_lock (simplified)

static volatile int lock = 0;

void lock() {

while (!__sync_bool_compare_and_swap(&lock, 0, 1))

asm volatile(“pause” ::: “memory”);

// asm volatile(“rep; nop” ::: “memory”);

}

void unlock() {

lock = 0;

}

Page 15: Оптимизация программ для современных процессоров и Linux, Александр Крижановский (NatSys Lab)

pthread_spin_lock (scheduled)

CPU0 CPU1

Thread0: lock() . . .

Thread0: some work Thread1: try lock() → loop

. . . Thread2: try lock() → loop // Thread0 preempted . . . (loop) . . .

Thread1: try lock() → loop Thread2: try lock() → loop

// 200% CPU usage

. . . // Thread2 preempted

. . . Thread0: unlock()

Ядро для этого использует preempt_disable()

Page 16: Оптимизация программ для современных процессоров и Linux, Александр Крижановский (NatSys Lab)

pthread_rwlock

● “дороже” pthread_mutex (больше сама структура данных ~ в 1.5 раза, сложнее операция взятия лока)

● Снижает lock contention при превалировании числа читателей над писателями, но снижается производительность per-cpu

Page 17: Оптимизация программ для современных процессоров и Linux, Александр Крижановский (NatSys Lab)

Lock contention

● Один процесс удерживает лок, другие процессы ждут — система становится однопроцессной

● Актуален с увеличением числа вычислительных ядер (или потоков исполнения) и числом блокировок в программе

● Признак: ресурсы сервера используются слабо (CPU, IO etc), но число RPS невысокий

● Методы борьбы: увеличение гранулярности блокировок, использование более легких методов синхронизации, lock-free

Page 18: Оптимизация программ для современных процессоров и Linux, Александр Крижановский (NatSys Lab)

Lock upgrade

pthread_rwlock_rdlock(&lock);

if (v > shared) {

pthread_rwlock_unlock(&lock);

return;

}

pthread_rwlock_unlock(&lock);

pthread_rwlock_wrlock(&lock);

if (v > shared) {

pthread_rwlock_unlock(&lock);

return;

}

shared = v;

pthread_rwlock_unlock(&lock);

Page 19: Оптимизация программ для современных процессоров и Linux, Александр Крижановский (NatSys Lab)

Big Reader Lock

Задача: очень дешевое чтение, очень дорогая запись

Решение: Массив per-cpu спин-локов: на чтение захватить только локальный лок, на запись - все

Снижается cache line bouncing. Блокировка на запись слишком дорогая и может заблокировать надолго читателей.

Пример: Linux kernel VFS mount (давно)

Page 20: Оптимизация программ для современных процессоров и Linux, Александр Крижановский (NatSys Lab)

Lock Batching (Lock Coarsening)

Задача: для обработки каждого пакета нужно захватить лок.

Решение: захватывать лок на каждые N пакетов и обрабатывать их за раз.

Только для модели pull (можем за раз вычитать несколько пакетов из сокета или очереди).

Пример: обработаь за раз накопленные IP фрагменты или Out Of Order TCP сегменты.

Page 21: Оптимизация программ для современных процессоров и Linux, Александр Крижановский (NatSys Lab)

Software Transactional Memory

● Атомарные операции над несколькими областями памяти (~ транзакции в БД)

● Принцип работы: через хэш по адресу переменной определяется заблокированна ли область памяти и если нет, то блокируется

● => высокий memory footprint и плохой cache hit● GCC-4.7 (программная реализация)● Intel Haswell (аппаратная реализация)

Page 22: Оптимизация программ для современных процессоров и Linux, Александр Крижановский (NatSys Lab)

Intel Haswell TSX

● Быстрее spin-lock'ов на:● малых транзакциях (до 32 кэш линеек (2KB))● коротких транзакциях (??)● пересекаемость данных не влияет - ?

● Glibc-2.17 уже использует для lock elision в pthread_mutex_lock()

Ссылки:● Andreas Kleen, “Modern Locking”● Nick Piggin, “kernel: inroduce brlock”● Studying Intel TSX: http://natsys-lab.blogspot.ru/2013/11/studying-intel-tsx-performance.html

Page 23: Оптимизация программ для современных процессоров и Linux, Александр Крижановский (NatSys Lab)

TSX vs Spin Lock: Transaction Size

Page 24: Оптимизация программ для современных процессоров и Linux, Александр Крижановский (NatSys Lab)

TSX vs Spin Lock: Transaction Time

Page 25: Оптимизация программ для современных процессоров и Linux, Александр Крижановский (NatSys Lab)

Иерархия кэшей (SMP)

Page 26: Оптимизация программ для современных процессоров и Linux, Александр Крижановский (NatSys Lab)

Иерархия кэшей (NUMA)

Page 27: Оптимизация программ для современных процессоров и Linux, Александр Крижановский (NatSys Lab)

Типы кэшей

● Data cache (L1d, L2d)● Instruction cache (L1i, L2i)● TLB – значения преобразований виртуальных адресов страниц в

физические (L1, L2)● L3 часто бывает смешанного типа

● Чем опасен TLB cache miss (опять conext switch...)?

Page 28: Оптимизация программ для современных процессоров и Linux, Александр Крижановский (NatSys Lab)

Cache Lookup (x86-64)

● L1: VIPT (Virtually Indexed Physically Tagged)

● L2, L3: PIPT (Physically Indexed Physically Tagged)

VIPT: инвалидация кэша на context switch

Page 29: Оптимизация программ для современных процессоров и Linux, Александр Крижановский (NatSys Lab)

Page Table

Page 30: Оптимизация программ для современных процессоров и Linux, Александр Крижановский (NatSys Lab)

getconf

# getconf LEVEL1_DCACHE_SIZE

65536

# getconf LEVEL1_DCACHE_LINESIZE

64

# grep -c processor /proc/cpuinfo

2

Page 31: Оптимизация программ для современных процессоров и Linux, Александр Крижановский (NatSys Lab)

Когерентность кэшей

● Непротиворечивость кэшированных данных на многопроцессорных системах

● Обеспечивается протоколом MESI (Modified, Exclusive, Shared, Invalid)

● RFO (Request For Ownership) := M → I

– CPU1 пишет по адресу X: X → M– CPU2 пишет по адресу X:

– CPU1: X → I– CPU2: X → M

Page 32: Оптимизация программ для современных процессоров и Linux, Александр Крижановский (NatSys Lab)

Пример: std::shared_ptr

● std::shared_ptr использует reference counter – целую переменную, разделяемую и модифицируемую всеми потоками

Page 33: Оптимизация программ для современных процессоров и Linux, Александр Крижановский (NatSys Lab)

False sharing

● MESI оперирует cacheline'ами● RFO довольно дорогая операция● Если две различные переменные находятся в одном cacheline, то

возникает RFO

Page 34: Оптимизация программ для современных процессоров и Linux, Александр Крижановский (NatSys Lab)

Выравнивание

● Компилятор автоматически выравнивает элементы структур данных

● GCC имеет специальные атрибуты, управляющие выравниванием данных

Page 35: Оптимизация программ для современных процессоров и Linux, Александр Крижановский (NatSys Lab)

Структуры данных: цена доступа

● Основное узкое место: время обращения к памяти ● По мере роста структур, данные перестают помещаться в кэши

(L1d, L2d, L3d, TLB L1d/L2d etc)● Выход их TLB (~1024 страницы) может стоить до 4х обращений к

памяти вместо одного

=> Основные критерии к структурам данных:● малый объем вспомогательных данных● пространственная локальность обращений● cache oblivious или conscious структуры данных

Page 36: Оптимизация программ для современных процессоров и Linux, Александр Крижановский (NatSys Lab)

Структуры данных & page table

a.0

c.0

a.1

c.1

Page tablea b

c

Application Tree

Page 37: Оптимизация программ для современных процессоров и Linux, Александр Крижановский (NatSys Lab)

Массив

● Бинарный поск 4х байт в странице (4KB) ~x10 быстрее линейного сканирования (for loop)

● Бинарный поск 4х байт в кэш линейке (64B) ~x2 быстрее линейного сканирования (foor loop)

● Бинарный поск 4х байт в кэш линейке (64B) ~x2 медленнее линейного сканирования (scas)

Сканировная в общем случае медленные, но хорошо поддаютя оптимизации.

Page 38: Оптимизация программ для современных процессоров и Linux, Александр Крижановский (NatSys Lab)

Список

● Неинтрузивный (классический список, двойная аллокация)

struct list { struct list *next; void *data; }

● Интрузивный (лучшая локальность данных, меньше аллокаций)

struct foo { struct foo *next; // other members }

Page 39: Оптимизация программ для современных процессоров и Linux, Александр Крижановский (NatSys Lab)

Radix-tree

● Гарантированность времени доступа● На практике использует больше всего памяти● плохая утилизация кэш линеек (один указатель на 64B)

Очень медленное: на тесте равномерного распределения IPv4 адресов почти в 2 раза проигрывает хэшу с простой хэш-функцией времени и 4 раза по памяти

(Linux VMM выбирает ключи для сохранения пространственной локальности)

Page 40: Оптимизация программ для современных процессоров и Linux, Александр Крижановский (NatSys Lab)

Бинарные деревья

● В целом, слишком много обращений к памяти● Балансировка может быть довольно дорогой

Page 41: Оптимизация программ для современных процессоров и Linux, Александр Крижановский (NatSys Lab)

{B,T}-tree

● Хорошая пространственная локальность● Как правило, время поиска можно считать константным для RAM-

only структур данных● Довольно дорогие вставки● Иногда медленнее хэшей● плохая утилизация кэш линеек (бинарный поиск на странице)● Отлично работает для систем фильтрации (когда дерево

стоится один раз на старте)

Page 42: Оптимизация программ для современных процессоров и Linux, Александр Крижановский (NatSys Lab)

Хэш

● Как правило, обладает хорошим средним временем доступа● На практике использует меньше всего памяти● Хорошая пространственная локальность: 1 случайное обращение

и линейное сканирование● Тяжело выбрать достаточно хорошую хэш-функцию (зависит от

ключей и нагрузки)● В некоторых случаях может обладать очень большим временем

поиска

Page 43: Оптимизация программ для современных процессоров и Linux, Александр Крижановский (NatSys Lab)

Хэш: оптимизация (1)

● Двойное хэширование● Определение размера на старте, как процент от доступной

памяти (без динамического рехэшинга)● Просто повысить гранулярность блокировок

Page 44: Оптимизация программ для современных процессоров и Linux, Александр Крижановский (NatSys Lab)

Хэш: оптимизация (2)

Page 45: Оптимизация программ для современных процессоров и Linux, Александр Крижановский (NatSys Lab)

Снижение lock contention(иерархические блокировки)

pthread_mutex_lock(&hash_table_lock);

Bucket *b0 = table_ + hash_1(new_key);

Bucket *b1 = table_ + hash_2(new_key);

// Initialize buckets, resize hashtable etc.

lock_2_buckets(b0, b1);

pthread_mutex_unlock(&hash_table_lock);

// Read/modify one of the buckets

unlock_2_buckets(b0, b1);

Page 46: Оптимизация программ для современных процессоров и Linux, Александр Крижановский (NatSys Lab)

Одновременный захват двух блокировок (deadlock)

while (1) {

pthread_mutex_lock(&b0->mtx_);

if (b0 == b1)

break;

struct timespec to;

to.tv_sec = 0;

to.tv_nsec = 10000000; // 0.01 sec

if (!pthread_mutex_timedlock(&b1->mtx_, &to))

break;

pthread_mutex_unlock(&b0->mtx_);

}

Page 47: Оптимизация программ для современных процессоров и Linux, Александр Крижановский (NatSys Lab)

CPU Binding

Бывает двух видов:● Процессов ● Прерываний

Служит для оптимизации работы кэшей процессоров

Page 48: Оптимизация программ для современных процессоров и Linux, Александр Крижановский (NatSys Lab)

NUMA Interconnect (AMD, 2009)

Page 49: Оптимизация программ для современных процессоров и Linux, Александр Крижановский (NatSys Lab)

NUMA

● Раньше [только] AMD, теперь и Intel i7 (QPI)

root@c460:~# numactl --hardware

available: 4 nodes (0-3)

node 0 cpus: 0 4 8 12 16 20 24 28 32 36

node 0 size: 163763 MB

node 0 free: 160770 MB

node 1 cpus: 2 6 10 14 18 22 26 30 34 38

node 1 size: 163840 MB

node 1 free: 160866 MB

node 2 cpus: 1 5 9 13 17 21 25 29 33 37

node 2 size: 163840 MB

node 2 free: 160962 MB

node 3 cpus: 3 7 11 15 19 23 27 31 35 39

node 3 size: 163840 MB

node 3 free: 160927 MB

node distances:

node 0 1 2 3

0: 10 21 21 21

1: 21 10 21 21

2: 21 21 10 21

3: 21 21 21 10

Page 50: Оптимизация программ для современных процессоров и Linux, Александр Крижановский (NatSys Lab)

Привязка прерываний

● APIC балансирует нагрузку между свободными ядрами (вообще-то не особо)

● Irqbalance умеет привязывать прерывания в зависимости от процессорной топологии и текущей нагрузки

● Не всегда следует привязывать прерывания руками● Прерывание обрабатывается локальным softirq, прикладной

процесс мигрирует на этот же CPU

Page 51: Оптимизация программ для современных процессоров и Linux, Александр Крижановский (NatSys Lab)

Пример перегрузки прерываниями

Cpu9 : 13.3%us, 62.1%sy, 0.0%ni, 1.0%id, 0.0%wa, 0.0%hi, 23.6%si, 0.0%st

Cpu10 : 0.0%us, 0.7%sy, 0.0%ni, 82.7%id, 0.0%wa, 0.0%hi, 16.6%si, 0.0%st

(Грубая оценка: cовременные x86-64 позволяют обрабатывать около 100 тыс пакетов в секунду/1Gbps на ядро + некоторая прикладная логика на пакет)

Page 52: Оптимизация программ для современных процессоров и Linux, Александр Крижановский (NatSys Lab)

Привязка прерываний

# cat /proc/irq/18/smp_affinity

3

# echo 1 > /proc/irq/18/smp_affinity

# cat /proc/irq/18/smp_affinity

1

Page 53: Оптимизация программ для современных процессоров и Linux, Александр Крижановский (NatSys Lab)

MSI-X (линии прерываний)root@c460:~# grep eth7 /proc/interrupts

214: 109437 131 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 IR-PCI-MSI-edge eth7

215: 0 2 3087484 0 0 0 0 164 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 IR-PCI-MSI-edge eth7-rx-0

…................

223: 1111160 0 8 0 0 0 0 164 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 IR-PCI-MSI-edge eth7-tx-0

224: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 IR-PCI-MSI-edge eth7-tx-1

…................

Page 54: Оптимизация программ для современных процессоров и Linux, Александр Крижановский (NatSys Lab)

MSI-X (пример оптимизации)

● MSI-X распределяет пакеты по хэшу <protocol, src_ip, src_port, dst_ip, dst_port> (хотя это никому не известно)

● 4 i7 процессора по 10 ядер:● Каждый процессор – независимый узел обработки● Выделяем по 1 ядру на обработку прерываний● Выделяем по 9 ядер на воркеров

=> ~ +20% производительности по сравнению с SMP

Linux 2.6.35: RPS (Receive Packet Steering) – программная реализация MSI-X (балансирует лучше)

Page 55: Оптимизация программ для современных процессоров и Linux, Александр Крижановский (NatSys Lab)

Процессы

● Часто кэши процессора разделяются ядрами (L2, L3)● Шины между ядрами одного процессора заметно быстрее шины

между процессорами

=> ● Для улучшения cache hit имеет смысл создавать не больше

тредов, чем физических ядер процессора ● Рабочие потоки (разделяющие кэш) лучше привязывать к ядрам

одного процессора

Page 56: Оптимизация программ для современных процессоров и Linux, Александр Крижановский (NatSys Lab)

Привязка процессов

● Для процессов

sched_setaffinity(pid_t pid, size_t cpusetsize, cpu_set_t *mask)

● Для потоков

pthread_setaffinity_np(pthread_t thread, size_t cpusetsize, const cpu_set_t *cpuset)

● Или можно использовать gettid(2)

Page 57: Оптимизация программ для современных процессоров и Linux, Александр Крижановский (NatSys Lab)

Пример (каналы памяти между пэкеджами и ядрами)

# dd if=/dev/zero count=2000000 bs=8192 | nc 10.10.10.10 7700

16384000000 bytes (16 GB) copied, 59.4648 seconds, 276 MB/s

# taskset 0x400 dd if=/dev/zero count=2000000 bs=8192 \

| taskset 0x200 nc 10.10.10.10 7700

16384000000 bytes (16 GB) copied, 39.8281 seconds, 411 MB/s

(И это 16-ядерный SMP!)