Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.
Оптимизация программ для 
современных процессоров и Linux 
Крижановский Александр 
ak@natsys-lab.com
Окружение 
● Много ядер => много потоков (или процессов) 
(общая тенденция к росту числа CPU) 
● 2 типа памяти: быстрый RA...
Пример
Декомпозиция 
● Функциональная декомпозиция 
● Декомпозиция по данным
Декомпозиция по данным 
Original: 
for (int i = 0; i < 10; ++i) 
foo(i); 
Thread 1: Thread 2: 
for (int i = 0; i < 5; ++i)...
Функциональная декомпозиция 
Original: 
foo(); 
bar(); 
Thread 1: Thread 2: 
boo(); bar(); 
● Пример: поток ввода, поток в...
Процессы vs Потоки 
● Один и тот же task_struct, один и тот же do_fork() 
● Потоки разделяют память, а процессы – нет. 
● ...
Потоки – много или мало? 
● Современные планировщики ОС обрабатывают N kernel-thread'ов 
за O(1) или O(log(N)) время 
● Ke...
Cache hit 
(X – single thread, Y - multi-thread) 
● Multi-thread cache hit всегда <= 
single thread 
● Single thread cache...
Синхронизация 
● pthread_mutex_lock() - только один поток владеет ресурсом 
● pthread_rwlock_wrlock()/pthread_rwlock_rdloc...
Pthreads & futex(2) 
static volatile int val = 0; 
void lock() { 
int c; 
while ((c = __sync_fetch_and_add(&val, 1)) 
lll_...
pthread_cond_broadcast: 
гремящее стадо 
● Cache line bouncing для разделяемых данных 
● Атомарные операции и, тем более, ...
Инвалидация кэшей на мьютексе 
● Если поток не может захватить мьютекс, то он уходит в сон 
● => на текущем процессоре буд...
pthread_spin_lock (simplified) 
static volatile int lock = 0; 
void lock() { 
while (!__sync_bool_compare_and_swap(&lock, ...
pthread_spin_lock (scheduled) 
CPU0 CPU1 
Thread0: lock() . . . 
Thread0: some work Thread1: try lock() → loop 
. . . Thre...
pthread_rwlock 
● “дороже” pthread_mutex (больше сама структура данных ~ в 1.5 
раза, сложнее операция взятия лока) 
● Сни...
Lock contention 
● Один процесс удерживает лок, другие процессы ждут — 
система становится однопроцессной 
● Актуален с ув...
Lock upgrade 
pthread_rwlock_rdlock(&lock); 
if (v > shared) { 
pthread_rwlock_unlock(&lock); 
return; 
} 
pthread_rwlock_...
Big Reader Lock 
Задача: очень дешевое чтение, очень дорогая запись 
Решение: Массив per-cpu спин-локов: на чтение захвати...
Lock Batching (Lock Coarsening) 
Задача: для обработки каждого пакета нужно захватить лок. 
Решение: захватывать лок на ка...
Software Transactional Memory 
● Атомарные операции над несколькими областями памяти (~ 
транзакции в БД) 
● Принцип работ...
Intel Haswell TSX 
● Быстрее spin-lock'ов на: 
● малых транзакциях (до 32 кэш линеек (2KB)) 
● коротких транзакциях (??) 
...
TSX vs Spin Lock: Transaction Size
TSX vs Spin Lock: Transaction Time
Иерархия кэшей (SMP)
Иерархия кэшей (NUMA)
Типы кэшей 
● Data cache (L1d, L2d) 
● Instruction cache (L1i, L2i) 
● TLB – значения преобразований виртуальных адресов с...
Cache Lookup (x86-64) 
● L1: VIPT (Virtually Indexed 
Physically Tagged) 
● L2, L3: PIPT (Physically Indexed 
Physically T...
Page Table
getconf 
# getconf LEVEL1_DCACHE_SIZE 
65536 
# getconf LEVEL1_DCACHE_LINESIZE 
64 
# grep -c processor /proc/cpuinfo 
2
Когерентность кэшей 
● Непротиворечивость кэшированных данных на 
многопроцессорных системах 
● Обеспечивается протоколом ...
Пример: std::shared_ptr 
● std::shared_ptr использует reference counter – целую переменную, 
разделяемую и модифицируемую ...
False sharing 
● MESI оперирует cacheline'ами 
● RFO довольно дорогая операция 
● Если две различные переменные находятся ...
Выравнивание 
● Компилятор автоматически выравнивает элементы структур 
данных 
● GCC имеет специальные атрибуты, управляю...
Структуры данных: цена доступа 
● Основное узкое место: время обращения к памяти 
● По мере роста структур, данные переста...
Структуры данных & page table 
Application Tree 
Page table a b 
a.0 
c.0 
a.1 
c.1 
c
Массив 
● Бинарный поск 4х байт в странице (4KB) ~x10 быстрее линейного 
сканирования (for loop) 
● Бинарный поск 4х байт ...
Список 
● Неинтрузивный (классический список, двойная аллокация) 
struct list { 
struct list *next; 
void *data; 
} 
● Инт...
Radix-tree 
● Гарантированность времени доступа 
● На практике использует больше всего памяти 
● плохая утилизация кэш лин...
Бинарные деревья 
● В целом, слишком много обращений к памяти 
● Балансировка может быть довольно дорогой
{B,T}-tree 
● Хорошая пространственная локальность 
● Как правило, время поиска можно считать константным для RAM-only 
ст...
Хэш 
● Как правило, обладает хорошим средним временем доступа 
● На практике использует меньше всего памяти 
● Хорошая про...
Хэш: оптимизация (1) 
● Двойное хэширование 
● Определение размера на старте, как процент от доступной 
памяти (без динами...
Хэш: оптимизация (2)
Снижение lock contention 
(иерархические блокировки) 
pthread_mutex_lock(&hash_table_lock); 
Bucket *b0 = table_ + hash_1(...
Одновременный захват двух блокировок 
(deadlock) 
while (1) { 
pthread_mutex_lock(&b0->mtx_); 
if (b0 == b1) 
break; 
stru...
CPU Binding 
Бывает двух видов: 
● Процессов 
● Прерываний 
Служит для оптимизации работы 
кэшей процессоров
NUMA Interconnect (AMD, 2009)
NUMA 
● Раньше [только] AMD, 
теперь и Intel i7 (QPI) 
root@c460:~# numactl --hardware 
available: 4 nodes (0-3) 
node 0 c...
Привязка прерываний 
● APIC балансирует нагрузку между свободными ядрами (вообще-то не 
особо) 
● Irqbalance умеет привязы...
Пример перегрузки прерываниями 
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, ...
Привязка прерываний 
# cat /proc/irq/18/smp_affinity 
3 
# echo 1 > /proc/irq/18/smp_affinity 
# cat /proc/irq/18/smp_affi...
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 ...
MSI-X (пример оптимизации) 
● MSI-X распределяет пакеты по хэшу <protocol, src_ip, 
src_port, dst_ip, dst_port> (хотя это ...
Процессы 
● Часто кэши процессора разделяются ядрами (L2, L3) 
● Шины между ядрами одного процессора заметно быстрее шины ...
Привязка процессов 
● Для процессов 
sched_setaffinity(pid_t pid, size_t cpusetsize, 
cpu_set_t *mask) 
● Для потоков 
pth...
Пример (каналы памяти между пэкеджами и 
ядрами) 
# dd if=/dev/zero count=2000000 bs=8192 | nc 10.10.10.10 7700 
163840000...
Upcoming SlideShare
Loading in …5
×

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

1,420 views

Published on

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

Published in: Internet
  • Be the first to comment

  • Be the first to like this

Оптимизация программ для современных процессоров и 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!)

×