Современный компилятор icl 8.0. Часть 2

Вступление


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

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

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

Тест 1


Уже очень давно идёт дискуссия о том, что действительно ли язык программирования C++ медленнее, чем чистый C. Она возникла с самого момента появления C++ и продолжается до сих пор. Утверждается, что программы, использующие расширенные синтаксические возможности C++, выполняются значительно медленнее, чем программы, написанные на чистом C. Собственно, всё, что можно написать на C++, можно легко реализовать и на С, вручную развернув конструкции С++. Это, правда, достаточно трудоёмко, и получается менее ясный текст, но, якобы, за счёт этого достигается выигрыш производительности, так как компилятору трудно эффективно реализовать сложные конструкции C++. Вот замедление программ С++ якобы и происходит из-за генерации компилятором дополнительного лишнего кода.

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

Итак, для сравнения качества компиляторов проверим их способность создавать эффективный C++ код. Рассмотрим следующую типичную и нужную задачу: создание удобной библиотеки небольших матриц. Матрицы применяются практически везде, например, в задачах компьютерной трёхмерной графики. Компьютерные игры и видеодрайверы наполнены операциями с матрицами. Пусть у нас есть переменные типа Matrix и хочется иметь возможность удобно ими оперировать, то есть, чтобы можно было написать

Matrix a,b,c,d,e,f;
a=b+c*d-e*f;


Что-то в таком духе, C++ позволяет переопределить стандартные операторы "+", "*" и т.п., чтобы они работали не только с числами, но и с введёнными пользователем новыми типами данных. В чистом C мы так красиво не смогли бы написать, пришлось бы писать

a=sub(add(d,mul(c,d)),mul(e,f));

Видно, что использование C++ позволяет упростить программу. Но переопределение стандартных операторов накладывает дополнительную нагрузку на компилятор, поскольку он должен самостоятельно встраивать в код определённые программистом собственные функции сложения-умножения. Тут и кроется возможная причина потери производительности, поскольку компилятор не понимает, как работают подставляемые функции, и подставляет их достаточно формально.

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

Таблица 1
 Компилятор C++ (float) C (float) C++ (double) C (double) C++ (double) No SSE2 C (double) No SSE2
 icl 8.0 5171 4610 5671 4672 5015 4391
 MSVC6.0 16437 4781 23671 4891 - -
 CBuilder6.0 17219 4656 53750 22250 - -

Замеры производились на процессоре Pentium4 2400C. Опции компиляторов были установлены на максимальную производительность. Файл с текстом программы тут.

Для удобства переведём время работы в проценты относительно C++ варианта с использованием icl.

Таблица 2
 Компилятор C++ (float) C (float) C++ (double) C (double) C++ (double) NoSSE2 C (double) No SSE2
 icl8.0 100% 89% 110% 90% 96% 85%
 MSVC6.0 318% 92% 458% 96% - -
 CBuilder6.0 334% 90% 1039% 430% - -

А также приведём соотношение времени выполнения С++ и С варианта для каждого компилятора отдельно.

Таблица 3
 Компилятор C++/C (float) C++/C (double)
 icl8.0 112% 121%
 MSVC6.0 344% 484%
 CBuilder6.0 369% 242%

Итак, видно, что только в случае использования icl переход на C++ даётся относительно бесплатно с точки зрения потери производительности, 10-20%. А в остальных случаях потери уже составляют разы. CBuilder вообще оказался в данном случае некондиционен, почему-то именно в тесте с использованием вещественных чисел типа double. Даже казалось, что программа зависла.

В чём же дело? Возможно, дело не только в языковых конструкциях. Может быть, icl может каким-то волшебным образом использовать SSE2 и создавать во всех случаях быстрый код? Нет, скомпилировав программу без использования SSE2 можно убедиться, что SSE2 в данном случае - не причём. Впрочем, это и так было заметно, поскольку у всех компиляторов более-менее одинаковый результат в C варианте. Кстати, использование SSE2 в данном случае вообще привело к замедлению, хотя в прошлой части статьи мы рассматривали его преимущества. Дело в том, что тут основное время тратится на инструкцию сложения вещественных чисел, а она в SSE2 на такт медленнее, чем при использовании FPU.

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

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

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

