Продолжается подписка на наши издания! Вы не забыли подписаться?

Кто сегодня самый шустрый-3?

Владислав Чистяков

Исходный код тестов.

Предыдущие серии: Первая серия, Вторая серия

Итак, как и принято во всех «порядочных» сериалах (и почему только я их так ненавижу?), краткое содержание предыдущих серий. В прошлых двух номерах нашего журнала нами были протестированы основные средства разработки, а именно производительность создаваемого ими кода:

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

Какой же главный вывод можно сделать из этих тестов? Похоже, что главным выводом является то, что чисто синтетические тесты мало показательны. К таким тестам можно отнести «вызов метода», «доступ к членам класса» и Float-тест. Причем если первые два представляют чисто академический интерес, то последний довольно важен. Программы, работающие с 3D-графикой, и программы, выполняющие интенсивные инженерные расчеты, довольно сильно зависят от скорости вычислений с плавающей точкой.

Кроме этого, выяснилось, что в наш тест вкралась досадная ошибка (о ней чуть позже). В конце концов, мы пришли к мысли, что нужно создать еще один Float-тест, но на этот раз состоящий из осмысленных вычислений, которые можно проверить, а не бездумного набора операций. В качестве алгоритма мы выбрали алгоритм вычисления элементарной сплайновой кривой Catmull-Rom в трехмерном пространстве. Чтобы процессору (а у нас, если вы помните, в тесте применялся AMD Athlon 1400 с 512 Mb DDR памяти) не показалось мало, было решено вычислить 50 миллионов вершин. В качестве входных данных были взяты 4 опорные вершины. Координаты последней вычисленной вершины, которые должна быть равны x:1, y:-1, z:0, мы выводили в окно сообщения (или консоль), чтобы проверить правильность полученного результата.

StrangeAttractr(50000000);
...

double CCMFCDlg::StrangeAttractr(long iInitVal)
{
  m_spIUtility->TimerStart();
  // Опорные точки.
  double x1 = -1, y1 = -1, z1 = 0;
  double x2 = -1, y2 =  1, z2 = 0;
  double x3 =  1, y3 = -1, z3 = 0;
  double x4 =  1, y4 =  1, z4 = 0;
  // Промежуточные переменные
  double px1, py1, pz1;
  double px2, py2, pz2;
  double px3, py3, pz3;
  double px4, py4, pz4;

  double ansx, ansy, ansz;

  long iOptCnt = iInitVal;//количество рассчитываемых точек
  double t = 0;
  double dt = 1.0 / iOptCnt; // инкремент параметра t на каждой итерации
  double n0, n1, n2, n3; // Коэффициент сплайна

  for (long i = 0; i < iOptCnt; i++) 
  {
    
    n0 = (-t * ((1 - t) * (1 - t))) / 2;
    n1 = (2 -5 * t * t + 3 * t * t * t) / 2;
    n2 = (t / 2) * (1 + 4 * t - 3 * t * t);
    n3 = -(( t * t) / 2) * (1 - t);

    px1 = x1 * n0; py1 = y1 * n0; pz1 = z1 * n0;
    px2 = x2 * n1; py2 = y2 * n1; pz2 = z2 * n1;
    px3 = x3 * n2; py3 = y3 * n2; pz3 = z3 * n2;
    px4 = x4 * n3; py4 = y4 * n3; pz4 = z4 * n3;

    ansx = px1 + px2 + px3 + px4;
    ansy = py1 + py2 + py3 + py4;
    ansz = pz1 + pz2 + pz3 + pz4;

    t += dt;
  }
  m_spIUtility->TimerEnd(/*sbsInfo*/);
  
  CString ss;
  ss.Format("Result(Last Point) is x:%f, y:%f, z:%f", ansx, ansy, ansz);
  MessageBox(ss);
  return 0;
}


