You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
224 lines
13 KiB
224 lines
13 KiB
СКОРОСТНЫЕ ВЫЧИСЛЕНИЯ
|
|
КРАТКИЙ КУРС
|
|
|
|
Цель методички - дать обзор по техникам оптимизации и быстрых вычислений для программистов на Си/С++.
|
|
Для этого мы рассмотрим устройство современных микропроцессоров и вносимые ими особенности в процесс вычислений.
|
|
|
|
ОПТИМИЗАЦИИ КЛАССИЧЕСКИХ ВЫЧИСЛЕНИЙ
|
|
|
|
В этом деле лучший - Агнер Фог: https://agner.org
|
|
По вопросам развертывания циклов, линий процессорного кеша, микроархитектуре Intel и SIMD-вычислениям отсылаем к его серии книг:
|
|
https://www.agner.org/optimize/
|
|
|
|
|
|
СУПЕРСКАЛЯРНЫЕ МИКРОПРОЦЕССОРЫ
|
|
|
|
В суперскалярных микропроцессорах высока степень избыточности вычислительных узлов.
|
|
Целочисленных и вещественных АЛУ имеется несколько штук, есть блок предсказания ветвлений, есть теневой регистровый файл, и еще куча всего дублируется.
|
|
Это дает возможность разбить последовательный код на куски и выполнять его параллельно.
|
|
|
|
To be done
|
|
|
|
БАРЬЕРЫ ПАМЯТИ
|
|
|
|
Барьер памяти - это способ контроля внеочередного выполнения инструкций процессором и/или компилятором.
|
|
Внеочередное выполнение кода неинтуитивно. Код ведь должен выполняться в том порядке, в котором он написан (на самом деле нет).
|
|
Операции над не связанными явно (!) друг с другом данными могут происходить либо не по порядку, либо вообще одновременно
|
|
в рамках одного и того же ядра, используя массивную избыточность вычислительных узлов процессора.
|
|
Барьер памяти же (в грубом приближении) заставляет выполнить код так, как он написан на экране (последовательно и предсказуемо),
|
|
а не так как хочет процессор с компилятором (в контр-интуитивном, но более оптимальном порядке).
|
|
То есть, гарантирует, что код до барьера памяти выполнится частично либо полностью, к моменту завершения инструкции барьера.
|
|
Барьер обычно является ассемблерной инструкцией (т.е. присутствует в системе команд процессора).
|
|
Барьер обычно является хинтом "между этими данными есть неявная связь", но необязательно.
|
|
|
|
В C++ есть три модели памяти для атомиков:
|
|
1. relaxed: гарантируется только то, что операции будут выполнены атомарно. В каком порядке - вопрос.
|
|
- модификация переменной "появится" в другом потоке не сразу
|
|
- поток thread2 "увидит" значения одной и той же переменной в том же порядке, в котором происходили её модификации в потоке thread1
|
|
- порядок модификаций разных переменных в потоке thread1 не сохранится в потоке thread2
|
|
relaxed-переменные можно использовать как счетчики или флаги остановки.
|
|
Самая быстрая и самая ненадежная модель памяти.
|
|
Аналог из транзакционной модели СУБД - READ UNCOMMITTED
|
|
2. sequential consistency, seq_cst: состояние памяти синхронизируется между всеми потоками программы.
|
|
- порядок модификаций разных атомарных переменных в потоке thread1 сохранится в потоке thread2
|
|
- все потоки будут видеть один и тот же порядок модификации всех атомарных переменных.
|
|
Сами модификации могут происходить в разных потоках
|
|
- все модификации памяти (не только модификации над атомиками) в потоке thread1, выполняющей store на атомарной переменной,
|
|
будут видны после выполнения load этой же переменной в потоке thread2
|
|
Самая медленная и самая надежная модель памяти.
|
|
Аналог из транзакционной модели СУБД - SERIALIZED
|
|
3. acquire/release: синхронизация пары.
|
|
- модификация атомарной переменной с release будет мгновенно видна в другом потоке, выполняющим чтение этой же атомарной переменной с acquire
|
|
- все модификации памяти в потоке thread1, выполняющей запись атомарной переменной с release,
|
|
будут видны после выполнения чтения той же переменной с acquire в потоке thread2
|
|
- процессор и компилятор не могут перенести операции записи в память ниже release операции в потоке thread1,
|
|
и нельзя перемещать выше операции чтения из памяти выше acquire операции в потоке thread2
|
|
Позволяет делать синхронизацию только между двумя потоками (в отличие от всех потоков в 1 и 2).
|
|
|
|
https://habr.com/ru/post/517918/
|
|
https://gcc.gnu.org/wiki/Atomic/GCCMM/AtomicSync
|
|
https://habr.com/ru/company/JetBrains-education/blog/523298/
|
|
https://habr.com/ru/post/545996/
|
|
https://habr.com/ru/post/546222/
|
|
https://habr.com/ru/post/546880/
|
|
https://elixir.bootlin.com/linux/latest/source/Documentation/memory-barriers.txt
|
|
|
|
ЗАМЕР ВРЕМЕНИ С НАНОСЕКУНДНОЙ ТОЧНОСТЬЮ
|
|
|
|
Точные замеры времени на архитектуре x86/x64 - тема объемная.
|
|
На скалярных процессорах с фиксированной частотой можно считать такты, а на x64 нельзя: результат будет различаться.
|
|
|
|
Один и тот же код может выполняться разное время из-за разного состояния узлов системы, что (грубо) определяется общей нагрузкой на систему.
|
|
Детерминизм отсутствует из-за:
|
|
- внеочередного исполнения инструкций
|
|
- суперскалярной архитектуры (конвеер, теневые регистры, состояние кэша и прочее дублирование)
|
|
- плавающей частоты как процессора, так и отдельных частей системы (шина)
|
|
|
|
Все усугубляется различиями реализаций между поколениями процессоров, между процессорами разных производителей,
|
|
внутри одного поколения одного производителя.
|
|
Ну и вдобавок, мы работаем в ОС общего назначения, с взаимовлиянием разных процессов и кода ядра друг на друга,
|
|
что добавляет хаоса.
|
|
|
|
Словом, несмотря на огромные скорости, для задач реального времени х64 так себе :)
|
|
|
|
Можно говорить о вероятностях: общее время выполнения цикла, среднее время выполнения итерации, оценки снизу и сверху.
|
|
|
|
В С++ есть chrono::high_resolution_timer. Но на разных компиляторах, разных ОС и процессорах его точность гуляет на три порядка.
|
|
Самое точное значение я видел на Linux / Intel(R) Core(TM) i5-4690K CPU @ 3.50GHz / gcc 8.4, с точностью в 30 наносекунд.
|
|
На Intel Core i5 480M @ 2.67GHz / Windows 7 / MSVC 2017 точность порядка 4000 наносекунд.
|
|
На Intel Atom x7-Z8750 @ 1.6GHz / Windows 10 / MSVC 2017 точность 320 наносекунд.
|
|
|
|
На x64 замеры времени можно делать с помощью Timestamp Counter (TSC) - имеющегося на всех поколениях х64 регистра MSR.
|
|
На последних поколениях, в нем содержится число тактов таймера фиксированной частоты с момента сброса процессора.
|
|
Для разных поколений и производителей процессоров смысл этого значения отличается.
|
|
Гарантию фиксированной частоты таймера можно проверить по наличию флага constant_tsc в /proc/cpuinfo.
|
|
Чтение таймера выполняется инструкциями RDTSC/RDTSCP, что в свою очередь имеет цену в тактах.
|
|
Замеры времени сами по себе влияют на выполнение: меняют состояние конвеера, являются предметом внеочередного выполнения,
|
|
влияют на кэш (ведь мы куда-то помещаем считанные данные).
|
|
|
|
Методика замеров следующая:
|
|
1. Вычисляем цену инструкции RDTSC в тактах при ТЕКУЩЕЙ* нагрузке:
|
|
- нагружаем конвеер циклом из этой инструкции, вычисляем среднее число тактов на инструкцию
|
|
2. Получаем (эмпирически**) текущую частоту таймера:
|
|
- замеряем несколько раз, сколько тактов протикает за одну секунду***
|
|
3. Определяем цену в наносекундах одного такта, на основании данных из 1 и 2.
|
|
4. Сэмплируем замеряемый участок инструкциями RDTSC, учитывая цену самой RDTSC
|
|
5. Интерпретируем многократные замеры.
|
|
* При изменении нагрузки, цена RDTSC может поплыть - из-за плавающей частоты процессора, состояния конвеера.
|
|
** Точное и универсальное получение частоты таймера достаточно сложно, как из-за разного смысла самого этого значения
|
|
на разных процессорах, так и из-за разницы в необходимых исходных данных и формулах.
|
|
https://stackoverflow.com/questions/42189976/calculate-system-time-using-rdtsc
|
|
Потому мы тут срезаем угол, уходим в эмпирику, но получаем достаточно правдоподобные значения.
|
|
*** И тут мы полагаемся на реализацию функции sleep в ОС/компиляторе, обладающую огромным шумом.
|
|
|
|
Интерпретация замеров:
|
|
- нулевое значение времени означает внеочередное выполнение двух RDTSC подряд:
|
|
выполнение замеряемого участка было заблокировано, и процессору пришлось выполнять второй замер вместо него.
|
|
То есть, на самом деле этот код выполнялся не нулевое время, а наоборот дольше обычного.
|
|
- большие всплески - это переключение контекста и ожидание, пока другой поток отработает свой квант времени.
|
|
Кроме того это могут быть миграции потоков между ядрами - как связанное с этим время,
|
|
так и рассинхрон таймеров между ядрами (гарантий их синхронности нет).
|
|
- меньшие всплески - ожидание передачи данных по шине при промахах в кеше
|
|
- остальные значения более-менее соответствуют истине.
|
|
|
|
Как видим, методика несовершенна.
|
|
Сильный шум не дает получить точные времянки, но все еще можно получить хотя бы качественные характеристики
|
|
выполнения кода, и посчитать распределения вероятностей времён.
|
|
|
|
В процессорах x86 есть встроенный блок слежения за производительностью — Performance Monitoring Unit (PMU),
|
|
среди которых есть независимый от частоты регистр (Описать Coreclock register)
|
|
|
|
ЗАДАЧИ РЕАЛЬНОГО ВРЕМЕНИ В УНИВЕРСАЛЬНЫХ ОС
|
|
|
|
Наилучшая ОСРВ однозадачна. Любое переключение контекста вносит задержку в выполнение; цена этой задержки может быть неизмерима.
|
|
В MS DOS контекст переключается только на прерываниях; в RT-11 еще при завершении асинхронного ввода-вывода.
|
|
В современных ОСРВ добавились таймеры, примитивы синхронизации.
|
|
Это тот максимум сервиса, который может позволить ОСРВ.
|
|
Попытки привнести в ОСРВ многозадачность ухудшают её характеристики.
|
|
|
|
Известен realtime-патч для Linux, он действительно повышает предсказуемость планировщика, снижает число переключений контекста
|
|
и уменьшает задержки в работе процесса РВ. Это подходит для многих задач микросекундной точности.
|
|
Наглядно про джиттер переключения контекста при обработке прерываний: https://habr.com/ru/post/562636/
|
|
|
|
Самый радикальный способ просто и дешево получить ОСРВ из обычного Linux - выделять отдельные ядра задачам РВ (thread affinity).
|
|
Вся обработка прерываний и системные процессы при этом выносятся на другие ядра.
|
|
Так устраняется сама возможность переключения контекста для процессов РВ (кроме отдельных обязательных прерываний).
|
|
https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux_for_real_time/8/html/tuning_guide/chap-general_system_tuning
|
|
|
|
|
|
СКОРОСТНЫЕ СЕТИ
|
|
|
|
Для уменьшения латентности (минимизация времени отклика) применяются такой хак как Kernel Bypass:
|
|
обработка сетевого потока минуя ядро, в userland. В частности такое применяется в сетевых картах SolarFlare (их термин OnLoading).
|
|
Весь стек TCP/IP скомпонован в библиотеке, работающей в режиме пользователя.
|
|
Прерывание от железа разумеется есть, и оно обрабатывается в ядре; но из приключений пакета в ядре исключаются сетевой фильтр (iptables) и все ненужное.
|
|
Так убираются лишние переключения контекста kernel/user, внутриядерные блокировки (которые например могут быть зажатыми для нашего процесса чем-то еще),
|
|
копирования буферов, вероятность повторного перепланирования слишком затянувшегося обработчика в ядре, итд.
|
|
Это обычно совмещается с аффинизацией задач РВ (см.выше) на выделенном ядре.
|
|
В библиотеке максимально используются спинлоки в качесте блокировок.
|
|
Для программиста это выглядит как обычные сокеты.
|
|
|
|
Bonding описать.
|
|
|
|
Для увеличения пропускной способности используется RDMA - удаленный прямой доступ к памяти (другой машины в сети).
|
|
При этом копирование в память компьютера происходит непосредственно с аппаратного приемного буфера сетевой карты,
|
|
минуя обработку центральным процессором, минуя ОС как таковую (по сути без генерации прерывания).
|
|
Низкая латентность здесь получается прицепом, но наиболее эффективно это именно для толстого постоянного потока данных (например, в кластеризированных СУБД).
|
|
Пример железа - Infiniband, библиотека libibverbs.
|
|
Применяется в interlinked-кластерах, кластерных СУБД.
|
|
|
|
Общее место скоростных сетей - отказ от копирования буферов (zero copy).
|
|
На гигабайтном потоке время копирования буфера *уже* уменьшает пропускную способность вдвое.
|
|
На 100ГБ потоке посчитайте сами.
|
|
Потому все расчеты и манипуляции с данными должны производиться непосредственно в передающих/приемных буферах.
|
|
|
|
ТЕНЕВАЯ БУФЕРИЗАЦИЯ (SHADOW BUFFERING/DOUBLE BUFFERING)
|
|
|
|
Для достижения высокой пропускной способности передачи данных (в сеть, при сбросе на диск) используется теневая буферизация:
|
|
- выделяется несколько буферов, только один из них активен (готов к передаче) в один момент времени
|
|
- неактивные буфера наполняются данными в отдельных потоках
|
|
- из активного буфера тем временем передаются данные
|
|
- по завершении передачи из активного буфера, он помечается как неактивный; выбирается следующий готовый к работе буфер.
|
|
|
|
Это частный случай идеи кольцевого буфера.
|
|
|
|
РАЗНОЕ
|
|
|
|
Вычисления с плавающей точкой без погрешности
|
|
https://habr.com/ru/post/523654/
|
|
|
|
Оптимизация математических вычислений и опция -ffast-math в GCC 11
|
|
https://habr.com/ru/company/ruvds/blog/586386/
|
|
|
|
Быстрый парсинг double
|
|
https://habr.com/ru/company/ruvds/blog/542640/
|
|
https://github.com/fastfloat/fast_float
|
|
|
|
Быстрая валидация UTF8
|
|
https://habr.com/ru/company/ruvds/blog/551060/
|
|
https://arxiv.org/pdf/2010.03090.pdf
|
|
|
|
ASM today
|
|
https://habr.com/ru/post/544786/
|
|
|
|
Кольцевой буфер без деления по модулю
|
|
https://habr.com/ru/company/otus/blog/557310/
|
|
|
|
epoll и Windows IO Completion Ports: практическая разница
|
|
https://habr.com/ru/company/infopulse/blog/415403/
|
|
|
|
Какой предел у предсказателя ветвлений? Проверили на x86 и M1
|
|
https://habr.com/ru/company/selectel/blog/557410/
|
|
|
|
ССЫЛКИ
|
|
1. Agner Fog, Optimization manuals https://www.agner.org/optimize/
|
|
2. Стоимость операций в тактах ЦП https://habr.com/ru/company/otus/blog/343566/
|
|
3. select / poll / epoll: практическая разница https://habr.com/ru/company/infopulse/blog/415259/
|
|
4. Evaluating the Cost of Atomic Operations onModern Architectures https://spcl.inf.ethz.ch/Publications/.pdf/atomic-bench.pdf
|
|
5. Intel Intrinsics Guide https://software.intel.com/sites/landingpage/IntrinsicsGuide/
|
|
6. Neon Intrinsics Reference https://developer.arm.com/architectures/instruction-sets/simd-isas/neon/intrinsics
|
|
разное
|
|
блоги
|
|
https://easyperf.net/notes/
|
|
http://scrutator.me/
|
|
|