Помимо тех особенностей компилятора, которую я обозначил ранее, мы затронем ещё несколько из книги «Микропроцессоры и вычислительные комплексы семейства Эльбрус», 2013. Этот источник доступен на сайте МЦСТ в разделе Публикации. Он включает в себя 273 страницы с учётом приложений и списка литературы. Мы всё отсюда сюда копипастить не будем. Нас сейчас интересует тамошняя глава 4.2 на странице 193.
Читаем после текста: «Наряду с этим после семантического анализа введены три последовательных этапа, непосредственно связанных с процессом оптимизации.»
На этапе глобального межпроцедурного анализа и оптимизации проверяется наличие в синтаксическом дереве рекурсивных функций, которые при обнаружении преобразуются в обычные циклы. Это позволяет уменьшить риск возникновения ошибок переполнения стека. На выходе этапа формируется машинное промежуточное представление исходной программы с минимально оптимизированным кодом.
Что это вообще значит? Я постараюсь пояснить простым языком с примерами.
Скриншот 7. Рекурсия. Да, просто обычная рекурсия, тут ничего особого.
Рекурсивная функция – это функция, которая вызывает сама себя. Вспоминайте предыдущую подглаву, где мы разбирали, что у Эльбруса работа с регистрами организована в 3 стека и хорошо бы их не переполнять. Вот чтобы избежать этого и проводится оптимизация подобных вызовов за счёт преобразования рекурсивных функций в обычные циклы, которые процессору весьма понятны, и с которыми он легко отработает.
У Эльбруса смена контекста – это затратная процедура (много тактов на это уходит, много времени теряется). Для того, чтобы Эльбрус быстро обрабатывал данные, лучше всего в случае с ним на языке C или C++ использовать обычные циклы вместо рекурсии, и непрерывно подавать на него эти самые данные для обработки без переключения контекста. Т.е. в случае с Эльбрусом параллельно работающий код будет обработан намного быстрее последовательного, и именно за этим рекурсия и заменяется циклом.
На этапе глобального попроцедурного анализа и оптимизации контролируются свойства параметров и результатов функций, а также зависимости между операциями в исходной программе. По результатам анализа осуществляется оптимизация машинного промежуточного представления путем упрощения индексных выражений, устранения ненужных вычислений, удаления ненужного копирования данных, лишних обращений в память и ненужного кода. В результате формируется машинное промежуточное представление исходной программы с более оптимизированным кодом.
Я это более простым языком не распишу. Анализируется весь код и процессору подаётся на вход команда, в которой устранены все ненужные вычисления. Компилятор анализирует программу (может и весь код с опцией -fwhole) и вычищает из неё ненужное. Обратите внимание на то, что производится чистка лишних обращений в память. Если данные достаточно вытащить и считать из памяти лишь раз, программа именно это и сделает.
На следующем этапе — при планировании и распределении регистров осуществляется поиск независимых друг от друга операций исходной программы. В случае их обнаружения реализуется оптимальное отображение независимых операций исходной программы на аппаратные регистры микропроцессора. На выходе планирования и распределения регистров формируется машинное промежуточное представление исходной программы с максимально оптимизированным кодом.
Если я правильно понял, здесь описывается грамотное распределение регистров между процессами, которые могут выполняться параллельно, независимо друг от друга. Т.е. компилятор сразу строит дерево зависимостей, какие функции от каких зависят, и основываясь на этом старается распланировать выполнение независимых друг от друга операций параллельно. Вспомните пример с другом, который пошёл в магазин за продуктами: ему же без разницы, в каком порядке там искать молоко и буханку хлеба. Он просто хватает то, что первым найдёт, и всё. Если это не один человек, а двое, 1-го отправляете в магазин, а 2-го – за документом.
Далее в подглаве 4.2.2 там описаны методы распараллеливания программ. Как вы понимаете, компилятор в Эльбрусе старается задействовать все ядра процессора при выполнении той или иной задачи. Да и не только: он старается задействовать и все 6 возможных АЛУ (арифметико-логические устройства) даже в рамках одного ядра.
Наиболее полный эффект от распараллеливания на уровне операций достигается при программной конвейеризации циклов, имеющей мощную аппаратную поддержку в виде предварительной подкачки данных и других решений. Она позволяет в несколько раз повысить скорость по сравнению с последовательным выполнением итераций.
Это то, о чём мы с вами говорили ещё в прошлой главе: компилятор собирает вашу программу так, чтобы она заранее готовила процессор к подгрузке тех или иных данных. Когда эти данные нужно подгрузить, процессор уже, по сути, сделал всю необходимую работу, и подал уже готовые данные программе на исполнение.
Скриншот 8. График роста производительности в тесте Spec CPU2006 с обновлениями компилятора. Взято с Альт.
Если верить графику с сайта Альт Линукса, с каждым годом на несколько процентов, да и получается повысить скорость выполнения кода за счёт оптимизаций компилятора. На этом тут я остановлюсь. Для тех, кто хочет изучить подробнее то, как работает компилятор, как в целом устроен Эльбрус, ознакомьтесь с публикациями на сайте МЦСТ. А мы пошли дальше.