Прежде чем пойти дальше, стоит уточнить, в чем же была ошибка в предыдущих тестах и к чему она приводила? Переменная цикла I (в первом Float-тесте) инициализировалась нулем и на первой итерации цикла происходило деление на ноль. Интересно, что ни один из компиляторов, вошедших в первую часть нашего обзора, ни звука не издал по этому поводу. Более того, Intel C++ compiler умудрился даже воспользоваться этой ошибкой в личных целях и занял первое место с воистину ошеломляющим результатом (0.29 секунды против 12.24 у C#, занявшего тогда второе место), более чем на порядок опередив всех конкурентов. Когда проводилась вторая часть нашего тестирования, в компанию подопытных был взят BCC. Он оказался первым, и пока что единственным компилятором, который смог внятно обнаружить деление на ноль и рассказать об этом нам. Мы переделали тест, проинициализировав i единицей, что уменьшило цикл на одну итерацию. Ввиду того, что одна итерация никак не могла повлиять на скорость вычислений, мы не стали перепроверять все тесты, а зря. Оказалось что скорость компилятора Intel C++ от этого значительно снизилась, хотя результат вычислений оказался в точности таким же. Эффект настолько неожиданный, что впору рекомендовать программистам выполнять деление на ноль в (профилактических) целях поднятия производительности :o). Измененный тест компилятор Intel C++ выполнил за 4.73 секунды, что, хотя и было очень хорошим результатом, тем не менее свергло этот компилятор с первой ступени пьедестала почета. На первое место переместился GCC, который выполнил этот тест за 3.44 секунды. Но этот результат тоже был очень похож на жульничество. Мы произвели ряд смелых экспериментов с командной строкой этого компилятора и выяснили, что преимущество в скорости достигается не за счет лучшей оптимизации самих вычислений с плавающий точкой, а за счет «раскручивания» цикла. По всей видимости, раскрутка дала возможность привести часть расчетов к константным вычислениям, и таким образом смухлевать. Почему это обман? Да потому, что в реальной жизни алгоритм не будет бесцельно накручиваться в цикле, чтобы отъесть процессорное время. Итак, при компиляции без опций -funroll-loops и -funroll-all-loops GCC показал более, чем скромный результат (14.46 секунды), уступив лишь BCC (его время было 15.12) и VB (который показал 15.13). Все это еще более затруднило выявление лидера и укрепило нас в мысли о необходимости создания еще одного менее синтетического (более осмысленного) теста.

К тому времени, когда мы занялись написанием этого теста (а вернее, отбросили около пяти вариантов из-за их непригодности) подоспели Borland C++ Builder 6.0 (BCB) и релиз VS.Net (VS7, т.е. входящие в него VC7 и C#). Естественно, что мы с радостью протестировали и их.

Ниже приведены их результаты и результаты выполнения нашего нового Float-теста.

Float-тест 2

Таблица 1. Результаты нового Float-теста.

Компилятор

Время

VC7 1.62
bcc 2.55
C# 3.14
gcc с опцией –funroll-loops 3.29
gcc 3.43
Delphi6 3.72
vc6

4.75

bcb 5.47
Java 5.73
Intel C++ 7.34
VB6 13.28

Расклад сил в новом тесте кардинально изменился. Лидером на сей раз стал VC7. Второе место, и это можно считать сенсацией, занял bcc. Он традиционно отставал во всех теста (в том числе и в первом Float-тесте), но в новом тесте вырвался на вторую позицию, причем с очень недурным результатом. На третьем месте – C#. Интересно, что в этом тесте он показал лучший результат, если не производилась прекомпиляция утилитой ngen! Мы произвели отдельное тестирование с и без использования этой утилиты, а также привели результаты, полученные на бете 2 (с ngen). Эти результаты можно увидеть в таблице 3. В любом случае столь высокий показатель «управляемого» компилятора говорит о большом потенциале платформы .Net, да и управляемых платформ (вроде Java) в целом.

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

Но все эти отставания не так интересны. Интересно другое. Во-первых, лидер первого Float-теста Intel C++ compiler скатился на предпоследнее место. Тяжело сказать, что это – случайность или закономерность, но это случившийся факт. Во-вторых, новичок нашего тестирования bcb показал обескураживающие результаты. В какой-то момент мы заметили, что bcb в отладочном режиме показывает лучшие результаты, чем в релизе! Подозрение сразу пало на оптимизатор. С выключенной оптимизацией bcb показал значительно более высокий результат (4.21 секунды), чем в режиме оптимизации по скорости (5.47 секунды). Мы перепроверили все несколько раз, но все было верно! Нужно также отметить, что выигрыш от оптимизации в других случаях был, но обычно он не превышал 10%. Для сравнения, даже VC7 при выключении оптимизации выдавал код, минимум вдвое более медленный, нежели с включенной оптимизацией.

Таблица 2. Результаты тестов Borland C++ Builder

с оптимизацией

без оптимизации

разница

Member Access 6.54 8.00 1.5
Method Call Test 9.35 10.08 0.7
Virtual Method Call 10.79 11.38 0.6
Quick sort 10.25 13.87 3.6
Buble sort 5.03 13.42 8.4
PI Computation 6.82 7.74 0.9
Tree Test 11.87 12.05 0.2
String Concatenation 34.43/12.481 34.58/19.111 0.1
Floating point Test1 15.12 17.03 1.9
Floating point Test2 5.47 4.21 -1.3

Таблица 3. Результаты тестов C#.

C#

С ngen

Без ngen

"С ngen" -
"Без ngen"

Бета 2

"С ngen" -
"Бета 2"

Member Access 2.38 2.38 0.0 3.54 -1.2
Method Call Test 1.44 1.44 0.0 2.16 -0.7
Virtual Method Call 7.92 6.90 1.0 7.22 0.7
Quick sort 9.74 9.40 0.3 9.17 0.6
Bubble sort 5.28 5.28 0.0 5.29 0.0
PI Computation 6.90 6.97 -0.1 6.90 0.0
Tree Test 18.24 18.39 -0.2 23.60 -5.4
String Concatenation 4.40 4.31 0.1 3.38 1.0
Floating point Test1 12.24 12.24 0.0 12.24 0.0
Floating point Test2 3.22 3.14 0.1    

Забавно, но бета-версия C#, которая была победителем в строковом тесте, сдала свои позиции в релиз-версии.

Таблица 4. Результаты тестов VC7.

Тест

Бета 2

Релиз

Member Access 1.44 1.43
Method Call Test 0 0
Virtual Method Call 5.77 5.76
Quik sort 8.08 8.16
Bubble sort 5.00 4.98
PI Computation 3.77 3.76
Tree Test 11.34 12.41
String Concatenation 4.11 4.02
Floating point Test1 12.26 12.23
Floating point Test2 - 1.62

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

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

Третье – новые платформы (Java и .Net), а вместе с ними и так любимая у нас Delphi, практически не уступают C++-компиляторам по скорости производимого кода, но принятые в них концепции, в частности рантайм-полиморфизм на базе виртуальных классов и нетипизированных Object-указателей замедляют конечный результат. Шаблоны, которые пока что доступны только в C++, дают преимущество в производительности. Конечно, можно вручную создавать контейнерные классы и алгоритмы для конкретных типов данных, но, из-за трудоемкости, этого попросту никто не делает, обходясь контейнерами, хранящими ссылки на виртуальные базовые классы (обычно тип Object).

Четвертое – мнение о том, что управляемые среды типа Java и .Net медленны и из-за этого не могут рассматриваться всерьез как универсальные языки, на которых можно создавать быстрое ПО, ошибочны. Конечно, эти платформы очень молоды и болезненно переживают контакты с внешним (для них) неуправляемым кодом. Но, тем не менее, они порождают быстрый код, учитывающий особенности аппаратного обеспечения. Основанием для плохого мнения об этих платформах, скорее всего, является низкая производительность графических библиотек. В Яве это старая болезнь, связанная с переносимостью, а в .Net – это наоборот, детская болезнь, связанная с тем, что в .Net в основном используется новая графическая библиотека GDI+. Она рассчитана на аппаратное ускорение, но первая ее версия является всего лишь оберткой для обычного GDI Windows. При этом все крутые операции типа градиентных заливок и сглаживания начертаний шрифтов выполняются за счет центрального процессора. Надеемся, что в будущих версиях появится аппаратная акселерация, и эти проблемы исчезнут.

Что же касается абсолютного победителя, то, скорее всего, на эту роль больше всех подходит VC7. Однако нет никакой гарантии, что завтра не найдется тест, в котором один из его конкурентов не вырвется вперед. :) Говорить же о преимуществе одного компилятора над другим еще более неверно, так как разные компиляторы одного и того же языка могут давать совершенно разные результаты. А появление платформ типа Ява и .Net способны снять с компиляторов проблемы оптимизации генерируемого года, так, MC++, хотя и является полноценным компилятором C++, но, тем не менее, порождает в основном, MSIL (байт-код платформы .Net). На сегодня больше интересен другой вопрос... какую платформу выбрать? Продолжать ли создавать обычный «неуправляемый» код или выбрать, Яву или .Net? Так что выбор средства разработки переходит в разряд выбора платформы разработки или даже стиля жизни.


Copyright © 1994-2016 ООО "К-Пресс"