Современный компилятор icl 8.0

Вступление


Рынок компиляторов весьма узок и очень не прост для вторжения. Как правило, основные игроки на нём - производители операционных систем. Они же и практически всегда предлагают в дополнение к ОС свои собственные компиляторы, которые зачастую становятся де-факто стандартом для разработчиков программного обеспечения. Как, например, корпорация Microsoft со своей Visual Studio, используемой повсеместно для создания коммерческого массового софта, ориентируемого на платформу Windows. В некоторых теоретических курсах, посвящённых информационным системам, компилятор даже рассматривается как часть этой самой операционной системы.

Рассматривая ту же платформу Windows, не возможно не отметить альтернативные решения в области компиляторов, предлагаемые под торговой маркой Borland, (так, наверное, сейчас правильнее говорить). История этих решений ещё дольше, чем история самой Windows. Десятилетиями разработчики совершенствовали свой продукт, постепенно был создан целый гигантский комплекс средств разработки приложений, обросший немыслимым количеством библиотек и функций на любой случай.

А знаете, что ещё есть? Полностью бесплатные компиляторы, да ещё предоставляемые в исходниках. Эти компиляторы умеют сами себя компилировать для создания исполняемого файла.

Но и это ещё не всё, кроме крупных производителей средств разработки есть ещё россыпь мелких, невесть как прицепившихся к постному пирогу компиляторного рынка. Например, разработчики с http://www.codeplay.com/ подрядились производить компилятор для PlayStation, но и не забывают про x86. На сайте можно скачать бесплатную ознакомительную версию, которая умеет интегрироваться в Visual Studio. Но будьте с ней аккуратны: некоторые прошлые версии часто вели себя, как птенцы кукушки, стараясь выкинуть из "теплого гнезда" Visual Studio другие компиляторы.

Есть ещё не очень известный в нашей стране http://www.metrowerks.com/. Эта фирма тоже специализируются на компиляторах и вспомогательных средствах разработки. Даже непонятно, что помешало их продукту завоевать популярность у нас. Наверное, то, что это не Delphi, и не MSVC. Нечто среднее, ни то, ни сё, - ни рыба, ни мясо.

И как в таких условиях независимому разработчику протолкнуть свой новый продукт в узкие воротца этого переполненного дешевого базара? Одной фирме удалось найти изящное решение. Раньше бытовало мнение, что главное для компилятора - чтобы он умел создавать программы, хорошо взаимодействующие с ОС. Многие ругали, например, Delphi, за то, что она "не благородного происхождения", не произведена MS, а, следовательно, ограничена в правах, как Золушка. Но одной только интеграцией с ОС сыт не будешь, есть ещё такое понятие, как производительность. Компилятор должен стараться производить как можно более быстрый код.

Компиляторы и тестируют по нескольким показателям: скорости кода, объёму исполняемого файла и времени компиляции. Сейчас второй показатель постепенно сходит на нет. Третий тоже не так важен, как раньше: появились специальные режимы быстрой компиляции, проверки синтаксиса программы без её непосредственной длительной компиляции. А вот первый параметр по-прежнему актуален для многих программ. Как можно увеличить скорость работы своего компилятора, чтобы сделать его более привлекательным для потенциальных потребителей? Улучшить его, и все дела. Но это же слишком прямолинейное решение, никакого изящества. Есть более интересные идеи, можно же улучшать свои результаты относительно чужих. Можно подойти к проблеме производства компиляторов с другого конца, улучшать не компилятор для создания более быстрого кода для выполнения процессором, а подогнать процессор, чтобы он более быстро выполнял код, созданный твоим компилятором. Сказано - сделано. Итак, на свет божий было выпущено немыслимое количество неудобных в программировании капризных процессоров, обладающих новыми, никому непонятными фичами, со странными закодированными именами. Процессоры быстро заполонили всё вокруг, и потребители потянулись за компилятором, умеющим создавать быстрый код для модных процессоров. Итак, переходим к рассмотрению сего чудесного компилятора.

Обзор


