Successfully reported this slideshow.
Your SlideShare is downloading. ×

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

Ad
Ad
Ad
Ad
Ad
Ad
Ad
Ad
Ad
Ad

Check these out next

1 of 57 Ad

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

Download to read offline

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

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

Advertisement
Advertisement

More Related Content

Slideshows for you (20)

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

Advertisement

More from Ontico (20)

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

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

×