Кстати, желательно, чтобы в данном эксперименте у процессоров был одинаковый размер КЭШа, так как в одном случае рабочие данные могут целиком помещаться в КЭШ, нивелируя зависимость от памяти, а в другом - наоборот. И сравнить результаты будет невозможно.

Желающие могут использовать этот небольшой тест для проверки других компиляторов, в первую очередь, Microsoft VisualStudio.Net. Если не боятся огорчиться. Можно так же рассмотреть зависимость сравнительного качества компиляции от исполняющего программу процессора. Можно предположить, что на равно рейтинговом AthlonXP, не говоря уже об Athlon64, картина будет более сглаженная.

Тест 2


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

Так же проведём предметное сравнение icl текущей версии 8.0 и предыдущей версии 7.1.

Таблица 4
 Опция icl7.1 icl8.0
 Od 61 63
 Специальной оптимизации нет, программа компилируется как есть.  
 O1 176 168
 Оптимизация на уменьшение размера кода и увеличение его локальности. В некоторых случаях приводит к увеличению производительность по сравнению с O2.  
 O2 195 199
 Включается простая оптимизация, без использования дополнительных наборов инструкций. Компилятор старается создавать эффективный код.  
 0 195 199
 Более агрессивная и требующая больше времени оптимизация. Компилятор может, например, трансформировать циклы.  
 +QxM + /G5 195 199
 Использование расширения команд MMX, эксклюзивная оптимизация под PentiumMMX.  
 +Qxi + /G6 195 204
 Эксклюзивная оптимизация под Pentium-II и не эксклюзивная - под Pentium-III.  
 +QxK + /G7 199 225
 Использование SSE и неэксклюзивная оптимизация под Pentium 4.  
 +Qip 199 225
 Inter procedural optimization, дополнительная оптимизация. Компилятор может самостоятельно встраивать функции в код программы вместо вызовов, передавать параметры функций через регистры, разбивать функции.  

Тестирование производилось на процессоре Pentium 4 2400С.

В реальности первое попавшееся приложение оказалось сильно ограничено производительностью памяти, так, что влияние различных опций оптимизации слабо выражено. Тем не менее, какие-то проценты от использования скалярных инструкций SSE получить можно. Так же заметно, что новая версия 8.0 улучшилась по сравнению с 7.1 в целом, и в особенности - как раз при не эксклюзивной оптимизации под Pentium 4 с использованием SSE. Она демонстрирует уже достаточно реальные 15% прироста. Поскольку это величина интегрального прироста по всей большой программе, разброс прироста производительности отдельных функций может быть значительно больше. Таким образом, вполне имеет смысл обновить версию icl на более новую. Кстати, версия 7.1, в свою очередь, тоже, по крайней мере, на десяток процентов производительнее популярной версии 6.0.

В данном случае не получилось никакого увеличения скорости от Inter procedural Optimization, а время компиляции эта опция увеличивает, так что включать её просто так смысла нет. На отдельных версиях программы её включение приводило даже к небольшому замедлению. У компилятора что-то от интенсивного анализа программы ум за разум заходил.

Автоматическая параллелизация


В icl есть опция выключения автоматической векторизации с использованием SIMD инструкций -Qvec-. Включается же векторизация автоматически при включении эксклюзивной оптимизации под SSE опциями QxK, QxW и т.п. И опция автоматической параллелизации с созданием нескольких программных потоков -Qparallel, которая по умолчанию выключена. А так же целый сонм опций выдачи диагностических сообщений об успешности или нет векторизации того или иного цикла. Понятно, что если в программе уйма векторизуемых циклов, то сотни remark захламят область вывода сообщений и ошибок компиляции.