Видимо, не стоит пересказывать весь набор опций icl, тем более, что профессионалы и так его знают. Рассмотрим в целом, что предлагает современный оптимизирующий компилятор разработчикам программного обеспечения, при этом отдельно остановимся на нововведениях восьмой версии.

Опции интеграции с Visual Studio рассматривать так же не будем, компилятор легко встраивается в оболочку, и может быть выбран как основной. Причём, как для всего проекта, так для отдельных входящих в проект файлов с исходным кодом. Это может быть очень полезно в некоторых случаях. Об опциях же программной совместимости с VS поговорим отдельно.

Оптимизация


Сразу начнём с наиболее интересного - ради чего, собственно, и был создан этот компилятор. Какие интересные возможности предлагаются в плане оптимизации программ? Первый набор опций задаёт целевую архитектуру, точнее, поколение процессоров для работы программы. Pentium, Pentium-II, и так далее. Программа компилируется с оптимизацией в расчёте на быстрейшее выполнение именно на данном типе процессоров. Однако при этом программа будет работать на всех процессорах, то есть, программа не использует новые специфические инструкции, появившиеся в данном процессоре, а оперирует только общим для всех процессоров набором инструкций.

За счёт чего может тогда достигаться выигрыш производительности? Например, некоторые инструкции могут выполняться на новых процессорах медленнее, чем на старых, а некоторые, наоборот, на старых - медленнее, чем на новых. Таким образом, из общего пула инструкций для выполнения одинаковой задачи для различных процессоров иногда стоит выбирать различные инструкции. Вот, хороший пример: инструкции inc и dec, увеличения и уменьшения, соответственно, на единичку операнда. Они были введены в x86 давным-давно, ещё до Pentium, для оптимизации часто встречающихся операций инкрементирования и декрементирования переменных. Чтобы вместо громоздкой операции add, тогда довольно медленной (тогда все инструкции были медленные или очень медленные) - выполнить простую инструкцию. Однако, для современных процессоров эти инструкции не ускоряют выполнение программы, а, наоборот, замедляют. Дело в том, что эти инструкции в зависимости от своего результата, например, 0 и т.п., обновляют внутренний регистр флагов процессора лишь частично, оставляя там следы выполнения предыдущих инструкций. В этом они отличаются от инструкций add/sub, которые полностью перезаписывают регистр флагов. И эта частичная перезапись может мешать конвейерному выполнению инструкций современными процессорами, так как порождает ложные зависимости между инструкциями. Результат старых инструкций, который должен был затереться, и не приниматься во внимание, мешает считать новые в последовательности инструкции независимыми и начинать их параллельную обработку.

Вот, кстати, многие старые программы как раз набиты этими инструкциями, и сверх большого роста производительности старых приложений на новых процессорах ожидать не приходится.

Ещё пример - для Pentium 4 лучше заменять целочисленное умножение на небольшую константу последовательностью сложений и сдвигов, так как целочисленное умножение выполняется в этом процессоре в блоке FPU, и лишняя пересылка данных сильно замедляет эту операцию. А на Pentium-III такого нет.

Не эксклюзивная оптимизация может также учитывать некоторые особенности внутренней архитектуры процессора для наибыстрейшего исполнения команд. Например, формирования последовательности инструкций для наиболее плотной загрузки всех исполнительных блоков процессора. Например, для Pentium-III был оптимален шаблон 4-1-1, одна сложная инструкция идёт в связке с двумя простыми.

Какого прироста производительности можно ожидать от применения не эксклюзивной оптимизации? Смело можно ожидать несколько (не много) десятков процентов прироста, однако, может вообще и не быть никакого прироста, если производительность программы ограничена производительностью памяти или большим количеством случайных ветвлений. Но отдельные функции могут ускориться довольно значительно.

Эксклюзивная оптимизация


Эксклюзивная оптимизация заключается в задействовании при создании программного кода инструкций, имеющихся только у определённого типа процессора. То есть, программа просто не будет работать на ранних моделях процессоров, или на современных - в общем, тех, которые не поддерживают данную систему команд. Например, использование SSE сделает невозможным запуск программы на процессорах Pentium-II и более ранних.

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

Кстати, можно для удобства компилировать несколько вариантов не всей программы, а только отдельных функций.

