Почти все персональные компьютеры, выпущенные за последние несколько лет, обладают как минимум двухъядерным процессором. Если у тебя, читатель, не очень старый комп или не какой-нибудь бюджетный ноутбук, то, вероятнее всего, ты обладатель многопроцессорной системы. А если еще любишь играть в игры, то тебе доступно около сотни GPU-ядер. Однако львиную долю времени вся эта мощь пылится без дела. Попробуем это исправить.
Введение
Очень редко мы задействуем всю мощь нескольких процессоров (или ядер) для решения повседневных задач, а использование мощного графического процессора чаще всего сводится к компьютерным играм. Лично мне это не по душе: я ведь работаю — почему процессор должен отдыхать? Непорядок. Нужно взять на вооружение все возможности и преимущества многопроцессорников (многоядерников) и распараллеливать все, что можно, сэкономив себе этим уйму времени. А если еще и подключить к работе мощную видеоплату с сотней ядер на борту… которая, конечно, сгодится лишь для узкого круга задач, но все же ускорит вычисления весьма существенно.
Linux в этом плане очень силен. Во-первых, в большинстве дистрибутивов из коробки доступны хорошие инструменты для параллельного выполнения, а во-вторых, написано немало софта, спроектированного с учетом многоядерных систем. О гибкости настройки, думаю, даже не нужно говорить. Единственное, с чем могут возникнуть проблемы, — драйверы на видеокарту, но тут уж ничего не поделаешь.
Для начала определимся с методами распараллеливания. Их существует два: средствами самого приложения, которое для выполнения задачи запускает несколько параллельных потоков (multithreading). Другой метод заключается в запуске нескольких копий приложения, каждая из которых будет обрабатывать определенную порцию данных. Операционная система в данном случае самостоятельно распределит процессы по ядрам или процессорам (multitasking).
Распараллеливаем прямо в терминале
Начнем, пожалуй, с параллельного запуска процессов прямо из окна терминала. Запуск в терминале процессов, работающих длительное время, не представляет проблему. А что, если нужно два таких процесса? «Тоже не проблема, — скажешь ты, — просто запустим второй процесс в другом окне терминала». А если нужно запустить десять или больше? Хм… Первое, что приходит в голову, — использовать утилиту xargs. Если задать ей опцию —max-procs=n, то софтина будет выполнять n процессов одновременно, что нам, конечно же, на руку. Официальный мануал рекомендует использовать вместе с опцией —max-procs группировку по аргументам (опция -n): без этого возможны проблемы с параллельным запуском. Для примера представим, что нам нужно заархивировать кучу больших (или маленьких) файлов:
$ cd folder/with/files
$ ls | xargs -n1 --max-procs=4 gzip
Сколько времени мы выиграли? Стоит ли заморачиваться с этим? Тут будет уместно привести цифры. На моем четырехъядерном процессоре обычное архивирование пяти файлов, каждый из которых весил около 400 Мб, заняло 1 мин 40 с. С использованием же xargs --max-procs=4
затраченное время сократилось почти в четыре раза: 34 с! Думаю, ответ на вопрос очевиден.
Давай попробуем что-нибудь поинтереснее. Переконвертируем, например, WAV-файлы в MP3 с помощью lame:
$ ls *.wav | xargs -n1 --max-procs=4 -I {} lame {} -o {}.mp3
Выглядит неуклюже? Согласен. Но параллельное выполнение процессов — это не основная задача xargs, а всего лишь одна из ее возможностей. Кроме того, xargs не очень хорошо ведет себя с передачей специальных символов, как, например, пробел или кавычки. И тут нам на помощь приходит замечательная утилита под названием GNU Parallel. Софтина доступна в стандартных репозиториях, но не рекомендую устанавливать ее оттуда: в репозиториях Ubuntu, например, мне попалась версия двухлетней давности. Лучше скомпилить себе свежую версию с исходников:
$ wget ftp.gnu.org/gnu/parallel/parallel-latest.tar.bz2
$ tar xjf parallel-latest.tar.bz2
$ cd parallel-20130822
$ ./configure && make
<span class="comment"># make install</span>
Само название утилиты говорит о ее узкой специализации. Действительно, Parallel намного удобнее для распараллеливания, и ее использование выглядит более логично. Приведенный выше пример с применением Parallel вместо xargs превращается в такой:
$ ls *.wav | parallel lame {} -o {}.mp3
Кстати, если ты сидишь под Ubuntu или Kubuntu, то пример не будет работать, выдавая непонятные ошибки. Фиксится это добавлением ключа ‘—gnu’ (касается и следующего примера). Подробнее о проблеме читай здесь.
А почему мы не задаем количество одновременно выполняемых процессов? Потому что Parallel сделает это за нас: она определит количество ядер и будет запускать по процессу на ядро. Конечно, можно задать это число и вручную с помощью опции -j. Кстати, если нужно запускать задачу на разных машинах, то для улучшения переносимости удобно задавать эту опцию в формате -j +2, что в данном конкретном случае означает «запускать одновременно на два процесса больше, чем есть вычислительных юнитов в системе».
Если подружить Parallel c Python, то получим мощный инструмент для параллельного выполнения задач. Например, загрузка из Сети веб-страниц и их последующая обработка может выглядеть так:
$ python makelist.py | parallel -j+2 <span class="string">'wget "{}" -O - | python parse.py'</span>
Но и без Python возможностей у этой утилиты предостаточно. Обязательно почитай man — там очень много интересных примеров.
Кроме Parallel и xargs, конечно, есть еще уйма других утилит со схожим функционалом, но они не умеют ничего такого, чего не умеют первые две.
С этим разобрались. Двигаемся дальше.
Параллельная компиляция
Собирать что-то из исходников — обычное дело для линуксоида. Чаще всего приходится собирать что-то незначительное, для таких проектов никто не думает ни о какой параллельной компиляции. Но иногда попадаются проекты побольше, и ждать окончания сборки приходится почти вечность: сборка, например, Android (AOSP) из исходников (в один поток) длится около пяти часов! Для такого рода проектов нужно пускать в ход все ядра.
Во-первых, думаю, очевидно, что собственно сама компиляция (GCC, например) не распараллеливается. Но большие проекты чаще всего составлены из большого количества независимых модулей, библиотек и прочего, которые можно и нужно компилировать одновременно. Нам, конечно же, не надо думать о том, как распараллелить компиляцию, — об этом позаботится make, но только при условии, что в makefile будут прописаны зависимости. В противном случае make не будет знать, в какой последовательности собирать и что можно собирать одновременно, а что нельзя. И поскольку makefile — это забота разработчиков, для нас все сводится к выполнению команды
$ make -jN
после чего make начнет сборку проекта, одновременно запуская до N задач.
Кстати, о выборе значения для параметра -j. В Сети часто рекомендуют использовать число 1,5 * <количество вычислительных юнитов>. Но это не всегда верно. Например, сборка проекта общим весом 250 Мб на моем четырехъядернике быстрее всего прошла со значением параметра -j, равным четырем (смотри скрин).
Зависимость времени компиляции от значения параметра
Чтобы выиграть еще немного времени, можно добавить ключ ‘-pipe’ к GCC. С этим ключом передача данных между разными стадиями компиляции происходит через каналы обмена (pipes), а не через временные файлы, что немного (совсем немного) ускоряет процесс.
Кроме make, можно попробовать также pmake — систему параллельной сборки, написанную на Python. Для юзверей ее использование мало чем отличается от make, а вот для разработчиков она может быть довольно интересной, поскольку имеет более обширные возможности, чем стандартный инструмент.
Параллельный Rsync
Если ты когда-нибудь использовал Rsync для синхронизации огромного количества маленьких файлов с удаленным сервером, то, наверное, заметил приличную задержку на стадии receiving file list. Можно ли ускорить этот этап за счет распараллеливания? Конечно, можно. Много времени здесь уходит на задержки в работе сети. Чтобы минимизировать эти временные потери, мы запустим несколько копий Rsync, а чтобы не копировались одни и те же файлы — натравим каждую копию, например, на отдельный каталог. Для этого заюзаем комбинацию параметров —include и —exclude, например так:
$ rsync -av --include=<span class="string">"/a*"</span> --exclude=<span class="string">"/*"</span> -P login@server:remote /localdir/
$ rsync -av --include=<span class="string">"/b*"</span> --exclude=<span class="string">"/*"</span> -P login@server:remote /localdir/
Можно запустить вручную несколько копий в разных терминалах, но можно подключить Parallel:
$ cat directory_list.txt | parallel rsync -av --include=<span class="string">"/{}*"</span> --exclude=<span class="string">"/*"</span> ...
Турбореактивное копирование файлов по SSH
Как правило, для синхронизации директорий между двумя хостами Rsync запускают поверх SSH. Ускорив SSH-соединение, ускорим и работу Rsync. А SSH можно ускорить за счет использования набора патчей OpenSSH HPN, устраняющих ряд узких мест в механизме буферизации серверной и клиентской части SSH. Кроме того, в HPN используется многопоточная версия алгоритма AES-CTR, что повышает скорость шифрования файлов (активируется флагом -oCipher=aes[128|192|256]-ctr
). Чтобы проверить, установлен ли у тебя OpenSSH HPN, вбей в терминале:
$ ssh -V
и ищи вход подстроки HPN. Если у тебя оказался обычный OpenSSH, установить HPN-версию можно так:
$ sudo add-apt-repository ppa:w-rouesnel/openssh-hpn
$ sudo apt-get update -y
$ sudo apt-get install openssh-server
Затем добавь в /etc/ssh/sshd_config
строки:
HPNDisabled no
TcpRcvBufPoll yes
HPNBufferSize 8192
NoneEnabled yes
после чего перезапусти сервис SSH. Теперь снова создай Rsync/SSH/SCP-подключение и оцени выигрыш.
Включаем поддержку HPN
Сжатие файлов
Все те ускорения, что мы проделывали выше, основаны на одновременном запуске нескольких копий одного и того же процесса. Планировщик процессов операционной системы разруливал эти процессы между ядрами (процессорами) нашей машины, за счет чего мы и получали ускорение. Вернемся к примеру, где мы сжимали несколько файлов. Но что, если нужно сжать один огромный файл, да еще и медленным bzip2? К счастью, сжатие файлов очень хорошо поддается параллельной обработке — файл разбивается на блоки, и они сжимаются независимо. Однако стандартные утилиты, вроде gzip и bzip2, такого функционала не имеют. Благо есть много сторонних продуктов, умеющих это. Рассмотрим только два из них: параллельный аналог gzip — pigz и аналог bzip2 — pbzip2. Две эти утилиты доступны в стандартных репозиториях Ubuntu.
Использование pigz абсолютно ничем не отличается от работы с gzip, кроме возможности указать количество потоков и размер блока. Размер блока в большинстве случаев можно оставить дефолтный, а как количество потоков желательно указать число, равное (или на 1–2 больше) количеству процессоров (ядер) системы:
$ pigz -c -p5 backup.tar > pigz-backup.tar.gz
Выполнение этой команды над файлом backup.tar весом в 620 Мб заняло у меня 12,8 с, результирующий же файл весил 252,2 Мб. Обработка того же файла с помощью gzip:
$ gzip -c backup.tar > gzip-backup.tar.gz
заняла 43 с. Результирующий файл при этом весил всего-то на 100 Кб меньше, по сравнению с предыдущим: 252,1 Мб. Опять же мы получили почти четырехкратное ускорение, что не может не радовать.
Pigz умеет распараллеливать только сжатие, но не распаковку, чего не скажешь про pbzip2 — который умеет и то и другое. Использование утилиты аналогично ее непараллельному варианту:
$ pbzip2 -c -p5 backup.tar > pbzip-backup.tar.bz2
Обработка того же файла backup.tar заняла у меня 38,8 с, размер результирующего файла — 232,8 Мб. Сжатие с использованием обычного bpzip2 заняло 1 мин 53 с, при размере файла в 232,7 Мб.
Как я уже говорил, с pbzip2 можно ускорить и распаковку. Но тут нужно учесть один нюанс — параллельно распаковывать можно только то, что до этого было запаковано параллельно, то есть только архивы, созданные с помощью pbzip2. Распаковка в несколько потоков обычного bzip2-архива будет произведена в один поток. Ну и еще немного цифр:
- обычная распаковка — 40,1 с;
- распаковка в пять потоков — 16,3 с.
Осталось только добавить, что архивы, созданные с помощью pigz и pbzip2, полностью совместимы с архивами, созданными с помощью их непараллельных аналогов.
Шифрование
По умолчанию для шифрования домашней директории в Ubuntu и всех производных дистрибутивах используется eCryptfs. На момент написания статьи eCryptfs не поддерживал мультипоточности. И это особенно заметно в папках с большим количеством маленьких файлов. Так что если у тебя многоядерник, то eCryptfs использовать нецелесообразно. Лучшей заменой будет использование систем dm-crypt или Truecrypt. Правда, они могут шифровать только целые разделы или контейнеры, но зато поддерживают мультипоточность.
INFO
Пакетный фильтр NPF из состава NetBSD 6.0 позволяет добиться максимальной производительности на многоядерных системах за счет параллельной многопоточной обработки пакетов с минимальным числом блокировок.
Если хочешь зашифровать только определенную директорию, а не целый диск, то можешь попробовать EncFS. Она очень похожа на eCryptfs, но работает, в отличие от последней, не в режиме ядра, а с использованием FUSE, что делает ее потенциально медленнее, чем eCryptfs. Но она поддерживает мультипоточность, поэтому на многоядерных системах будет выигрыш в скорости. К тому же она очень проста в установке (доступна в большинстве стандартных репозиториев) и использовании. Нужно всего-то выполнить
$ encfs ~/.crypt-raw ~/crypt
ввести парольную фразу, и все: в .crypt-raw будут лежать зашифрованные версии файлов, а в crypt — незашифрованные. Чтобы размонтировать EncFS, выполни:
$ fusermount -u ~/name
Конечно, все это можно автоматизировать. О том, как это сделать, можно почитать здесь.
Люблю смотреть, как ядра пашут
Загружать-то процессор на полную мы загружаем, но нужно иногда и мониторить его работу. В принципе, почти каждый дистрибутив имеет хорошую оснастку для мониторинга использования процессора, включая информацию о каждом отдельном ядре или процессоре. В Kubuntu, например, KSysGuard очень удачно отображает текущую загруженность ядер (смотри скрин «KSysGuard на четырехъядерной системе»).
KSysGuard на четырехъядерной системе
Но есть и другие интересные утилиты, позволяющие созерцать работу процессора. Любителям консольных решений по душе придется htop — более красочный и интерактивный аналог top. Еще советую обратить внимание на Conky — мощный и легко настраиваемый системный монитор. Очень легко его настроить для мониторинга загруженности каждого ядра и процессора в целом. Для каждого ядра можно вывести отдельный график. На скриншоте можешь посмотреть мой вариант конфигурации утилиты.
Вот так выглядит Conky
Htop в действии
Содержимое соответствующего конфигурационного файла я выложил сюда. В Сети, кстати, полно интересных конфигов, которые можно взять за основу и переделать под себя.
Но эти утилиты дают лишь информацию о загруженности, чего часто может оказаться недостаточно. Mpstat из набора sysstat выдает более интересную информацию, как, например, время простоя каждого ядра, время, потраченное на ожидание ввода/вывода, или время, затраченное на обработку прерываний.
Вывод утилиты mpstat
GPU не только для игр
Не секрет, что современные GPU обладают очень большими вычислительными мощностями. Но из-за того, что ядра GPU имеют особую архитектуру и ограниченный набор доступных команд, GPU пригоден лишь для решения узкого круга задач. Раньше выполнять вычисления на GPU могли только гуру шейдеров. Сейчас же производители видеокарт делают все возможное, чтобы упростить жизнь энтузиастов и разработчиков, желающих задействовать мощности графических процессоров в своих проектах: CUDA от NVIDIA, AMD FireStream, открытый стандарт OpenCL. С каждым годом вычисления на GPU становятся все доступнее и доступнее.
Вычисление хешей
На сегодняшний день из запускаемых на GPU задач популярнее всего, наверное, вычисление хешей. А все это из-за Bitcoin-майнинга, который, собственно, и заключается в вычислении хешей. Большинство Bitcoin-майнеров доступны под Linux. Если ты хочешь майнить Bitcoin’ы и если твой графический процессор поддерживает OpenCL (если поддерживает CUDA, то и OpenCL тоже), тогда рекомендую обратить внимание на bfgminer: он быстр, удобен и функционален, хоть и не так уж прост в настройке.
Ускорение Snort за счет GPU
Очень интересный концепт под названием Gnort разработали исследователи из греческого института FORTH (Foundation for Research and Technology — Hellas). Они предлагают повысить эффективность обнаружения атак Snort’ом за счет переноса на GPU кода, отвечающего за проверку регулярных выражений. Если верить графикам, приведенным в официальной PDF’ке исследования, они добились почти двукратного увеличения пропускной способности Snort.
Но не Bitcoin’ом единым живем. Ничто не мешает использовать мощности GPU для брутфорса хешей (с целью узнать свой забытый пароль, конечно же, не более). В решении этой задачи хорошо зарекомендовала себя утилита oclHashcat-plus — настоящий комбайн по бруту хешей. Умеет подбирать хеши MD5 с солью и без соли, SHA-1, NTLM, кешированные пароли домена, пароли баз данных MySQL, пароли GRUB, и это еще даже не половина списка.
Шифрование на GPU
Очень интересное применение мощностей графических процессоров представили нам студенты Вэйбинь Сунь (Weibin Sun) и Син Линь (Xing Lin) из университета Юты в рамках проекта KGPU. Суть проекта заключается в переносе исполнения некоторых частей кода ядра Linux на CUDA-совместимый GPU. Первым разработчики решили вынести на GPU алгоритм AES. К сожалению, на этом развитие проекта и остановилось, хотя разработчики обещали продолжить работу. Но помимо этого, уже существующую наработку можно использовать для ускорения AES-шифрования в eCryptfs и dm-crypt, жаль только, что ядро версии 3.0 и выше не поддерживается.
Мониторинг производительности GPU
А почему бы и нет? Конечно, загруженность каждого GPU ядра узнать не удастся, но хоть какую-то информацию о происходящем на GPU получить можно. Программка CUDA-Z (почти аналог Windows-программы GPU-Z), кроме разной статической информации о GPU, умеет получать и динамическую: текущую скорость обмена данными между графическим процессором и машиной, а также общую производительность всех ядер GPU в флопсах.
Вкладка CUDA-Z с информацией о производительности GPU
Выводы
Многоядерные или же многопроцессорные рабочие станции достаточно давно вошли в нашу повседневную жизнь — пора менять и наш однопоточный подход при работе с ними. Ведь распараллеливание задач на таких системах дает нам огромный выигрыш времени, в чем мы и убедились.