Оптимизация программ для 
современных процессоров и Linux 
Крижановский Александр 
ak@natsys-lab.com
Окружение 
● Много ядер => много потоков (или процессов) 
(общая тенденция к росту числа CPU) 
● 2 типа памяти: быстрый RAM и медленный диск 
=> используется кэширование 
● NUMA: доступ к памяти другого процессора сильно дороже 
(кластер внутри машины) 
● Между потоками разделяются одна или более структур данных
Пример
Декомпозиция 
● Функциональная декомпозиция 
● Декомпозиция по данным
Декомпозиция по данным 
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.
Функциональная декомпозиция 
Original: 
foo(); 
bar(); 
Thread 1: Thread 2: 
boo(); bar(); 
● Пример: поток ввода, поток вывода, рабочий поток, поток 
реконфигурации и пр. 
● Код – тоже данные (декомпозиция по данным) 
● Не масштабируется.
Процессы vs Потоки 
● Один и тот же task_struct, один и тот же do_fork() 
● Потоки разделяют память, а процессы – нет. 
● Процессы: 
● pros: меньше раздедяемых данных (выше параллельность), 
выше отказоустойчивость 
● cons: тяжелее программировать 
PostgreSQL: процессы, shared memory для buffer pool, блокировки 
тоже в shared memory (еще примеры: Apache, Nginx)
Потоки – много или мало? 
● Современные планировщики ОС обрабатывают N kernel-thread'ов 
за O(1) или O(log(N)) время 
● Kernel-thread ~ process: тяжелое создание и ~6 страниц памяти => 
нужен пул тредов 
● M потоков на один CPU => ухудшается cache hit 
(поток должен завершить текущую задачу и только потом перейти 
к следующей)
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”
Синхронизация 
● pthread_mutex_lock() - только один поток владеет ресурсом 
● pthread_rwlock_wrlock()/pthread_rwlock_rdlock() - один писатель 
или много читателей 
● pthread_cond_wait() - ожидать наступления события 
● pthread_spin_lock() - проверяет блокировку в цикле 
● (первые 3 работают через futex(2))
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); 
}
pthread_cond_broadcast: 
гремящее стадо 
● Cache line bouncing для разделяемых данных 
● Атомарные операции и, тем более, системный вызов – дорогие 
операции 
● Поэтому лучше использовать pthread_cond_signal()
Инвалидация кэшей на мьютексе 
● Если поток не может захватить мьютекс, то он уходит в сон 
● => на текущем процессоре будет запущен другой поток 
● => этот поток “вымоет” L2/L3 кэш и инвалидирует L1 
● Когда мьютекс будет отпущен, то поток продолжит выполнение с 
“вымытым” кэшем или на другом процессоре
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; 
}
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()
pthread_rwlock 
● “дороже” pthread_mutex (больше сама структура данных ~ в 1.5 
раза, сложнее операция взятия лока) 
● Снижает lock contention при превалировании числа читателей 
над писателями, но снижается производительность per-cpu
Lock contention 
● Один процесс удерживает лок, другие процессы ждут — 
система становится однопроцессной 
● Актуален с увеличением числа вычислительных ядер (или 
потоков исполнения) и числом блокировок в программе 
● Признак: ресурсы сервера используются слабо (CPU, IO etc), но 
число RPS невысокий 
● Методы борьбы: увеличение гранулярности блокировок, 
использование более легких методов синхронизации, lock-free
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);
Big Reader Lock 
Задача: очень дешевое чтение, очень дорогая запись 
Решение: Массив per-cpu спин-локов: на чтение захватить только 
локальный лок, на запись - все 
Снижается cache line bouncing. Блокировка на запись слишком 
дорогая и может заблокировать надолго читателей. 
Пример: Linux kernel VFS mount (давно)
Lock Batching (Lock Coarsening) 
Задача: для обработки каждого пакета нужно захватить лок. 
Решение: захватывать лок на каждые N пакетов и обрабатывать их 
за раз. 
Только для модели pull (можем за раз вычитать несколько пакетов 
из сокета или очереди). 
Пример: обработаь за раз накопленные IP фрагменты или Out Of 
Order TCP сегменты.
Software Transactional Memory 
● Атомарные операции над несколькими областями памяти (~ 
транзакции в БД) 
● Принцип работы: через хэш по адресу переменной определяется 
заблокированна ли область памяти и если нет, то блокируется 
● => высокий memory footprint и плохой cache hit 
● GCC-4.7 (программная реализация) 
● Intel Haswell (аппаратная реализация)
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
TSX vs Spin Lock: Transaction Size
TSX vs Spin Lock: Transaction Time
Иерархия кэшей (SMP)
Иерархия кэшей (NUMA)
Типы кэшей 
● Data cache (L1d, L2d) 
● Instruction cache (L1i, L2i) 
● TLB – значения преобразований виртуальных адресов страниц в 
физические (L1, L2) 
● L3 часто бывает смешанного типа 
● Чем опасен TLB cache miss (опять conext switch...)?
Cache Lookup (x86-64) 
● L1: VIPT (Virtually Indexed 
Physically Tagged) 
● L2, L3: PIPT (Physically Indexed 
Physically Tagged) 
VIPT: инвалидация кэша на 
context switch
Page Table
getconf 
# getconf LEVEL1_DCACHE_SIZE 
65536 
# getconf LEVEL1_DCACHE_LINESIZE 
64 
# grep -c processor /proc/cpuinfo 
2
Когерентность кэшей 
● Непротиворечивость кэшированных данных на 
многопроцессорных системах 
● Обеспечивается протоколом MESI (Modified, Exclusive, Shared, 
Invalid) 
● RFO (Request For Ownership) := M → I 
– CPU1 пишет по адресу X: X → M 
– CPU2 пишет по адресу X: 
–CPU1: X → I 
–CPU2: X → M
Пример: std::shared_ptr 
● std::shared_ptr использует reference counter – целую переменную, 
разделяемую и модифицируемую всеми потоками
False sharing 
● MESI оперирует cacheline'ами 
● RFO довольно дорогая операция 
● Если две различные переменные находятся в одном cacheline, то 
возникает RFO
Выравнивание 
● Компилятор автоматически выравнивает элементы структур 
данных 
● GCC имеет специальные атрибуты, управляющие выравниванием 
данных
Структуры данных: цена доступа 
● Основное узкое место: время обращения к памяти 
● По мере роста структур, данные перестают помещаться в кэши 
(L1d, L2d, L3d, TLB L1d/L2d etc) 
● Выход их TLB (~1024 страницы) может стоить до 4х обращений к 
памяти вместо одного 
=> Основные критерии к структурам данных: 
● малый объем вспомогательных данных 
● пространственная локальность обращений 
● cache oblivious или conscious структуры данных
Структуры данных & page table 
Application Tree 
Page table a b 
a.0 
c.0 
a.1 
c.1 
c
Массив 
● Бинарный поск 4х байт в странице (4KB) ~x10 быстрее линейного 
сканирования (for loop) 
● Бинарный поск 4х байт в кэш линейке (64B) ~x2 быстрее 
линейного сканирования (foor loop) 
● Бинарный поск 4х байт в кэш линейке (64B) ~x2 медленнее 
линейного сканирования (scas) 
Сканировная в общем случае медленные, но хорошо поддаютя 
оптимизации.
Список 
● Неинтрузивный (классический список, двойная аллокация) 
struct list { 
struct list *next; 
void *data; 
} 
● Интрузивный (лучшая локальность данных, меньше аллокаций) 
struct foo { 
struct foo *next; 
// other members 
}
Radix-tree 
● Гарантированность времени доступа 
● На практике использует больше всего памяти 
● плохая утилизация кэш линеек (один указатель на 64B) 
Очень медленное: на тесте равномерного распределения IPv4 
адресов почти в 2 раза проигрывает хэшу с простой хэш- 
функцией времени и 4 раза по памяти 
(Linux VMM выбирает ключи для сохранения пространственной 
локальности)
Бинарные деревья 
● В целом, слишком много обращений к памяти 
● Балансировка может быть довольно дорогой
{B,T}-tree 
● Хорошая пространственная локальность 
● Как правило, время поиска можно считать константным для RAM-only 
структур данных 
● Довольно дорогие вставки 
● Иногда медленнее хэшей 
● плохая утилизация кэш линеек (бинарный поиск на странице) 
● Отлично работает для систем фильтрации (когда дерево 
стоится один раз на старте)
Хэш 
● Как правило, обладает хорошим средним временем доступа 
● На практике использует меньше всего памяти 
● Хорошая пространственная локальность: 1 случайное обращение 
и линейное сканирование 
● Тяжело выбрать достаточно хорошую хэш-функцию (зависит от 
ключей и нагрузки) 
● В некоторых случаях может обладать очень большим временем 
поиска
Хэш: оптимизация (1) 
● Двойное хэширование 
● Определение размера на старте, как процент от доступной 
памяти (без динамического рехэшинга) 
● Просто повысить гранулярность блокировок
Хэш: оптимизация (2)
Снижение 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);
Одновременный захват двух блокировок 
(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_); 
}
CPU Binding 
Бывает двух видов: 
● Процессов 
● Прерываний 
Служит для оптимизации работы 
кэшей процессоров
NUMA Interconnect (AMD, 2009)
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
Привязка прерываний 
● APIC балансирует нагрузку между свободными ядрами (вообще-то не 
особо) 
● Irqbalance умеет привязывать прерывания в зависимости от 
процессорной топологии и текущей нагрузки 
● Не всегда следует привязывать прерывания руками 
● Прерывание обрабатывается локальным softirq, прикладной 
процесс мигрирует на этот же CPU
Пример перегрузки прерываниями 
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 на ядро + некоторая 
прикладная логика на пакет)
Привязка прерываний 
# cat /proc/irq/18/smp_affinity 
3 
# echo 1 > /proc/irq/18/smp_affinity 
# cat /proc/irq/18/smp_affinity 
1
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 
…................
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 (балансирует лучше)
Процессы 
● Часто кэши процессора разделяются ядрами (L2, L3) 
● Шины между ядрами одного процессора заметно быстрее шины 
между процессорами 
=> 
● Для улучшения cache hit имеет смысл создавать не больше 
тредов, чем физических ядер процессора 
● Рабочие потоки (разделяющие кэш) лучше привязывать к ядрам 
одного процессора
Привязка процессов 
● Для процессов 
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)
Пример (каналы памяти между пэкеджами и 
ядрами) 
# 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!)

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

  • 1.
    Оптимизация программ для современных процессоров и Linux Крижановский Александр [email protected]
  • 2.
    Окружение ● Многоядер => много потоков (или процессов) (общая тенденция к росту числа CPU) ● 2 типа памяти: быстрый RAM и медленный диск => используется кэширование ● NUMA: доступ к памяти другого процессора сильно дороже (кластер внутри машины) ● Между потоками разделяются одна или более структур данных
  • 3.
  • 4.
    Декомпозиция ● Функциональнаядекомпозиция ● Декомпозиция по данным
  • 5.
    Декомпозиция по данным 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.
  • 6.
    Функциональная декомпозиция Original: foo(); bar(); Thread 1: Thread 2: boo(); bar(); ● Пример: поток ввода, поток вывода, рабочий поток, поток реконфигурации и пр. ● Код – тоже данные (декомпозиция по данным) ● Не масштабируется.
  • 7.
    Процессы vs Потоки ● Один и тот же task_struct, один и тот же do_fork() ● Потоки разделяют память, а процессы – нет. ● Процессы: ● pros: меньше раздедяемых данных (выше параллельность), выше отказоустойчивость ● cons: тяжелее программировать PostgreSQL: процессы, shared memory для buffer pool, блокировки тоже в shared memory (еще примеры: Apache, Nginx)
  • 8.
    Потоки – многоили мало? ● Современные планировщики ОС обрабатывают N kernel-thread'ов за O(1) или O(log(N)) время ● Kernel-thread ~ process: тяжелое создание и ~6 страниц памяти => нужен пул тредов ● M потоков на один CPU => ухудшается cache hit (поток должен завершить текущую задачу и только потом перейти к следующей)
  • 9.
    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”
  • 10.
    Синхронизация ● pthread_mutex_lock()- только один поток владеет ресурсом ● pthread_rwlock_wrlock()/pthread_rwlock_rdlock() - один писатель или много читателей ● pthread_cond_wait() - ожидать наступления события ● pthread_spin_lock() - проверяет блокировку в цикле ● (первые 3 работают через futex(2))
  • 11.
    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); }
  • 12.
    pthread_cond_broadcast: гремящее стадо ● Cache line bouncing для разделяемых данных ● Атомарные операции и, тем более, системный вызов – дорогие операции ● Поэтому лучше использовать pthread_cond_signal()
  • 13.
    Инвалидация кэшей намьютексе ● Если поток не может захватить мьютекс, то он уходит в сон ● => на текущем процессоре будет запущен другой поток ● => этот поток “вымоет” L2/L3 кэш и инвалидирует L1 ● Когда мьютекс будет отпущен, то поток продолжит выполнение с “вымытым” кэшем или на другом процессоре
  • 14.
    pthread_spin_lock (simplified) staticvolatile 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; }
  • 15.
    pthread_spin_lock (scheduled) CPU0CPU1 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()
  • 16.
    pthread_rwlock ● “дороже”pthread_mutex (больше сама структура данных ~ в 1.5 раза, сложнее операция взятия лока) ● Снижает lock contention при превалировании числа читателей над писателями, но снижается производительность per-cpu
  • 17.
    Lock contention ●Один процесс удерживает лок, другие процессы ждут — система становится однопроцессной ● Актуален с увеличением числа вычислительных ядер (или потоков исполнения) и числом блокировок в программе ● Признак: ресурсы сервера используются слабо (CPU, IO etc), но число RPS невысокий ● Методы борьбы: увеличение гранулярности блокировок, использование более легких методов синхронизации, lock-free
  • 18.
    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);
  • 19.
    Big Reader Lock Задача: очень дешевое чтение, очень дорогая запись Решение: Массив per-cpu спин-локов: на чтение захватить только локальный лок, на запись - все Снижается cache line bouncing. Блокировка на запись слишком дорогая и может заблокировать надолго читателей. Пример: Linux kernel VFS mount (давно)
  • 20.
    Lock Batching (LockCoarsening) Задача: для обработки каждого пакета нужно захватить лок. Решение: захватывать лок на каждые N пакетов и обрабатывать их за раз. Только для модели pull (можем за раз вычитать несколько пакетов из сокета или очереди). Пример: обработаь за раз накопленные IP фрагменты или Out Of Order TCP сегменты.
  • 21.
    Software Transactional Memory ● Атомарные операции над несколькими областями памяти (~ транзакции в БД) ● Принцип работы: через хэш по адресу переменной определяется заблокированна ли область памяти и если нет, то блокируется ● => высокий memory footprint и плохой cache hit ● GCC-4.7 (программная реализация) ● Intel Haswell (аппаратная реализация)
  • 22.
    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
  • 23.
    TSX vs SpinLock: Transaction Size
  • 24.
    TSX vs SpinLock: Transaction Time
  • 25.
  • 26.
  • 27.
    Типы кэшей ●Data cache (L1d, L2d) ● Instruction cache (L1i, L2i) ● TLB – значения преобразований виртуальных адресов страниц в физические (L1, L2) ● L3 часто бывает смешанного типа ● Чем опасен TLB cache miss (опять conext switch...)?
  • 28.
    Cache Lookup (x86-64) ● L1: VIPT (Virtually Indexed Physically Tagged) ● L2, L3: PIPT (Physically Indexed Physically Tagged) VIPT: инвалидация кэша на context switch
  • 29.
  • 30.
    getconf # getconfLEVEL1_DCACHE_SIZE 65536 # getconf LEVEL1_DCACHE_LINESIZE 64 # grep -c processor /proc/cpuinfo 2
  • 31.
    Когерентность кэшей ●Непротиворечивость кэшированных данных на многопроцессорных системах ● Обеспечивается протоколом MESI (Modified, Exclusive, Shared, Invalid) ● RFO (Request For Ownership) := M → I – CPU1 пишет по адресу X: X → M – CPU2 пишет по адресу X: –CPU1: X → I –CPU2: X → M
  • 32.
    Пример: std::shared_ptr ●std::shared_ptr использует reference counter – целую переменную, разделяемую и модифицируемую всеми потоками
  • 33.
    False sharing ●MESI оперирует cacheline'ами ● RFO довольно дорогая операция ● Если две различные переменные находятся в одном cacheline, то возникает RFO
  • 34.
    Выравнивание ● Компиляторавтоматически выравнивает элементы структур данных ● GCC имеет специальные атрибуты, управляющие выравниванием данных
  • 35.
    Структуры данных: ценадоступа ● Основное узкое место: время обращения к памяти ● По мере роста структур, данные перестают помещаться в кэши (L1d, L2d, L3d, TLB L1d/L2d etc) ● Выход их TLB (~1024 страницы) может стоить до 4х обращений к памяти вместо одного => Основные критерии к структурам данных: ● малый объем вспомогательных данных ● пространственная локальность обращений ● cache oblivious или conscious структуры данных
  • 36.
    Структуры данных &page table Application Tree Page table a b a.0 c.0 a.1 c.1 c
  • 37.
    Массив ● Бинарныйпоск 4х байт в странице (4KB) ~x10 быстрее линейного сканирования (for loop) ● Бинарный поск 4х байт в кэш линейке (64B) ~x2 быстрее линейного сканирования (foor loop) ● Бинарный поск 4х байт в кэш линейке (64B) ~x2 медленнее линейного сканирования (scas) Сканировная в общем случае медленные, но хорошо поддаютя оптимизации.
  • 38.
    Список ● Неинтрузивный(классический список, двойная аллокация) struct list { struct list *next; void *data; } ● Интрузивный (лучшая локальность данных, меньше аллокаций) struct foo { struct foo *next; // other members }
  • 39.
    Radix-tree ● Гарантированностьвремени доступа ● На практике использует больше всего памяти ● плохая утилизация кэш линеек (один указатель на 64B) Очень медленное: на тесте равномерного распределения IPv4 адресов почти в 2 раза проигрывает хэшу с простой хэш- функцией времени и 4 раза по памяти (Linux VMM выбирает ключи для сохранения пространственной локальности)
  • 40.
    Бинарные деревья ●В целом, слишком много обращений к памяти ● Балансировка может быть довольно дорогой
  • 41.
    {B,T}-tree ● Хорошаяпространственная локальность ● Как правило, время поиска можно считать константным для RAM-only структур данных ● Довольно дорогие вставки ● Иногда медленнее хэшей ● плохая утилизация кэш линеек (бинарный поиск на странице) ● Отлично работает для систем фильтрации (когда дерево стоится один раз на старте)
  • 42.
    Хэш ● Какправило, обладает хорошим средним временем доступа ● На практике использует меньше всего памяти ● Хорошая пространственная локальность: 1 случайное обращение и линейное сканирование ● Тяжело выбрать достаточно хорошую хэш-функцию (зависит от ключей и нагрузки) ● В некоторых случаях может обладать очень большим временем поиска
  • 43.
    Хэш: оптимизация (1) ● Двойное хэширование ● Определение размера на старте, как процент от доступной памяти (без динамического рехэшинга) ● Просто повысить гранулярность блокировок
  • 44.
  • 45.
    Снижение 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);
  • 46.
    Одновременный захват двухблокировок (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_); }
  • 47.
    CPU Binding Бываетдвух видов: ● Процессов ● Прерываний Служит для оптимизации работы кэшей процессоров
  • 48.
  • 49.
    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
  • 50.
    Привязка прерываний ●APIC балансирует нагрузку между свободными ядрами (вообще-то не особо) ● Irqbalance умеет привязывать прерывания в зависимости от процессорной топологии и текущей нагрузки ● Не всегда следует привязывать прерывания руками ● Прерывание обрабатывается локальным softirq, прикладной процесс мигрирует на этот же CPU
  • 51.
    Пример перегрузки прерываниями 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 на ядро + некоторая прикладная логика на пакет)
  • 52.
    Привязка прерываний #cat /proc/irq/18/smp_affinity 3 # echo 1 > /proc/irq/18/smp_affinity # cat /proc/irq/18/smp_affinity 1
  • 53.
    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 …................
  • 54.
    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 (балансирует лучше)
  • 55.
    Процессы ● Частокэши процессора разделяются ядрами (L2, L3) ● Шины между ядрами одного процессора заметно быстрее шины между процессорами => ● Для улучшения cache hit имеет смысл создавать не больше тредов, чем физических ядер процессора ● Рабочие потоки (разделяющие кэш) лучше привязывать к ядрам одного процессора
  • 56.
    Привязка процессов ●Для процессов 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)
  • 57.
    Пример (каналы памятимежду пэкеджами и ядрами) # 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!)