Как некоторым это ни покажется странным, определение сделано "честно", то есть, если программа имеет код, оптимизированный под SSE вариант, то он автоматически выберется на всех процессорах, поддерживающих SSE, а не только на Pentium. Это "криворукие" программисты определяют фичи по имени процессора (если процессор называется Pentium-III или Pentium 4, то он поддерживает SSE). Для определения поддержки определённого набора инструкций в процессоре есть специальный регистр флагов. Вот, был известный случай: Quake III, оказывается, содержал два варианта - один эксклюзивно оптимизированный под SSE с помощью icl, а другой - оптимизированный под 3DNow! при помощи неизвестного компилятора. И вот, на AthlonXP автоматически выбирался 3DNow! вариант, хотя этот процессор так же поддерживал и SSE. А оптимизация под 3DNow! оказалась хуже, видимо, так как SSE-оптимизированный вариант компилировался при помощи компилятора, который сам хорошо понимает SSE. Таким образом, на AthlonXP выполнялся не самый быстрый вариант. И вот очень смешно оказалось, как обозреватели сравнивали показатели Pentium 4 и AthlonXP и гадали, почему же Pentium 4 быстрее, то ли из-за более быстрой шины, то ли или из-за размера кэша. А сравнивалась на самом деле производительность двух различных программ.

Рассмотрим теперь, что же может дать в плане прироста скорости автоматическая оптимизация под дополнительные наборы инструкций (собственно, их названия широко известны: MMX, SSE, SSE2, сейчас вот появилось SSE3).

Какой в принципе прирост скорости может обеспечить SIMD оптимизация, обсуждено в этой статье. Сейчас же обратим внимание именно на возможности автоматической оптимизации. Оставим пока что за скобками способности icl по автоматическому использованию векторных SIMD инструкций, поскольку эта возможность заслуживает отдельного разговора. Но что может дать простая перекомпиляция программы с использованием набора инструкций SSE в случае использования в программе вещественных чисел одинарной точности, или SSE2 - в случае использования чисел двойной точности? На самом деле, не так уж и мало, пару десятков процентов вполне может дать, в отдельных случаях - и больше. То есть, некоторая отдельная функция может значительно ускориться, но на общую интегральную производительность программы это может слабо повлиять.

На чём зиждется ускорение? Регистровый файл SSE имеет удобную прямую адресацию, в отличие от не очень удобного стека x87 FPU. И компилятор легко может использовать новые регистры для хранения промежуточных данных, уменьшая количество используемых инструкций и обращений к памяти. Дополнительный эффект даёт возможность исполнения параллельно большего числа независимых инструкций. Это также заслуга плоского регистрового файла, когда несколько операций могут иметь своими операндами данные из различных регистров.

Ещё один частный, но важный момент заключается в наличии в SSE быстрой инструкции преобразования вещественных чисел в целые. На x87 стандартный компилятор часто использует целую функцию для конвертирования, а при использовании SSE это достигается всего одной инструкцией.

Автоматическая оптимизация под MMX и целочисленные инструкции SSE - довольно трудна, так как требует от программиста придерживаться специальных правил при написании текста программы, и актуальна в специальных случаях, так как мало кто складывает между собой массивы байтов. Это уже скорее относится к SIMD векторизации. Однако icl в своём составе содержит варианты стандартных библиотечных функций, оптимизированных под MMX. Например, часто употребительные функции копирования областей памяти и обнуления массива. Так что, даже если вы вообще не используете целочисленные данные в программе, то всё равно может иметь смысл откомпилировать программу в расчёте на процессор с поддержкой MMX.

Кстати, есть и стандартные библиотечные математические функции, оптимизированные с использованием SSE и SSE2. Если вам не нужна очень большая точность, то можно для вычисления некоторых математических функций использовать не FPU, а инструкции SSE. Например, обратный квадратный корень существенно быстрее считается на SSE, так как в состав SSE входит быстрая табличная функция нахождения приближённого значения этого корня. И полученное приближённое значение затем можно быстро довести до необходимой точности итеративными алгоритмами. Получится быстрее, чем на FPU, так как в процессе выполнения инструкций процессор не будет задействовать микрокод, несколько нарушающий выполнение общего потока инструкций. Список оптимизированных математических функций различной точности можно легко найти в help-файле к компилятору.

