Влияние Cisc Design на стратегии оптимизации компиляторов
Введение: Непреходящее влияние CISC на современные компиляторы
Взаимосвязь между архитектурой процессора и оптимизацией программного обеспечения является краеугольным камнем информатики. Среди наиболее эффективных архитектурных парадигм - комплексная инструкция Set Computing (CISC), философия дизайна, которая формировала разработку компилятора на протяжении десятилетий. В отличие от ее аналога - сокращенный набор инструкций Set Computing (RISC), который опирается на небольшой набор быстрых, простых инструкций, процессоры CISC упаковывают богатые многоступенчатые операции - такие как копирование строк, многочленная оценка или арифметика памяти к памяти - в единичные машинные инструкции. Эта сложность напрямую влияет на то, как компиляторы генерируют, оптимизируют и планируют машинный код. Понимание этого взаимодействия необходимо для любого, кто работает в системном программировании, дизайне компилятора или инженерии производительности, поскольку наследие CISC сохраняется в доминирующих архитектурах, таких как x86 и его потомки.
В этой статье исследуется глубокое влияние дизайна CISC на стратегии оптимизации компиляторов. Мы рассмотрим ключевые области, включая выбор инструкций, плотность кода, слияние макроопераций, распределение регистров в соответствии с переменными длинами инструкций и современные проблемы, связанные с разложением микроопераций CISC. Благодаря конкретным примерам и ссылкам на реальные архитектуры мы покажем, как компиляторы эволюционировали, чтобы использовать мощь CISC, уменьшая при этом присущую ему сложность.
Краткая история CISC: от мэйнфреймов до x86
Корни CISC восходят к 1960-м и 1970-м годам, когда память была дорогой и процессоры были медленными. Для уменьшения количества инструкций, необходимых для данной программы, архитекторы упаковали больше функциональности в каждую инструкцию. Система IBM/360, представленная в 1964 году, является основополагающим примером: ее набор инструкций включал арифметику на значения в памяти, условные ветви с несколькими кодами состояний и высокоуровневые операции, такие как «Сравнить и ветвь» [IBM System/360 Принципы работы]. Эта философия дизайна продолжилась с архитектурой VAX корпорации цифрового оборудования (1977), которая могла похвастаться более чем 300 инструкциями, многие из которых способны выполнять сложное движение данных и арифметику в одной операции [Comer, «VAX Architecture»].
Наиболее устойчивое семейство CISC - это архитектура x86, возникшая с Intel 8086 в 1978 году. набор инструкций x86 развивался через расширения, такие как MMX, SSE и AVX, накапливая сотни инструкций, которые сильно различаются по длине (1-15 байтов). Несмотря на революцию RISC 1980-х годов, которая доказала, что более простые инструкции могут давать более высокие тактовые частоты и более легкую конвейеризацию, CISC оставался доминирующим на рынках настольных компьютеров и серверов из-за обратной совместимости и обширной установленной программной базы. Сегодня процессоры x86 (Intel Core, AMD Ryzen) используют гибридный подход: они декодируют сложные инструкции CISC в более мелкие, похожие на RISC микрооперации (μops) для выполнения, метод, который непосредственно информирует современные стратегии компилятора.
Стратегии оптимизации компиляторов, на которые влияет CISC
Богатство набора инструкций CISC создает как возможности, так и проблемы для компиляторов. Ниже мы рассмотрим ключевые области, в которых дизайн CISC стимулирует принятие решений по оптимизации.
Выбор инструкций: балансирование мощности и стоимости
В системе RISC выбор команд относительно прост: компилятор отображает высокоуровневые операции на небольшой набор простых инструкций, полагаясь на оптимизатор для слияния последовательностей, где это выгодно. В CISC компилятор должен выбирать из обширного меню инструкций, каждая из которых имеет различную длину, задержку и использование ресурсов. Например, для вычисления компилятор RISC может генерировать три инструкции (множить, добавлять, хранить). Компилятор CISC может использовать одну команду, такую как , если архитектура поддерживает его, или операнд на основе памяти для снижения давления регистра.
Современные компиляторы (GCC, LLVM) используют модели сопоставления шаблонов и основанные на стоимости для принятия этих решений. Целевой бэкэнд (например, x86's в LLVM) содержит сотни шаблонов, которые выбирают лучшую последовательность команд для заданного ИК-паттерна. Например, когда цикл содержит последовательность нагрузок памяти и добавления, компилятор может выбрать режим индексирования адреса (например, ) для выполнения вычисления адреса и операции памяти в одной инструкции. Это уменьшает количество команд, но вводит сложность: компилятор должен гарантировать, что вычисление адресов не переполняет или не вызывает сбоя сегментации. Расширенные компиляторы также рассматривают возможности слияния команд по основным блокам, используя глобальный выбор инструкций для максимизации плотности кода и минимизации динамического количества команд.
Использование Code Density и Cache
Одним из исторических преимуществ CISC является плотность кода. Поскольку одна инструкция CISC может заменить несколько инструкций RISC, полученная двоичная часто меньше. Например, инструкция CISC , которая загружается с адреса памяти с использованием 32-битного смещения, занимает всего 5-7 байт, в то время как эквивалентная последовательность RISC (адрес загрузки в регистр, затем нагрузка из регистра) может потребовать 8-12 байт. Меньший код означает лучшее использование кэша инструкций, что имеет решающее значение для производительности в связанных с памятью рабочих нагрузках.
Компиляторы используют плотность кода с помощью таких методов, как:
- Укорочение инструкций: Когда это возможно, компилятор выбирает наименьшее кодирование (например, используя вместо с 32-битным моментом, если значение укладывается в 8 бит). Современные кодировки CISC переменной длины (x86-64) даже позволяют 2-байтную форму для общих инструкций. GCC и LLVM выполняют оптимизацию размера, которые пытаются сжать инструкции.
- Стек против распределения регистров: В глубоко вложенном коде CISC компиляторы иногда разбрасывают регистры в стек с использованием компактных инструкций push/pop (]/] в x86 только по 1 байту каждый), а не общие с движениями памяти регистра, которые занимают 3-4 байта.
- Использование сложных режимов адресации: Режим индексированной адресации () позволяет одной инструкции загружаться из элемента массива. Компиляторы тщательно оценивают, компенсируется ли более длительное кодирование инструкции (до 7 байт) устранением отдельной инструкции расчета адреса. Для плотных циклов экономия в размере кода и уменьшенное количество μop часто наклоняют баланс.
Однако повышенная плотность кода не всегда улучшает производительность. Более длинные инструкции могут занять больше времени для декодирования (особенно в ранних x86-проводниках), а кодирование с переменной длиной делает преддекодирование и прогнозирование ветвей более трудным. Поэтому компиляторы применяют оптимизацию плотности выборочно, часто в сочетании с оптимизацией под управлением профиля (PGO), чтобы определить горячие пути, где меньший код наиболее полезен.
Макрооперация Fusion и микрооперация разложения
Современные процессоры CISC (x86 от Pentium M и далее) внутренне разбивают сложные инструкции на простые микрооперации (μops), которые отображают в конвейер исполнения. Например, x86 разлагается на μop нагрузки, арифметический μop и μop хранилища. Это разложение позволяет процессору сохранять трубопровод полным и эксплуатировать выполнение вне порядка, но это также означает, что одна инструкция CISC может отображаться в механизме выполнения в виде трех отдельных операций.
Компиляторы должны учитывать эту микроархитектуру. Появились две ключевые стратегии:
- Макрофьюжн: Некоторые инструкции CISC объединяют две логические операции (например, сравнение и ветвь). На x86 определенные пары, такие как , за которыми следует , сливаются процессором в один мкоп. Компилятор может стимулировать слияние, сохраняя прилегающее сравнение и ветвь и избегая инструкций, которые изменяют коды условий между ними. GCC и LLVM включают целевые пропуски планирования, которые организуют инструкции для максимизации макрофьюжн.
- Микро-оперативное кэширование: Последние ядра x86 (Intel Haswell и более поздние) включают в себя кэш μop, в котором хранятся декодированные μops для циклов. Для этого компиляторы генерируют код, который соответствует размеру линии кэша μop (часто 4-6 μops). Они также выравнивают заголовки петлей к границам линии кэша. Это низкоуровневая оптимизация, которая требует глубоких знаний о конвейере декодирования процессора.
Интересно, что микро-оперативное разложение иногда делает RISC-подобные простые инструкции быстрее, чем их эквиваленты CISC. Например, последовательность и с использованием регистров может быть декодирована в меньшее количество полных μop, чем один , который потребляет три слота μop. Современные компиляторы используют модели затрат, которые имитируют количество μop, задержку и использование порта для выбора лучшей последовательности. Файл LLVM даже определяет маршруты планирования, которые отражают микроархитектуру конкретных ядер Intel или AMD.
Регистр распределения и переменных длин инструкций
Распределение реестра осложняется CISC, поскольку многие инструкции могут напрямую обращаться к памяти, делая давление регистра менее критическим, но также вводя компромиссы. Когда компилятор выделяет регистр для часто используемой переменной, он может избегать операций памяти, но полученные инструкции регистра-регистратора обычно длиннее (из-за байтов модификатора), чем версии, доступные для памяти. Например, (с байтом ModRM) составляет 2-4 байта, в то время как составляет всего 2 байта. Напротив, инструкции RISC всегда одинаковой длины (обычно 4 байта), поэтому размер кода не зависит от распределения регистра.
Компиляторы CISC должны взвесить преимущество сохранения значения в регистре против возможности увеличения размера кода и декодирования задержки. Они часто используют эвристику на основе глубины цикла и размера функции. Например, в горячем цикле компилятор будет предпочитать регистры, чтобы избежать задержки памяти, даже если это означает использование более длинных кодов команд. В холодном коде или больших функциях он может агрессивно передаваться в память, чтобы сохранить двоичный малый. Оптимизация под руководством профиля дополнительно информирует об этом решении, определяя, какие пути наиболее чувствительны к производительности.
Другая проблема заключается в ограниченном количестве регистров общего назначения в x86: только 8 в 32-битном режиме (EAX, EBX, ECX, EDX, ESI, EDI, EBP, ESP) и 16 в 64-битном режиме. Этот дефицит заставляет компиляторы быть умными в отношении назначения регистра. Многие инструкции CISC имеют неявное использование регистра (например, использует EAX и EDX неявно), что ограничивает распределитель. Современные компиляторы используют распределители с цветом графика с конкретными ограничениями (например, «не назначать EAX для этого значения, потому что он будет забиваться следующим подразделением»). Кроме того, они могут вставлять push / pop для сохранения регистров через вызовы - основной элемент CISC, который добавляет 1-2 байта на регистр.
Проблемы, связанные с сложностью CISC
Хотя CISC предлагает множество возможностей для оптимизации, он также создает значительные препятствия для авторов компиляторов.
Расписание инструкций и переменная задержка
В архитектурах RISC большинство инструкций имеют предсказуемую, однородную задержку (часто 1 цикл для простых операций ALU). Инструкции CISC могут иметь широко различающиеся задержки. Например, простая может принимать 1 цикл, в то время как (целое деление) занимает 20–40 циклов. Даже одна и та же инструкция может иметь разные задержки в зависимости от типов операндов (регистр против памяти) и выравнивания. Это делает статическое планирование инструкций чрезвычайно сложным. Компиляторы часто полагаются на таблицы инструкций (например, таблицы справочных руководств по оптимизации Intel), которые перечисляют задержки, пропускную способность и использование портов для каждого варианта инструкций. Алгоритмы планирования затем пытаются скрыть инструкции с высокой задержкой, перемещая независимую работу вперед. Однако переменная длина инструкций CISC также влияет на полосу пропускания декодирования: процессор может декодировать только ограниченное количество байтов за цикл (обычно 4–6 байт в x86), поэтому длинные инструкции могут сначала запланировать более короткие инструкции, чтобы сохранить трубопровод декодирования полным. Это тонкий
Сложность оптимизации Peephole
Богатый набор инструкций CISC требует сложных оптимизаторов пикселей, которые могут распознавать высокоуровневые шаблоны. Например, последовательность, подобная , может быть заменена одним , если компилятор проверяет, что флаги состояний не используются в другом месте. Это преобразование сохраняет две инструкции и снижает давление регистра. Однако шаблон должен быть безопасным: к местоположению памяти может быть получен доступ другим потоком или псевдонимом с другим указателем. Компиляторы должны выполнять точный анализ псевдонимов для применения таких пикселей. ISA x86 также включает в себя множество инструкций, которые имеют неявные побочные эффекты (например, модифицирует EDI и EFLAGS), что делает рискованным замену без глубоких знаний.
Современные LLVM и GCC имеют обширные проездные отверстия, которые работают во время целевого бэкэнда. Например, проездной LLVM заменяет определенные низкоуровневые шаблоны более эффективными инструкциями CISC. Этот пропуск эвристичен и должен тщательно поддерживаться, поскольку новые микроархитектуры процессора вводят различные компромиссы. Кроме того, компиляторы часто снижают ИК-инструкции до инструкций CISC на ранней стадии, чтобы обеспечить более точное соответствие шаблонам, но это может усложнить более поздние проходы, такие как планирование инструкций.
Силовые и тепловые соображения
Хотя это и не проблема компилятора как таковая, энергопотребление становится все более важным. Инструкции CISC, которые связывают несколько исполнительных блоков (например, ], которые сплавляют мультидобавленные) могут вызывать высокие динамические всплески мощности. Компиляторы, нацеленные на мобильные и встроенные процессоры x86 (например, Intel Atom), иногда избегают таких энергоемких инструкций в пользу последовательности более простых операций, даже если это увеличивает размер кода. Решение принимается в конвейере оптимизации, часто через целевую модель затрат, которая включает бюджет мощности. Автоматическая векторизация также играет роль: использование инструкций AVX-512 может ускорить численный код, но может вызвать тепловое дросселирование, если используется слишком агрессивно. Современные компиляторы разоблачают прагмы и флаги, чтобы позволить разработчикам контролировать эти компромиссы.
Возможности: использование CISC для повышения производительности
Несмотря на сложность, богатый набор инструкций CISC предлагает уникальные возможности оптимизации, которые часто не могут соответствовать.
Специализированные инструкции для криптографических и медиа-нагрузок
Семьи CISC, такие как x86, накопили широкий спектр специализированных инструкций.
- AES-NI:, и связанные с ними инструкции ускоряют операции Advanced Encryption Standard. Компиляторы могут распознавать циклы, которые выполняют раунды AES и заменяют их этими отдельными инструкциями, достигая коэффициентов ускорения в 10-20 раз по сравнению с реализациями программного обеспечения [Intel AES-NI Optimization Guide].
- SHA расширения: и другие ускоряют алгоритмы хеширования.
- AVX-512: Сплавленные многократные добавления, рассеяния/собирания и обнаружения конфликтов могут резко ускорить HPC и векторизованный код. Компиляторы используют автовекторизации проходов для генерации этих инструкций, часто с проверками времени выполнения для поддержки процессора.
- BMI/BMI2: Инструкции по манипулированию битами (например, , ) позволяют компактно реализовывать определенные операции с битовым полем. Компиляторы для базы данных и сетевого кода могут автоматически заменять циклы этими инструкциями.
Для их использования компиляторы должны знать набор функций целевого процессора. LLVM и GCC используют проверки CPUID и аннотации атрибутов, специфичных для цели (например, ). При многочастной настройке компилятор может генерировать несколько кодовых путей и выбирать подходящий во время выполнения через многоверсию функций.
Совместимость кода и бинарный переписывание
Обратная совместимость CISC является одновременно благословением и проклятием. Для оптимизации компилятора это означает, что существующий объектный код из более старых компиляторов иногда может быть улучшен с помощью инструментов двоичного перезаписи (например, PIN-инструмент Intel или автоматические оптимизаторы, такие как BOLT). Эти инструменты выполняют оптимизацию последней мили, что компиляторы не могут легко сделать, потому что им не хватает информации о времени выполнения. Например, BOLT может переупорядочение основных блоков в функции для улучшения производительности кэша команд или замены последовательности инструкций CISC на более новое, более короткое кодирование [BOLT: Бинарная оптимизация и Инструмент для настройки]. Хотя эта экосистема не является строго оптимизацией компилятора, она использует кодирование переменной длины CISC и богатый набор инструкций для дополнительных преимуществ.
Вывод: Эволюционная роль CISC в разработке компиляторов
Влияние дизайна CISC на стратегии оптимизации компиляторов глубоко и многогранно. От выбора инструкций и плотности кода до слияния микро-операций и распределения регистров сложность CISC заставляет компиляторы использовать сложные анализы и модели затрат. Современные процессоры x86, несмотря на их наследие CISC, приняли методы, вдохновленные RISC, такие как кэши микро-операций и макро-фьюжн, размывая грань между двумя парадигмами. Компиляторы должны адаптироваться к каждой новой микро-архитектуре, балансируя использование мощных инструкций CISC с необходимостью декодирования эффективности и энергосбережения.
Заглядывая вперед, CISC, вероятно, останется актуальным через экосистему x86, в то время как ARM (дизайн RISC) получает преимущество в серверах и ноутбуках. Это означает, что составители компиляторов должны поддерживать несколько бэкэнд-мишеней, каждая со своим собственным набором компромиссов. Для разработчиков понимание того, как CISC формирует выход компилятора, является ключом к написанию кода, который может быть эффективно оптимизирован - например, с помощью внутренних функций для специализированных инструкций или путем написания циклов, которые являются дружественными к макро-фузии и кэшированию μop. наследие CISC не только в аппаратном обеспечении, но и в сложных алгоритмах компилятора, которые были разработаны для его укрощения.
Для дальнейшего чтения обратитесь к руководствам разработчиков программного обеспечения Intel® 64 и IA-32 Architectures, в которых подробно описаны все инструкции x86 и их поведение, а также к руководствам по оптимизации Agner Fog, в которых представлены таблицы микроархитектуры, используемые авторами компиляторов. Документация для серверов LLVM также предлагает понимание того, как реализуются цели CISC.