https://www.agner.org/optimize/#manuals - отличные мануалы по оптимизациям от датского эволюционного биолога (программирует, наверное, для души).
C++
Optimizing software in C++: An optimization guide for Windows, Linux and Mac platforms — стоимость абстракций в C++ коде.
Большая часть и так должна быть хорошо известна программистам.
Многие знают также способ борьбы с виртуальными функциями с помощью CRTP+static_cast.
- Runtime-полиморфизм в C++ - еще про способы реифицировать полиморфизм
A Journey Into Non-Virtual Polymorphism in C++ - Rudyard Merriam - C++23 способ провернуть то же самое через deducing this
, немного менее сuriously
Deducing this Patterns - Ben Deane - более детально.
Один из прикольных паттернов оттуда:
struct my_vector : vector<int> { |
Asm
The microarchitecture of Intel, AMD, and VIA CPUs - позиционируется как гайд для ассемблерщиков и компиляторщиков. Но всё еще понятно для gamedev программистов и любителей почитать дизассемблированный листинг сгенерированного компилятором C++-кода.
Для меня мануал оказался интересен объяснением архитектуры современных процессоров языком, всё еще понятным программистам, а не железячникам. Плюс в универе когда-то был курс по архитектуре компьютеров, который начинался с 8086 и заканчивался очень беглым описанием первых Pentium, а тут как раз продолжение ровно с того места, на котором он заканчивался.
Для примера, многие C++-программисты могут ответить, что ++i
лучше чем i+=1
, так как у процессора есть специальная инструкция inc
, оптимизированная для прибавления единицы. Однако можно проверить, что современные компиляторы вставляют вместо этого add eax, 1
(а с оптимизациями и вообще lea eax, [rdi + 1]
). Почему же они не используют “быстрый” inc
?
- современные процессоры умеют
Out-of-order execution
. Инструкции разбиваются на микро-операции.
Какой-нибудь push eax
может разделяться на операцию вычисления нового адреса верхушки стека (для чего есть отдельный пайплайн stack engine), и операцию перемещения из регистра по этому адресу. Выполнить первую микро-операцию можно отдельно от второй и даже раньше, чем предыдущию инструкцию, которая записывает значение в этот регистр. Соответственно, процессор “знает” о том, какие микрооперации можно выполнять независимо от других, а какие должны дождаться выполнения предыдущих
Забавно, что микро-операции затем снова могут склеиваться в одну макро-операцию(instruction fusion), для экономии памяти. Существует кеш декодированных инструкций, чтобы не тратить время на повторное декодирование для коротких циклов. Интересно, если ли в каких-нибудь процессорах регистры, в которые можно загрузить инструкции (хотя бы в архитектурах, где инструкции одинакового размера)
- еще одна фича процессоров — вместо прямого использования указанного регистра, может использоваться временный невидимый регистр.
mov eax, [mem1] # допустим, что mem1 в кеше |
- многие инструкции меняют значения флагов. Флаги хранятся в регистре, который тоже может быть заменён на временный. Некоторые операции требуют синхронизации временного регистра с обычным (retirement).
- по историческим причинам inc не меняет значения carry-флага процессора, в отличие от add. Из-за этого следующие за ней инструкции, использующие значения регистра флагов, должны собрать актуальное значение этого регистра флагов (дополнительная микро-операция). lea же хороша тем, что вообще не трогает флаги.
Еще один ненормальный факт из мира асма, непонятный C++ программистам: у некоторых процессоров с несколькими декодировщиками инструкций эти декодировщики обладают неодинаковыми возможностями - например, первый может декодировать инструкции, которые генерируют от 1 до 4 микроопераций, а второй и третий - только инструкции с 1 микрооперацией. В этом случае для достижения максимальной скорости выполнения инструкции должны быть расставлены “в стихотворной форме”, по паттерну 4-1-1, чтобы занять одновременно все 3 декодировщика (генерация одновременно по 6 микроопераций за цикл, а при ритме 2-2-2 - по 2 микрооперации за цикл).
Инструкции могут декодироваться заранее, если следующие стадии заняты вычислениями. Корректному декодированию могут мешать инструкции условных переходов (if/loop
) и indirect jump с несколькими вариантами перехода(switch/function pointers/virtual
). Аппаратный способ борьбы с неправильным декодированием — предсказания переходов.
Простейший “предсказатель” логического перехода — таблица из адреса перехода и соотвествующего ему двухбитного saturating-счётчика (“почти точно правда/скорее правда/скорее ложь/почти точно ложь”). Другой вариант трактовки бит в счётчике — вместо “переход произойдёт/не произойдёт” - “будет ли как в прошлый раз/будет не так, как прошлый раз” (позволяет предсказывать flip-flop переходы). Более сложные варианты могут содержать “память” на несколько предыдущих значений, например, 4 saturating-счётчика, каждый из которых показывает, какая вероятность перехода в следующее состояние в зависимости от 2х предыдущих состояний. Для indirect jump также необходимы несколько saturating-счётчиков для каждого из вариантов перехода. Предсказатели циклов могут запоминать количество повторов цикла. Различные варианты могут объединяться в гибридные предсказатели.
Если отойти от магии инструкций и их предсказаний — важными являются также предсказуемые паттерны доступа к данным. Чтобы данные эффективно попадали в кеши, они должны лежать в памяти последовательно. Кеш-строк много, так что удобная тактика для циклов — за первый проход захватить все нужные для последующих проходов данные в кеш-строки, чтобы второй и последующий проходы использовали данные из этих кеш-строк.
Array of structures of arrays - Array of structures of arrays - способ раскладывания в памяти данных, чтобы и в кеш побольше попало, и чтобы обрабатывать пачками. В плане возможностей описания данных в памяти, си не очень далеко ушёл от ассемблера.
Оптимизации компиляторов
Chandler Carruth, но в ту же тему:
Optimizing the Emergent Structures of C++ — про возможности в оптимизации инструкций и данных компилятором.
Efficiency with Algorithms, Performance with Data Structures — как обычно, векторы и хеш-таблицы без списков внутри эффективные, списки и графы (и структуры в которых они неявно спрятаны) — нет.