Можно, кстати, в некоторых случаях и просто довольствоваться приближёнными значениями, их погрешность составляет меньше, чем 1.5*2^(-12).

Итак, мы окинули взглядом возможности по автоматической эксклюзивной оптимизации программ с использованием скалярных инструкций SSE и SSE2. Можно ещё упомянуть эксклюзивную оптимизацию под Pentium-II (и более новые процессоры), в которых добавились несколько специальных инструкций cmove (conditional move), заменяющих короткие простые ветвления вида:

int a,b,c,d,e;
if (a>b)
c=d;
else
c=e;

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

Так что имеет смысл для увеличения производительности откомпилировать вариант программы с эксклюзивной оптимизацией под SSE, и, таким образом, MMX и cmove, и ещё, возможно, сделать вариант для совместимости с более старыми процессорами. Все современные процессоры поддерживают SSE, это и Pentium-III, и AthlonXP. Может пострадать только производительность на простом Athlon1200 без SSE. Он и сейчас достаточно мощный даже по сравнению с новыми процессорами, но вот отсутствие SSE снижает его ценность. Можно комбинировать уровни не эксклюзивной и эксклюзивной оптимизации, задать, например, не эксклюзивную оптимизацию в расчёте на Pentium 4, она же хорошо работает на AthlonXP, а эксклюзивную установить на уровне Pentium-II.

Очень приятно, что программисту для этого практически ничего не потребуется, всю работу взвалит на себя оптимизирующий компилятор.

Специальные опции


Настала пора поговорить о специальных опциях оптимизирующего компилятора. В первую очередь, о контроле над соблюдением стандартов вычислений с использованием вещественных чисел. Оптимизирующий компилятор любит изменять порядок вычислений в сложных формулах, но, поскольку в машинной арифметике a*(b*c) не всегда равно (a*b)*c, то это может привести к изменению результатов работы программы в зависимости от оптимизации. Как правило, это свидетельствует о не очень удачном выборе алгоритма, который обладает свойством накапливать погрешность вычислений. Ведь в этом случае вы не можете быть заранее уверенными, что вам, в принципе, хватает точности представления вещественных чисел. Однако, если известно, что вычисления идут на пределе точности, имеет смысл принудительно заставить компилятор с помощью опций –Qp или более мягкой –Qprec вычислять выражения в определённом программой порядке. У компилятора есть ещё манера заменять (a/b) на (a*(1/b)) для ускорения вычислений, где-то выносить 1/b за скобки, это тоже приводит к небольшой потери точности. Это также можно отдельно запретить опцией –Qprec_div

Компилятору можно в целом задать уровень оптимизации, например, есть специальная опция –Os, включающая всю оптимизацию, кроме значительно увеличивающей размер кода с незначительным увеличением производительности.

Специальные возможности. Профилирование программ


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

Как такая информация может помочь? Во-первых, скомпилировать код для ветвлений в соответствии с собранной статистикой ветвлений, так, чтобы наиболее вероятный путь исполнения программы был предсказан процессором в соответствии со статическим механизмом предсказания ветвлений. Этот механизм не основан на сборе статистики предыдущих выполнений ветвления, и применяется когда о ветвлении ничего неизвестно. Просто предсказывается, что условные переходы вперёд по коду не выполняются, а назад - выполняются. И вот код, реализующий ветвления в программе, аранжируется в соответствии с этим механизмом, так, чтобы не часто исполняемые ветвления шли вперёд, а наиболее вероятные - назад. Естественно, для этого используется собранная при запуске специальной версии программы информация о ветвлениях.

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

Каким ещё образом может пригодиться информация о течении программного потока? Она может помочь для расположения исполняемого кода программы так, чтобы в наибольшей степени уменьшить противные Instruction Cache промахи. То есть, чтобы код программы эффективно кэшировался с помощью кэша инструкций первого уровня, или Trace Cache микроопераций в процессорах Pentium4. С этим прямо связано определение компилятором, какие функции подставлять, как inline, то есть, встраивать их код в тело программы вместо вызовов, а какие - нет, так как они вызываются относительно редко, и только без толку раздуют размер исполняемого файла.