Если же в программе совсем не наблюдается подходящих циклов, то имеет смысл выключить векторизацию, чтобы уменьшить время компиляции программы. Оно и так не маленькое, icl осуществляет компиляцию на порядок медленнее, чем VS. А разного рода автоматическая параллелизация требует серьёзного анализа исходного кода на предмет выявления зависимостей по памяти, которые могут вызвать некорректное выполнение векторизованной программы. Так что интеллектуальная оптимизация - очень ресурсоёмкая задача для современных процессоров. Неплохо было бы, если скорость компиляции была бы в десять раз больше. Вообще, icl часто используют исключительно для окончательный "релизной" сборки программы, как раз из-за его медлительности. Но во многих случаях это невозможно, к тому же, icl и компилятор VS выдают несколько отличающиеся сообщения и ошибки. К сожалению, однако, сам процесс параллелизации не векторизируешь при помощи SSE2 из-за постоянных ветвлений, и, вероятно, Hyper-Threading icl не умеет использовать. Так что, очень возможно, производительность icl, как и некоторых других компиляторов, окажется выше на Athlon64, а не на Pentium 4. По крайней мере, если экстраполировать результаты известных тестов производительности компиляторов. По странной иронии, если компилятор от Intel оказался лучшим для платформы AMD, то процессоры AMD, вероятно, лучшие для самого интеловского компилятора.

Циклы с простыми арифметическими операциями над массивами - есть любимый объект компилятора с точки зрения использования SIMD. Причём, там векторизуется не только простое сложение-умножение, в SIMD входит и операция квадратного корня и деления, и даже операции min и max. Но и это ещё не всё, в состав компилятора входят специальные SIMD-варианты некоторых элементарных математических функций типа sin и cos. Так что идеальный цикл для векторизации выглядит, например, так:

float* a,b,c;
for (i=0; i<n; i++)
a[i]=sin(b[i]*c[i]);


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

char* a,b;
int index;
index=0;
for (i=n1; i<n2; i++)
{
a[index]=b[i];
index++;
}


Это вспомогательный цикл копирования массивов, его можно было бы заменить на вызов соответствующей функции. Компилятор перед выполнением цикла вставляет проверку на различность массивов a и b, чтобы векторизированный цикл исполнялся корректно.

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

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

Полуавтоматическая векторизация


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

И вот для векторизации есть специальный набор прагм. Например, перед циклом можно написать

#pragma vector always

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

Есть более мягкая директива

#pragma vector aligned

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

Прагма #pragma ivdep - предназначена для обозначения отсутствия возможных пересечений в памяти используемых в цикле массивов. Компилятор не всегда может определить, что массивы на самом деле точно различны.

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

Автоматическая параллелизация


Для подходящих циклов умный компилятор может автоматически создавать многопоточный код, использующий несколько процессоров в многопроцессорных конфигурациях. Проблемы автоматической параллелизации во многом аналогичны проблемам векторизации, но некоторые из них более ярко выражены. В первую очередь, создание нескольких потоков требует системных вызовов, то есть, значительного времени. И это время вполне может съесть весь прирост от параллельного выполнения. Компилятор использует вероятностный эвристический анализатор для определения подходящих циклов, и можно в опциях задать вероятность, при достижении которой будет создана параллельная версия. От 0 до 100%. То есть, в случае 100% - параллелизовываться будут только абсолютно подходящие циклы.

Зато в параллелизуемых циклах могут быть ветвления и даже вызовы функций. При включении опции -Qip Inter procedural optimization компилятор может в некоторых простых случаях определить, что вызываемые функции не имеют сторонних эффектов и могут выполняться параллельно.

В программе из теста 2 автоматически параллелизовалось два цикла, тот же цикл, что и векторизовался, и ещё один цикл такого вида:

int a,b;
b=0;
for (i=0; i<n; i++)
{
a=i*i;
b+=a;
}


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

