Сравнительный анализ компиляторов С++

ОГЛАВЛЕНИЕ

К сожалению, выбор компилятора часто обусловлен, опять-таки, идеологией и соображениями вроде "его все используют". Конечно, среда разработки Microsoft Visual C++ несколько более удобна, чем у портированного gcc - но это ведь вовсе не значит, что релиз своего продукта вы должны компилировать с использованием MSVC++. Используйте оболочку, компилируйте промежуточные версии на MSVC++ (кстати, время компиляции у него гораздо меньше, чем у gcc), но релиз можно собрать с использованием другого компилятора, например от Intel. И, в зависимости от компилятора, можно получить прирост в производительности на 10% просто так, на ровном месте. Но какой "правильный" компилятор выбрать, чтобы он сгенерировал максимально быстрый код? К сожалению, однозначного ответа на этот вопрос нет - одни компиляторы лучше оптимизируют виртуальные вызовы, другие - лучше работают с памятью.

Попробуем определить, кто в чем силен среди компиляторов для платформы Wintel (x86-процессор + Win32 ОС). В забеге принимают участие компиляторы Microsoft Visual C++ 6.0, Intel C++ Compiler 4.5, Borland Builder 6.0, MinGW (портированный gcc) 3.2.

Порядок тестирования

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

Вроде все просто - но тут начинают возникать определенные проблемы. Провести тестирование некоторых конструкций (например, обращение к полю объекта) не удастся из-за оптимизации на уровне компилятора: строки типа for (unsigned i=0;i<10000000;i++) dummy = obj->dummyField; все компиляторы просто выбросили из конечного бинарного кода.

Вторым неприятным моментом является то, что в результаты всех тестов неявно вошло время выполнения самого цикла "for", в котором происходит набор статистики. В некоторых реализациях оно может быть очень даже существенным (например, два такта на одну итерацию пустого for для gcc). Измерить "чистое" время выполнения пустого цикла удалось не для всех компиляторов - VC++ и Intel Compiler выполняют достаточно хорошую "раскрутку" кода и исключают из конечного кода все пустые циклы, inline-вызовы пустых методов и т.д. Даже конструкцию вида for (unsigned i=0;i<16;i++) dummy++; VC++ реализовал как dummy += 16;.

Наличие такой нетривиальной низкоуровневой оптимизации наводит на мысль о необходимости анализа сгенерированного кода на уровне ассемблера. Во-первых, это позволит убедиться в том, что мы действительно измерили то, что хотели измерить (а не оптимизированный компилятором пустой цикл, из которого он выбросил все "лишние" вызовы). Во-вторых, это позволит более точно определить, чей код наиболее оптимален, что существенно дополнит картину тестирования.

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

Для измерения времени выполнения тестов использовался счетчик машинных тактов, доступный по команде процессора RDTSC, что позволило не только сравнить время выполнения большого количества однотипных операций, но и получить приближенное время выполнения операции в тактах (вторая величина является более показательной и удобной для сравнения). Все тесты проводились на Pentium III (700 МГц), параметры компиляции были установлены в "-O2 -6" (оптимизация по скорости + оптимизация под набор команд Pentium Pro). Кроме того, для Borland Builder была добавлена опция --fast-call - передача параметров через регистры (Intel Compiler, MSVC++ и gcc автоматически используют передачу параметров через регистры при использовании оптимизации по скорости).

Тестирование было разделено на несколько независимых частей. Первая - тестирование скорости работы основных конструкций языка (виртуальные вызовы, прямые вызовы и т.д.). Вторая - тестирование скорости работы STL. Третья - тестирование менеджера памяти, поставляемого вместе с компилятором. Четвертая - разбор ассемблерного кода таких базовых операций, как вызов функции и построения цикла. Пятая - сравнение времени компиляции и размера выполняемого файла.