Умный компилятор может найти и другие применения статистике работы программы, например, для оценки типичного количества итераций в отдельно взятом цикле, с целью определения потенциальной выгоды от его векторизации, паралеллизации или развёртки.

И какого же роста производительности можно ожидать от оптимизации программы с использованием собранной там образом информации? Десятка полтора процентов можно вполне ожидать, однако, как всегда, в сложной программе некоторые функции могут ускориться значительно больше других, а некоторые - ничего не приобрести. Так что интегральный эффект по всей программе может достаточно сильно варьироваться и достигать больших значений.

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

Опять-таки, достоинством этого вида оптимизации является то, что она не требует особых усилий от программиста: просто скомпилировал программу особым образом и протестировал во всех основных режимах работы.

Приготовьтесь к оптимизации


Рассмотрим теперь новые возможности восьмой версии в области профилирования. В ней добавились специальные отдельные утилиты для упрощения процесса профилирования. Первая из них, это Code Coverage Tool. Данная утилита показывает, какие именно участки исходного текста программы были исполнены в процессе тестирования, и, таким образом, для которых есть необходимая статистика исполнения, а какие - нет. Утилита принимает файлы с текстом программы и файл с собранной информацией и создаёт специальный html-файл с листингом программы, где различным цветом отмечены покрытые и не выполнявшиеся участки текста. Кстати, она так же демонстрирует статистику исполнения различных блоков программы, сколько раз какая функция вызывалась, и какая ветвь ветвления выполнялась. Это может быть само по себе очень полезно для исследования поведения программы даже в отрыве от оптимизации с использованием профилирования.

Вторая утилита называется Test-prioritization Tool. Она используется для упрощения анализа больших программ. Она принимает в качестве параметров несколько файлов с собранной информацией, полученной при запуске программы с различными исходными данными, и показывает, насколько сравнительно эффективными были различные тесты. Некоторые из них могли дублировать друг друга, то есть, покрывать одни и те же участки кода. Причём, она может выявлять не просто повторения, а и более сложные случаи, когда один тест является лишним, так как два других в совокупности полностью покрывают задействованные этим тестом участки кода. Причём, именно в совокупности, то есть, по отдельности не покрывают, а вместе - покрывают.

Нововведения восьмой версии


Раз уж зашла речь об отличиях новой версии, отметим, что разработчики icl вроде положили конец халяве, когда некоторые другие ленивые производители использовали волшебный компилятор для собственных нужд. Для оптимизации программ под собственные процессоры. Любят вот некоторые использовать чужой компилятор для, например, компиляции тестов Spec. Это такой набор тестовых программ, в основном, научно-технического свойства, который даётся в исходных кодах и компилируется отдельно специально для каждого процессора с максимальной эксклюзивной оптимизацией именно под данный процессор. И даже не считают такое использование не собственного компилятора позорным, берут просто, как будто в общежитии живут.

И вот в новой версии навели тень на плетень, поменяли опции эксклюзивной оптимизации с оптимизации под, допустим, набор SSE2, на опцию эксклюзивной оптимизации под Pentium 4. И сделали ещё дополнительно слабо документированные опции использования SSE и SSE2. Такая эксклюзивная версия для Pentium 4 не должна выполняться на каких-либо других процессорах, так что, теперь, чтобы сделать оптимизированную версию для других процессоров с SSE2, надо подумать. Таким образом, если раньше "халявщики" просто позорились, то теперь они могут быть вообще вынуждены использовать старую версию icl. Будем надеяться, что это подтолкнёт их к действиям по созданию собственного "волшебного" компилятора.

Заключение


Итак, первая часть рассказа о современном компиляторе на примере icl 8.0 подошла к концу, во второй части мы поговорим о других специальных возможностях в области оптимизации. В первую очередь, рассмотрим возможности по автоматической параллелизации программ с использованием SIMD инструкций и multi-threading. Проведём небольшой тест компиляторов.

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