Вообще, с точки зрения параллелизации компилятор любит циклы с известным количеством итераций, чтобы он мог на этапе компиляции определить объём вычислений, и, таким образом - выгодность параллелизации.

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

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

Полуавтоматическая параллелизация


В отличие от директив векторизации, набор прагм для автоматической параллелизации имеет стандарт, который поддерживается большим количеством компиляторов для симметричных многопроцессорных систем, а не только одним icl. Этот стандарт называется OpenMP (Open Multi Processing). Он предназначен для более удобного высокоуровневого параллельного программирования и абстрагирования от API, предоставляемого операционной системой. Например, для того, чтобы в Windows создать простейшее многопоточное приложение, необходима уйма вызовов системных функций с большим количеством параметров, причём, такой код совсем не будет переносимым. OMP предоставляет гораздо более удобный механизм параллельного программирования.

Итак, пусть, например, нам требуется распараллелить построение графика функции. С помощью OMP можно просто написать:

#pragma omp parallel for
for (i=0; i<1000; i++)
{
double y;

y=f(i/1000.0);
#pragma omp critical
setpixel(i,y*1000);
}


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

В состав OpenMP входит большой, но достаточно ясный набор директив и несколько функций, они (и небольшие расширения стандарта) описаны в документации к компилятору. OpenMP включается опцией -Qopenmp. Можно, кстати, специальной опцией -Qopenmp_stubs скомпилировать многопоточную программу с директивами OpenMP в последовательном режиме как однопоточное приложение. В этом, кстати, ещё одно преимущество OpenMP перед Windows API.

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

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

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

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

Использование icl совместно с другими средами разработки


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

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

В составе новых версий Visual Studio Net появились и появляются специфические расширения стандарта С++, новые зарезервированные слова. Не все из этих нестандартных расширений поддерживаются текущей версией icl, в документации приведён список неподдерживаемых возможностей. В этой связи очень полезной оказывается возможность отдельно задавать для каждого файла с исходным текстом используемый компилятор. Таким образом, можно, например, интерфейс взаимодействия с Windows оставить за VC, а некую расчётную часть программы откомпилировать с помощью icl.

В дополнение, icl поддерживает 80 битные вещественные числа, то есть, тип long double. В отличие от VC, который трактует long double, просто как double. Поддержка включается опцией -Qlong_double. Однако, это вызывает массу несовместимостей с библиотечными функциями, и рекомендуется включать эту опцию только для отдельных файлов, например, вычислительной части.

В большинстве случаев icl успешно компилирует программу, если она компилируется и VC. Но в некоторых случаях компилятор VC создаёт код на основе не совсем корректно написанного исходного текста, а icl - нет. Тогда желательно подредактировать текст, но иногда это сделать затруднительно, например, если идёт компиляция исходных кодов библиотек. На этот случай в icl есть набор специальных опций. Как говорят разработчики icl, их компилятор совместим с VC "bug-to-bug". Опция -Qms2 включает режим полной совместимости, по умолчанию установлена опция -Qms1, включающая "most Microsoft compatible bugs". Эту совместимость на уровне ошибок можно выключить опцией -Qms0. Причём, некоторые из этих опций появились в версии 8.0, так что если вы испытывали проблемы с компиляцией при использовании icl, можно посмотреть новую версию. Есть ещё набор опций для установки совместимости с различными версиями VC, -Qvc6, -Qvc7, -Qvc7.1.

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

Можно поступить более просто и надёжно, разбив программу на несколько dll, и откомпилировав каждую своим компилятором. Можно, например, интерфейсную часть программы написать на Delphi, а расчётную - с использованием icl. Можно даже из программы на Delphi вызывать методы С++ классов, а не просто функции. Нужно только, чтобы было установлено одинаковое выравнивание переменных. Единственная проблема совместимости заключалась в различном расположении в таблице методов классов одноимённых перегруженных функций в CBuilder Delphi и VC.

Заключение


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