Найти в Дзене

Секреты C++

Процесс преобразования исходного текста в исполняемый модуль (СОМ- или ЕХЕ-файл) состоит из нескольких последовательных шагов. Сначала C-препроцессор вставляет include-файлы и осуществляет макроподстановку. Затем лексический анализатор "разбирает" программу на наименьшие единицы компиляции - лексемы (правда, в литературе по языку C++ они обычно называются токены, от англ. "tokens").

Процесс преобразования исходного текста в исполняемый модуль (СОМ- или ЕХЕ-файл) состоит из нескольких последовательных шагов. Сначала C-препроцессор вставляет include-файлы и осуществляет макроподстановку. Затем лексический анализатор "разбирает" программу на наименьшие единицы компиляции - лексемы (правда, в литературе по языку C++ они обычно называются токены, от англ. "tokens"). После этого наступает очередь синтаксического анализатора, который пытается понять, что означают эти токены именно здесь, и наконец, начинается непосредственно генерация исполняемого кода. При этом, современные компиляторы не генерируют код сразу! Первым делом они изучают код и (если вы их попросили) пытаются его оптимизировать.

Под оптимизацией понимается не только использование коротких переходов и вызовов подпрограмм там, где это возможно, но и более интеллектуальная деятельность. Так, например, компилятор может вынести инвариант цикла за скобки, перегруппировать цепочку вычислений, чтобы не перевычислять общие подвыражения и т.д. Следовательно, для понимания причины различия во времени выполнения одной и той же программы, полученной с помощью различных компиляторов, необходимо посмотреть на код, который они сгенерировали.


Все компиляторы использовались с ключами максимальной оптимизации. В общем случае это приводит к тому, что код тяжело отождествить с программой, поскольку оптимизатор пытается его улучшить. К счастью, компиляторы фирм Borland и Microsoft предоставляют возможность вывести полученный код в отдельный файл, а с компилятором Watcom поставляется утилита, преобразующая OBJ-файл в ассемблеровский листинг. Для экономии места я не буду приводить полные листинги программ, лишь ту часть, которая иллюстрирует предмет сегодняшней заметки.


Код для инициализации large-массива с помощью указателя. cnt и p_large существуют только в регистрах. Cnt, вообще говоря, есть в неявной форме в цикле loop, который декрементирует регистр СХ SIZE раз. То есть компилятор догадался произвести обратный отсчет.
Следующий пример, это инициализация huge-массива с помощью указателя. cnt и здесь не хранится, но работа немного осложнилась нормализацией (выделено * - звёздочкой). _AHINCR - внутренняя константа компилятора, равная 0fffh. Со смещением ничего не делают, поскольку оно и так обнулится при переходе через 0ffffh.
Идём дальше. Инициализация large-массива с помощью индекса. Можно задаться вопросом: почему этот вариант компактнее, чем первый код, а работает медленнее? Ответ прост - там используется цикл loop, работающий быстрее, чем последовательность. Не буду приводить участок кода Microsoft С, инициализирующий huge-массив с помощью индекса. Скажу лишь, что и там нормализация осуществляется непосредственно при модификации указателя.


А теперь взглянем на код, который генерирует
Watcom С (инициализация large-массива с помощью указателя). Во внутреннем цикле неявно присутствует cnt (в регистрах), кроме того, i реализована как локальная переменная в оперативной памяти. Это и служит главной причиной отставания Watcom-кода от Microsoft-кода. Кроме того, несмотря на то, что заранее известно, сколько раз будет выполняться цикл по cnt, всё равно генерируется стандартная форма цикла с предусловием. Обратите внимание: Microsoft этого не делает!
Теперь посмотрим на Borland и попробуем понять, почему с индексом он работает быстрее. Пример кода для инициализации large-массива, с помощью указателя (Borland C++ ). Все временные переменные хранятся в памяти. Все циклы выполняются с проверкой предусловия. Вероятно, Borland предполагает ручную оптимизацию, например, мы могли написать, что i и cnt - register. Теперь посмотрим, почему с использованием индекса работа идёт быстрее. Пример кода для инициализации large-массиаа с помощью индекса (Borland С++). Оказывается, использование индекса освободило Borland от необходимости обращаться к указателю и, следовательно, дало выигрыш в скорости. Причина аномального отставания Borland от Watcom на huge-модели заключается в слепом следовании инструкции: в языке С параметры подпрограммам передаются на стеке. Но это не одна из заповедей, да и подпрограмма нормализации - внутреннее, глубоко интимное дело компилятора.


Тем не менее, Borland аккуратно "заталкивает" длинный указатель в стек и вызывает подпрограмму нормализации, в то время как более сообразительный Watcom передает указатель в регистрах, а умница Microsoft просто вставляет код нормализации.